Compare commits

...

234 Commits

Author SHA1 Message Date
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
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
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
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
296 changed files with 8418 additions and 2428 deletions

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 = 617
versionName = '6.6.7'
versionCode = 626
versionName = '6.7.4'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -82,18 +82,19 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:57c9d26916') {
implementation('com.github.KotatsuApp:kotatsu-parsers:103f578c61') {
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.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: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,7 +104,7 @@ 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-alpha03'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0'
implementation 'androidx.work:work-runtime:2.9.0'
@@ -120,18 +121,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.8.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'
kapt 'com.google.dagger:hilt-compiler:2.51'
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'
@@ -146,19 +147,19 @@ dependencies {
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:20240205'
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'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.51'
}

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

@@ -145,6 +145,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 +221,27 @@
<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" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"

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

@@ -14,7 +14,6 @@ import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import com.google.android.material.R as materialR
@@ -26,7 +25,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
return
}
supportActionBar?.run {

View File

@@ -27,7 +27,6 @@ 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.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import javax.inject.Inject
@@ -45,13 +44,7 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!catchingWebViewUnavailability {
setContentView(
ActivityBrowserBinding.inflate(
layoutInflater,
),
)
}) {
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
return
}
supportActionBar?.run {

View File

@@ -55,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>
@@ -83,7 +83,7 @@ open class BaseApp : Application(), Configuration.Provider {
processLifecycleScope.launch(Dispatchers.Default) {
setupDatabaseObservers()
}
workScheduleManager.get().init()
workScheduleManager.init()
WorkServiceStopHelper(workManagerProvider).setup()
}

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

@@ -30,6 +30,7 @@ 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.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
@@ -48,20 +49,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 = 19
@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 +93,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 +115,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration15To16(),
Migration16To17(context),
Migration17To18(),
Migration18To19(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room

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

@@ -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,16 @@ 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())
}

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,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
@@ -189,7 +190,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 +227,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
@@ -26,6 +27,7 @@ import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.find
import org.koitharu.kotatsu.parsers.util.isNumeric
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
@@ -70,6 +72,9 @@ 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) }
@@ -101,14 +106,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)
@@ -180,11 +192,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)
@@ -266,6 +280,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 +377,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 +416,15 @@ 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)
fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
}
@@ -407,6 +437,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 +492,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"
@@ -476,8 +514,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_GRID_SIZE = "grid_size"
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 +534,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"
@@ -518,7 +560,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 +574,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 +604,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"
@@ -568,8 +614,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_CF_GRAYSCALE = "cf_grayscale"
const val KEY_IGNORE_DOZE = "ignore_dose"
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

@@ -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> :
@@ -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

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

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

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

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

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

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

@@ -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.colorSurface)
}
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

@@ -27,7 +27,6 @@ import android.provider.Settings
import android.view.View
import android.view.ViewPropertyAnimator
import android.view.Window
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IntegerRes
import androidx.annotation.WorkerThread
@@ -216,21 +215,6 @@ 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 Context.checkNotificationPermission(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else {

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 == "" })
.lifecycle(lifecycleOwner)
.crossfade(context)
.target(this)

View File

@@ -19,6 +19,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
@@ -72,7 +73,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 +88,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

@@ -2,7 +2,6 @@ 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
@@ -115,8 +114,8 @@ private val reportableExceptions = arraySetOf<Class<*>>(
)
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
@@ -24,13 +23,11 @@ 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 {

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,22 @@
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),
)
}
}

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

@@ -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(
@@ -61,3 +65,22 @@ fun MangaDetails.mapChapters(
}
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

@@ -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
@@ -136,7 +138,10 @@ class DetailsActivity :
},
),
)
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.containerDetails))
viewModel.onActionDone.observeEvent(
this,
ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom),
)
viewModel.onShowTip.observeEvent(this) { showTip() }
viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranch.observe(this) {
@@ -148,6 +153,7 @@ class DetailsActivity :
viewModel.isChaptersEmpty.observe(this, chaptersMenuInvalidator)
val menuInvalidator = MenuInvalidator(this)
viewModel.favouriteCategories.observe(this, menuInvalidator)
viewModel.isStatsEnabled.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator)
viewModel.branches.observe(this) {
viewBinding.buttonDropdown.isVisible = it.size > 1
@@ -185,6 +191,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 +210,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 +332,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(' ')

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) {
@@ -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

@@ -23,6 +23,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,
@@ -43,6 +44,7 @@ class DetailsMenuProvider(
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.isStatsEnabled.value
menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
)
@@ -101,6 +103,12 @@ class DetailsMenuProvider(
}
}
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

@@ -25,6 +25,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
@@ -42,6 +43,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
@@ -76,6 +78,7 @@ class DetailsViewModel @Inject constructor(
private val extraProvider: ListExtraProvider,
private val detailsLoadUseCase: DetailsLoadUseCase,
private val progressUpdateUseCase: ProgressUpdateUseCase,
private val readingTimeUseCase: ReadingTimeUseCase,
) : BaseViewModel() {
private val intent = MangaIntent(savedStateHandle)
@@ -97,6 +100,10 @@ class DetailsViewModel @Inject constructor(
val favouriteCategories = interactor.observeIsFavourite(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val isStatsEnabled = settings.observeAsStateFlow(viewModelScope + Dispatchers.Default, AppSettings.KEY_STATS_ENABLED) {
isStatsEnabled
}
val remoteManga = MutableStateFlow<Manga?>(null)
val newChaptersCount = details.flatMapLatest { d ->
@@ -169,10 +176,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 {
@@ -200,6 +218,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
@@ -298,6 +324,7 @@ class DetailsViewModel @Inject constructor(
page = 0,
scroll = 0,
percent = percent,
force = true,
)
}
}
@@ -324,6 +351,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

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.graphics.Typeface
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -7,15 +8,16 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import com.google.android.material.R as materialR
import org.koitharu.kotatsu.list.ui.model.ListModel
import com.google.android.material.R as MR
fun chapterListItemAD(
clickListener: OnListItemClickListener<ChapterListItem>,
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
) = adapterDelegateViewBinding<ChapterListItem, ListModel, ItemChapterBinding>(
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) },
) {
@@ -26,31 +28,38 @@ fun chapterListItemAD(
bind { payloads ->
if (payloads.isEmpty()) {
binding.textViewTitle.text = item.chapter.name
binding.textViewNumber.text = item.chapter.number.toString()
binding.textViewDescription.textAndVisible = item.description()
binding.textViewDescription.textAndVisible = item.description
}
when {
item.isCurrent -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_primary)
binding.textViewNumber.setTextColor(context.getThemeColor(materialR.attr.colorOnPrimary))
binding.textViewTitle.drawableStart = ContextCompat.getDrawable(context, R.drawable.ic_current_chapter)
binding.textViewTitle.setTextColor(context.getThemeColorStateList(android.R.attr.textColorPrimary))
binding.textViewDescription.setTextColor(context.getThemeColorStateList(android.R.attr.textColorPrimary))
binding.textViewTitle.typeface = Typeface.DEFAULT_BOLD
binding.textViewDescription.typeface = Typeface.DEFAULT_BOLD
}
item.isUnread -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
binding.textViewNumber.setTextColor(context.getThemeColor(materialR.attr.colorOnTertiary))
binding.textViewTitle.drawableStart = if (item.isNew) {
ContextCompat.getDrawable(context, R.drawable.ic_new)
} else {
null
}
binding.textViewTitle.setTextColor(context.getThemeColorStateList(android.R.attr.textColorPrimary))
binding.textViewDescription.setTextColor(context.getThemeColorStateList(MR.attr.colorOutline))
binding.textViewTitle.typeface = Typeface.DEFAULT
binding.textViewDescription.typeface = Typeface.DEFAULT
}
else -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
binding.textViewTitle.drawableStart = null
binding.textViewTitle.setTextColor(context.getThemeColorStateList(android.R.attr.textColorHint))
binding.textViewDescription.setTextColor(context.getThemeColorStateList(android.R.attr.textColorHint))
binding.textViewTitle.typeface = Typeface.DEFAULT
binding.textViewDescription.typeface = Typeface.DEFAULT
}
}
binding.imageViewBookmarked.isVisible = item.isBookmarked
binding.imageViewDownloaded.isVisible = item.isDownloaded
binding.textViewTitle.drawableStart = if (item.isNew) {
ContextCompat.getDrawable(context, R.drawable.ic_new)
} else {
null
}
}
}

View File

@@ -5,22 +5,20 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class ChaptersAdapter(
onItemClickListener: OnListItemClickListener<ChapterListItem>,
) : BaseListAdapter<ChapterListItem>(), FastScroller.SectionIndexer {
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
setHasStableIds(true)
delegatesManager.addDelegate(chapterListItemAD(onItemClickListener))
}
override fun getItemId(position: Int): Long {
return items[position].chapter.id
addDelegate(ListItemType.CHAPTER, chapterListItemAD(onItemClickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(null))
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
val item = items.getOrNull(position) ?: return null
return item.chapter.number.toString()
return findHeader(position)?.getText(context)
}
}

View File

@@ -9,7 +9,9 @@ import android.view.View
import androidx.core.graphics.ColorUtils
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.core.util.ext.getItem
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import com.google.android.material.R as materialR
class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
@@ -25,6 +27,12 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
paint.style = Paint.Style.FILL
}
override fun getItemId(parent: RecyclerView, child: View): Long {
val holder = parent.getChildViewHolder(child) ?: return RecyclerView.NO_ID
val item = holder.getItem(ChapterListItem::class.java) ?: return RecyclerView.NO_ID
return item.chapter.id
}
override fun onDrawBackground(
canvas: Canvas,
parent: RecyclerView,

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.details.ui.model
import android.text.format.DateUtils
import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.core.model.formatNumber
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaChapter
@@ -10,6 +12,14 @@ data class ChapterListItem(
private val uploadDateMs: Long,
) : ListModel {
var description: String? = null
private set
get() {
if (field != null) return field
field = buildDescription()
return field
}
var uploadDate: CharSequence? = null
private set
get() {
@@ -38,13 +48,20 @@ data class ChapterListItem(
val isNew: Boolean
get() = hasFlag(FLAG_NEW)
fun description(): CharSequence? {
val scanlator = chapter.scanlator?.takeUnless { it.isBlank() }
return when {
uploadDate != null && scanlator != null -> "$uploadDate$scanlator"
scanlator != null -> scanlator
else -> uploadDate
private fun buildDescription(): String {
val joiner = StringJoiner("")
chapter.formatNumber()?.let {
joiner.add("#").append(it)
}
uploadDate?.let { date ->
joiner.add(date.toString())
}
chapter.scanlator?.let { scanlator ->
if (scanlator.isNotBlank()) {
joiner.add(scanlator)
}
}
return joiner.complete()
}
private fun hasFlag(flag: Int): Boolean {

View File

@@ -7,6 +7,7 @@ data class MangaBranch(
val name: String?,
val count: Int,
val isSelected: Boolean,
val isCurrent: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {

View File

@@ -12,6 +12,9 @@ import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
@@ -26,6 +29,9 @@ import org.koitharu.kotatsu.details.ui.DetailsViewModel
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.withVolumeHeaders
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
@@ -57,13 +63,17 @@ class ChaptersFragment :
callback = this,
)
with(binding.recyclerViewChapters) {
addItemDecoration(TypedListSpacingDecoration(context, true))
checkNotNull(selectionController).attachToRecyclerView(this)
setHasFixedSize(true)
isNestedScrollingEnabled = false
adapter = chaptersAdapter
}
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
viewModel.chapters
.map { it.withVolumeHeaders(requireContext()) }
.flowOn(Dispatchers.Default)
.observe(viewLifecycleOwner, this::onChaptersChanged)
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
binding.textViewHolder.isVisible = it
}
@@ -144,6 +154,9 @@ class ChaptersFragment :
val buffer = HashSet<Long>()
var isAdding = false
for (x in items) {
if (x !is ChapterListItem) {
continue
}
if (x.chapter.id in ids) {
isAdding = true
if (buffer.isNotEmpty()) {
@@ -159,7 +172,13 @@ class ChaptersFragment :
}
R.id.action_select_all -> {
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
val ids = chaptersAdapter?.items?.mapNotNull {
if (it is ChapterListItem) {
it.chapter.id
} else {
null
}
} ?: return false
controller.addAll(ids)
true
}
@@ -183,7 +202,15 @@ class ChaptersFragment :
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
val selectedIds = selectionController?.peekCheckedIds() ?: return false
val allItems = chaptersAdapter?.items.orEmpty()
val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds }
val items = allItems.withIndex().mapNotNull<IndexedValue<ListModel>, IndexedValue<ChapterListItem>> { x ->
val value = x.value
@Suppress("UNCHECKED_CAST")
if (value is ChapterListItem && value.chapter.id in selectedIds) {
x as IndexedValue<ChapterListItem>
} else {
null
}
}
var canSave = true
var canDelete = true
items.forEach { (_, x) ->
@@ -207,15 +234,15 @@ class ChaptersFragment :
}
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
requireViewBinding().recyclerViewChapters.invalidateItemDecorations()
viewBinding?.recyclerViewChapters?.invalidateItemDecorations()
}
override fun onWindowInsetsChanged(insets: Insets) = Unit
private fun onChaptersChanged(list: List<ChapterListItem>) {
private fun onChaptersChanged(list: List<ListModel>) {
val adapter = chaptersAdapter ?: return
if (adapter.itemCount == 0) {
val position = list.indexOfFirst { it.isCurrent } - 1
val position = list.indexOfFirst { it is ChapterListItem && it.isCurrent } - 1
if (position > 0) {
val offset = (resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
adapter.setItems(

View File

@@ -13,7 +13,9 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -65,11 +67,12 @@ class PagesFragment :
detailsViewModel.selectedBranch,
) { details, history, branch ->
if (details != null && (details.isLoaded || details.chapters.isNotEmpty())) {
PagesViewModel.State(details, history, branch)
PagesViewModel.State(details.filterChapters(branch), history, branch)
} else {
null
}
}.observe(this, viewModel::updateState)
}.flowOn(Dispatchers.Default)
.observe(this, viewModel::updateState)
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPagesBinding {

View File

@@ -21,6 +21,7 @@ import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.formatNumber
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
@@ -306,7 +307,7 @@ class DownloadsViewModel @Inject constructor(
return chapters.mapNotNullTo(ArrayList(size)) {
if (chapterIds == null || it.id in chapterIds) {
DownloadChapter(
number = it.number.toInt(),
number = it.formatNumber(),
name = it.name,
isDownloaded = it.id in localChapters,
)

View File

@@ -4,7 +4,7 @@ import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
data class DownloadChapter(
val number: Int,
val number: String?,
val name: String,
val isDownloaded: Boolean,
) : ListModel {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.download.ui.worker
import androidx.collection.MutableObjectLongMap
import kotlinx.coroutines.delay
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
@@ -9,7 +10,7 @@ class DownloadSlowdownDispatcher(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val defaultDelay: Long,
) {
private val timeMap = HashMap<MangaSource, Long>()
private val timeMap = MutableObjectLongMap<MangaSource>()
suspend fun delay(source: MangaSource) {
val repo = mangaRepositoryFactory.create(source) as? RemoteMangaRepository ?: return
@@ -17,7 +18,7 @@ class DownloadSlowdownDispatcher(
return
}
val lastRequest = synchronized(timeMap) {
val res = timeMap[source] ?: 0L
val res = timeMap.getOrDefault(source, 0L)
timeMap[source] = System.currentTimeMillis()
res
}

View File

@@ -91,6 +91,7 @@ class DownloadWorker @AssistedInject constructor(
private val localMangaRepository: LocalMangaRepository,
private val mangaDataRepository: MangaDataRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val settings: AppSettings,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
notificationFactoryFactory: DownloadNotificationFactory.Factory,
) : CoroutineWorker(appContext, params) {
@@ -182,7 +183,7 @@ class DownloadWorker @AssistedInject constructor(
}
val repo = mangaRepositoryFactory.create(manga.source)
val mangaDetails = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = LocalMangaOutput.getOrCreate(destination, mangaDetails)
output = LocalMangaOutput.getOrCreate(destination, mangaDetails, settings.preferredDownloadFormat)
val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl }
if (coverUrl.isNotEmpty()) {
downloadFile(coverUrl, destination, repo.source).let { file ->
@@ -193,12 +194,12 @@ class DownloadWorker @AssistedInject constructor(
val chapters = getChapters(mangaDetails, includedIds)
for ((chapterIndex, chapter) in chapters.withIndex()) {
checkIsPaused()
if (chaptersToSkip.remove(chapter.id)) {
if (chaptersToSkip.remove(chapter.value.id)) {
publishState(currentState.copy(downloadedChapters = currentState.downloadedChapters + 1))
continue
}
val pages = runFailsafe {
repo.getPages(chapter)
repo.getPages(chapter.value)
} ?: continue
val pageCounter = AtomicInteger(0)
channelFlow {
@@ -237,7 +238,7 @@ class DownloadWorker @AssistedInject constructor(
),
)
}
if (output.flushChapter(chapter)) {
if (output.flushChapter(chapter.value)) {
runCatchingCancellable {
localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga())
}.onFailure(Throwable::printStackTraceDebug)
@@ -377,19 +378,26 @@ class DownloadWorker @AssistedInject constructor(
private fun getChapters(
manga: Manga,
includedIds: LongArray?,
): List<MangaChapter> {
val chapters = checkNotNull(manga.chapters) {
"Chapters list must not be null"
}.toMutableList()
if (includedIds != null) {
val chaptersIdsSet = includedIds.toMutableSet()
chapters.retainAll { x -> chaptersIdsSet.remove(x.id) }
): List<IndexedValue<MangaChapter>> {
val chapters = checkNotNull(manga.chapters) { "Chapters list must not be null" }
val chaptersIdsSet = includedIds?.toMutableSet()
val result = ArrayList<IndexedValue<MangaChapter>>((chaptersIdsSet ?: chapters).size)
val counters = HashMap<String?, Int>()
for (chapter in chapters) {
val index = counters[chapter.branch] ?: 0
counters[chapter.branch] = index + 1
if (chaptersIdsSet != null && !chaptersIdsSet.remove(chapter.id)) {
continue
}
result.add(IndexedValue(index, chapter))
}
if (chaptersIdsSet != null) {
check(chaptersIdsSet.isEmpty()) {
"${chaptersIdsSet.size} of ${includedIds.size} requested chapters not found in manga"
}
}
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
return chapters
check(result.isNotEmpty()) { "Chapters list must not be empty" }
return result
}
private suspend inline fun <T> withMangaLock(manga: Manga, block: () -> T) = try {

View File

@@ -57,7 +57,7 @@ class MangaSourcesRepository @Inject constructor(
observeIsNsfwDisabled(),
dao.observeEnabled(SourcesSortOrder.MANUAL),
) { skipNsfw, sources ->
sources.count { skipNsfw || !MangaSource(it.source).isNsfw() }
sources.count { !skipNsfw || !MangaSource(it.source).isNsfw() }
}.distinctUntilChanged()
}

View File

@@ -68,9 +68,8 @@ class FavouritesCategoryEditActivity :
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val order = savedInstanceState.getSerializableCompat<ListSortOrder>(KEY_SORT_ORDER)
if (order != null) {
selectedSortOrder = order
savedInstanceState.getSerializableCompat<ListSortOrder>(KEY_SORT_ORDER)?.let {
selectedSortOrder = it
}
}

View File

@@ -15,8 +15,8 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE,
)
]
),
],
)
data class HistoryEntity(
@PrimaryKey(autoGenerate = false)
@@ -28,4 +28,5 @@ data class HistoryEntity(
@ColumnInfo(name = "scroll") val scroll: Float,
@ColumnInfo(name = "percent") val percent: Float,
@ColumnInfo(name = "deleted_at") val deletedAt: Long,
@ColumnInfo(name = "chapters") val chaptersCount: Int,
)

View File

@@ -90,10 +90,11 @@ class HistoryRepository @Inject constructor(
.distinctUntilChanged()
}
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int, percent: Float) {
if (shouldSkip(manga)) {
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int, percent: Float, force: Boolean) {
if (!force && shouldSkip(manga)) {
return
}
assert(manga.chapters != null)
db.withTransaction {
mangaRepository.storeManga(manga)
db.getHistoryDao().upsert(
@@ -105,14 +106,12 @@ class HistoryRepository @Inject constructor(
page = page,
scroll = scroll.toFloat(), // we migrate to int, but decide to not update database
percent = percent,
chaptersCount = manga.chapters?.size ?: -1,
deletedAt = 0L,
),
)
trackingRepository.syncWithHistory(manga, chapterId)
val chapter = manga.chapters?.findById(chapterId)
if (chapter != null) {
scrobblers.forEach { it.tryScrobble(manga.id, chapter) }
}
scrobblers.forEach { it.tryScrobble(manga, chapterId) }
}
}

View File

@@ -24,6 +24,7 @@ class HistoryUpdateUseCase @Inject constructor(
page = readerState.page,
scroll = readerState.scroll,
percent = percent,
force = false,
)
}

View File

@@ -30,6 +30,7 @@ class MarkAsReadUseCase @Inject constructor(
page = pages.lastIndex,
scroll = 0,
percent = 1f,
force = true,
)
}

View File

@@ -6,7 +6,6 @@ import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
class HistoryListAdapter(
@@ -17,13 +16,6 @@ class HistoryListAdapter(
) : MangaListAdapter(coil, lifecycleOwner, listener, sizeResolver), FastScroller.SectionIndexer {
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

@@ -11,7 +11,9 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkManageIntent
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
@@ -27,6 +29,7 @@ class HistoryListFragment : MangaListFragment() {
super.onViewBindingCreated(binding, savedInstanceState)
RecyclerScrollKeeper(binding.recyclerView).attach()
addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel))
viewModel.isStatsEnabled.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
}
override fun onScrolledToEnd() = Unit

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.history.ui
import android.content.Context
import android.content.Intent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
@@ -9,6 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.stats.ui.StatsActivity
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
@@ -24,6 +26,11 @@ class HistoryListMenuProvider(
menuInflater.inflate(R.menu.opt_history, menu)
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_stats)?.isVisible = viewModel.isStatsEnabled.value
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_clear_history -> {
@@ -31,6 +38,11 @@ class HistoryListMenuProvider(
true
}
R.id.action_stats -> {
context.startActivity(Intent(context, StatsActivity::class.java))
true
}
else -> false
}
}

View File

@@ -71,6 +71,12 @@ class HistoryListViewModel @Inject constructor(
g && s.isGroupingSupported()
}
val isStatsEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_STATS_ENABLED,
valueProducer = { isStatsEnabled },
)
override val content = combine(
sortOrder.flatMapLatest { repository.observeAllWithHistory(it) },
isGroupingEnabled,

View File

@@ -83,4 +83,5 @@ class TypedListSpacingDecoration(
private fun ListItemType?.isEdgeToEdge() = this == ListItemType.MANGA_NESTED_GROUP
|| this == ListItemType.FILTER_SORT
|| this == ListItemType.FILTER_TAG
|| this == ListItemType.CHAPTER
}

View File

@@ -18,3 +18,5 @@ fun File.hasCbzExtension() = isCbzExtension(extension)
fun Uri.isZipUri() = scheme.let {
it == URI_SCHEME_ZIP || it == "cbz" || it == "zip"
}
fun Uri.isFileUri() = scheme == "file"

View File

@@ -12,6 +12,8 @@ import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.find
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSONToSet
@@ -86,19 +88,20 @@ class MangaIndex(source: String?) {
fun getCoverEntry(): String? = json.getStringOrNull("cover_entry")
fun addChapter(chapter: MangaChapter, filename: String?) {
fun addChapter(chapter: IndexedValue<MangaChapter>, filename: String?) {
val chapters = json.getJSONObject("chapters")
if (!chapters.has(chapter.id.toString())) {
if (!chapters.has(chapter.value.id.toString())) {
val jo = JSONObject()
jo.put("number", chapter.number)
jo.put("url", chapter.url)
jo.put("name", chapter.name)
jo.put("uploadDate", chapter.uploadDate)
jo.put("scanlator", chapter.scanlator)
jo.put("branch", chapter.branch)
jo.put("entries", "%08d_%03d\\d{3}".format(chapter.branch.hashCode(), chapter.number))
jo.put("number", chapter.value.number)
jo.put("volume", chapter.value.volume)
jo.put("url", chapter.value.url)
jo.put("name", chapter.value.name)
jo.put("uploadDate", chapter.value.uploadDate)
jo.put("scanlator", chapter.value.scanlator)
jo.put("branch", chapter.value.branch)
jo.put("entries", "%08d_%03d\\d{3}".format(chapter.value.branch.hashCode(), chapter.index + 1))
jo.put("file", filename)
chapters.put(chapter.id.toString(), jo)
chapters.put(chapter.value.id.toString(), jo)
}
}
@@ -162,7 +165,8 @@ class MangaIndex(source: String?) {
id = k.toLong(),
name = v.getString("name"),
url = v.getString("url"),
number = v.getInt("number"),
number = v.getFloatOrDefault("number", 0f),
volume = v.getIntOrDefault("volume", 0),
uploadDate = v.getLongOrDefault("uploadDate", 0L),
scanlator = v.getStringOrNull("scanlator"),
branch = v.getStringOrNull("branch"),

View File

@@ -5,6 +5,7 @@ import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.creationTime
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.toListSorted
@@ -71,7 +72,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
MangaChapter(
id = "$i${f.name}".longHashCode(),
name = f.nameWithoutExtension.toHumanReadable(),
number = i + 1,
number = 0f,
volume = 0,
source = MangaSource.LOCAL,
uploadDate = f.creationTime,
url = f.toUri().toString(),
@@ -99,8 +101,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.IO) {
val file = chapter.url.toUri().toFile()
if (file.isDirectory) {
file.walkCompat()
.filter { hasImageExtension(it) }
file.children()
.filter { it.isFile && hasImageExtension(it) }
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
.map {
val pageUri = it.toUri().toString()
@@ -128,14 +130,16 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
private fun String.toHumanReadable() = replace("_", " ").toCamelCase()
private fun getChaptersFiles() = root.walkCompat()
.filter { it.hasCbzExtension() }
private fun getChaptersFiles() = root.walkCompat(includeDirectories = true)
.filter { it != root && it.isChapterDirectory() || it.hasCbzExtension() }
.associateByTo(TreeMap(AlphanumComparator())) { it.name }
private fun findFirstImageEntry(): String? {
return root.walkCompat().firstOrNull { hasImageExtension(it) }?.toUri()?.toString()
return root.walkCompat(includeDirectories = false)
.firstOrNull { hasImageExtension(it) }?.toUri()?.toString()
?: run {
val cbz = root.walkCompat().firstOrNull { it.hasCbzExtension() } ?: return null
val cbz = root.walkCompat(includeDirectories = false)
.firstOrNull { it.hasCbzExtension() } ?: return null
ZipFile(cbz).use { zip ->
zip.entries().asSequence()
.firstOrNull { !it.isDirectory && hasImageExtension(it.name) }
@@ -147,4 +151,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
private fun fileUri(base: File, name: String): String {
return File(base, name).toUri().toString()
}
private fun File.isChapterDirectory(): Boolean {
return isDirectory && children().any { hasImageExtension(it) }
}
}

View File

@@ -7,6 +7,7 @@ import androidx.core.net.toFile
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.readText
import org.koitharu.kotatsu.core.util.ext.toListSorted
@@ -71,12 +72,13 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
publicUrl = fileUri,
source = MangaSource.LOCAL,
coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()),
chapters = chapters.sortedWith(org.koitharu.kotatsu.core.util.AlphanumComparator())
chapters = chapters.sortedWith(AlphanumComparator())
.mapIndexed { i, s ->
MangaChapter(
id = "$i$s".longHashCode(),
name = s.ifEmpty { title },
number = i + 1,
number = 0f,
volume = 0,
source = MangaSource.LOCAL,
uploadDate = 0L,
url = uriBuilder.fragment(s).build().toString(),
@@ -126,7 +128,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
}
}
entries
.toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name })
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
.map { x ->
val entryUri = zipUri(file, x.name)
MangaPage(
@@ -142,7 +144,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
private fun findFirstImageEntry(entries: Enumeration<out ZipEntry>): ZipEntry? {
val list = entries.toList()
.filterNot { it.isDirectory }
.sortedWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name })
.sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
val map = MimeTypeMap.getSingleton()
return list.firstOrNull {
map.getMimeTypeFromExtension(it.name.substringAfterLast('.'))

View File

@@ -4,7 +4,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
@@ -47,12 +46,12 @@ class LocalMangaDirOutput(
flushIndex()
}
override suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) = mutex.withLock {
val output = chaptersOutput.getOrPut(chapter) {
override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String) = mutex.withLock {
val output = chaptersOutput.getOrPut(chapter.value) {
ZipOutput(File(rootFile, chapterFileName(chapter) + SUFFIX_TMP))
}
val name = buildString {
append(FILENAME_PATTERN.format(chapter.branch.hashCode(), chapter.number, pageNumber))
append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) {
append('.')
append(ext)
@@ -92,9 +91,9 @@ class LocalMangaDirOutput(
}
suspend fun deleteChapter(chapterId: Long) = mutex.withLock {
val chapter = checkNotNull(index.getMangaInfo()?.chapters) {
val chapter = checkNotNull(index.getMangaInfo()?.chapters?.withIndex()) {
"No chapters found"
}.findById(chapterId) ?: error("Chapter not found")
}.find { x -> x.value.id == chapterId } ?: error("Chapter not found")
val chapterDir = File(rootFile, chapterFileName(chapter))
chapterDir.deleteAwait()
index.removeChapter(chapterId)
@@ -111,11 +110,11 @@ class LocalMangaDirOutput(
file.renameTo(resFile)
}
private fun chapterFileName(chapter: MangaChapter): String {
index.getChapterFileName(chapter.id)?.let {
private fun chapterFileName(chapter: IndexedValue<MangaChapter>): String {
index.getChapterFileName(chapter.value.id)?.let {
return it
}
val baseName = "${chapter.number}_${chapter.name.toFileNameSafe()}".take(18)
val baseName = "${chapter.index}_${chapter.value.name.toFileNameSafe()}".take(18)
var i = 0
while (true) {
val name = (if (i == 0) baseName else baseName + "_$i") + ".cbz"

View File

@@ -4,7 +4,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.internal.format
import okio.Closeable
import org.koitharu.kotatsu.core.prefs.DownloadFormat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.parsers.model.Manga
@@ -21,7 +23,7 @@ sealed class LocalMangaOutput(
abstract suspend fun addCover(file: File, ext: String)
abstract suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String)
abstract suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String)
abstract suspend fun flushChapter(chapter: MangaChapter): Boolean
@@ -35,22 +37,32 @@ sealed class LocalMangaOutput(
const val SUFFIX_TMP = ".tmp"
private val mutex = Mutex()
suspend fun getOrCreate(root: File, manga: Manga): LocalMangaOutput = withContext(Dispatchers.IO) {
val preferSingleCbz = manga.chapters.let {
it != null && it.size <= 3
suspend fun getOrCreate(
root: File,
manga: Manga,
format: DownloadFormat,
): LocalMangaOutput = withContext(Dispatchers.IO) {
val targetFormat = if (format == DownloadFormat.AUTOMATIC) {
if (manga.chapters.let { it != null && it.size <= 3 }) {
DownloadFormat.SINGLE_CBZ
} else {
DownloadFormat.MULTIPLE_CBZ
}
} else {
format
}
checkNotNull(getImpl(root, manga, onlyIfExists = false, preferSingleCbz))
checkNotNull(getImpl(root, manga, onlyIfExists = false, format = targetFormat))
}
suspend fun get(root: File, manga: Manga): LocalMangaOutput? = withContext(Dispatchers.IO) {
getImpl(root, manga, onlyIfExists = true, preferSingleCbz = false)
getImpl(root, manga, onlyIfExists = true, format = DownloadFormat.AUTOMATIC)
}
private suspend fun getImpl(
root: File,
manga: Manga,
onlyIfExists: Boolean,
preferSingleCbz: Boolean,
format: DownloadFormat,
): LocalMangaOutput? {
mutex.withLock {
var i = 0
@@ -75,10 +87,10 @@ sealed class LocalMangaOutput(
continue
}
!onlyIfExists -> if (preferSingleCbz) {
LocalMangaZipOutput(zip, manga)
} else {
LocalMangaDirOutput(dir, manga)
!onlyIfExists -> when (format) {
DownloadFormat.AUTOMATIC -> null
DownloadFormat.SINGLE_CBZ -> LocalMangaZipOutput(zip, manga)
DownloadFormat.MULTIPLE_CBZ -> LocalMangaDirOutput(dir, manga)
}
else -> null

View File

@@ -52,9 +52,9 @@ class LocalMangaZipOutput(
index.setCoverEntry(name)
}
override suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) = mutex.withLock {
override suspend fun addPage(chapter: IndexedValue<MangaChapter>, file: File, pageNumber: Int, ext: String) = mutex.withLock {
val name = buildString {
append(FILENAME_PATTERN.format(chapter.branch.hashCode(), chapter.number, pageNumber))
append(FILENAME_PATTERN.format(chapter.value.branch.hashCode(), chapter.index + 1, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) {
append('.')
append(ext)
@@ -104,7 +104,7 @@ class LocalMangaZipOutput(
}
}
}
otherIndex?.getMangaInfo()?.chapters?.let { chapters ->
otherIndex?.getMangaInfo()?.chapters?.withIndex()?.let { chapters ->
for (chapter in chapters) {
index.addChapter(chapter, null)
}

View File

@@ -0,0 +1,90 @@
package org.koitharu.kotatsu.local.ui.info
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.widget.TextViewCompat
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.DialogLocalInfoBinding
import org.koitharu.kotatsu.parsers.model.Manga
import com.google.android.material.R as materialR
@AndroidEntryPoint
class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>() {
private val viewModel: LocalInfoViewModel by viewModels()
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
return super.onBuildDialog(builder)
.setTitle(R.string.saved_manga)
.setNegativeButton(R.string.close, null)
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogLocalInfoBinding {
return DialogLocalInfoBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: DialogLocalInfoBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
viewModel.path.observe(this) {
binding.textViewPath.text = it
}
combine(viewModel.size, viewModel.availableSize, ::Pair).observe(this) {
if (it.first >= 0 && it.second >= 0) {
setSegments(it.first, it.second)
} else {
binding.barView.animateSegments(emptyList())
}
}
}
private fun setSegments(size: Long, available: Long) {
val view = viewBinding?.barView ?: return
val total = size + available
val segment = SegmentedBarView.Segment(
percent = (size.toDouble() / total.toDouble()).toFloat(),
color = KotatsuColors.segmentColor(view.context, materialR.attr.colorPrimary),
)
requireViewBinding().labelUsed.text = view.context.getString(
R.string.memory_usage_pattern,
getString(R.string.this_manga),
FileSize.BYTES.format(view.context, size),
)
requireViewBinding().labelAvailable.text = view.context.getString(
R.string.memory_usage_pattern,
getString(R.string.available),
FileSize.BYTES.format(view.context, available),
)
TextViewCompat.setCompoundDrawableTintList(
requireViewBinding().labelUsed,
ColorStateList.valueOf(segment.color),
)
view.animateSegments(listOf(segment))
}
companion object {
const val ARG_MANGA = "manga"
private const val TAG = "LocalInfoDialog"
fun show(fm: FragmentManager, manga: Manga) {
LocalInfoDialog().withArgs(1) {
putParcelable(ARG_MANGA, ParcelableManga(manga))
}.showDistinct(fm, TAG)
}
}
}

View File

@@ -0,0 +1,41 @@
package org.koitharu.kotatsu.local.ui.info
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageManager
import javax.inject.Inject
@HiltViewModel
class LocalInfoViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val localMangaRepository: LocalMangaRepository,
private val storageManager: LocalStorageManager,
) : BaseViewModel() {
private val manga = savedStateHandle.require<ParcelableManga>(LocalInfoDialog.ARG_MANGA).manga
val path = MutableStateFlow<String?>(null)
val size = MutableStateFlow(-1L)
val availableSize = MutableStateFlow(-1L)
init {
launchLoadingJob(Dispatchers.Default) {
val file = manga.url.toUri().toFileOrNull() ?: localMangaRepository.findSavedManga(manga)?.file
requireNotNull(file)
path.value = file.path
size.value = file.computeSize()
availableSize.value = storageManager.computeAvailableSize()
}
}
}

View File

@@ -7,10 +7,12 @@ import androidx.activity.OnBackPressedCallback
import androidx.annotation.IdRes
import androidx.core.view.isEmpty
import androidx.core.view.iterator
import androidx.core.view.size
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.transition.MaterialFadeThrough
import kotlinx.coroutines.Dispatchers
@@ -33,6 +35,7 @@ import org.koitharu.kotatsu.local.ui.LocalListFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
import org.koitharu.kotatsu.tracker.ui.feed.FeedFragment
import java.util.LinkedList
import com.google.android.material.R as materialR
private const val TAG_PRIMARY = "primary"
@@ -180,6 +183,9 @@ class MainNavigationDelegate(
for (item in items) {
menu.add(Menu.NONE, item.id, Menu.NONE, item.title)
.setIcon(item.icon)
if (menu.size >= navBar.maxItemCount) {
break
}
}
}
@@ -190,12 +196,15 @@ class MainNavigationDelegate(
private fun observeSettings(lifecycleOwner: LifecycleOwner) {
settings.observe()
.filter { x -> x == AppSettings.KEY_TRACKER_ENABLED || x == AppSettings.KEY_SUGGESTIONS }
.filter { x ->
x == AppSettings.KEY_TRACKER_ENABLED || x == AppSettings.KEY_SUGGESTIONS || x == AppSettings.KEY_NAV_LABELS
}
.onStart { emit("") }
.flowOn(Dispatchers.Default)
.flowOn(Dispatchers.IO)
.onEach {
setItemVisibility(R.id.nav_suggestions, settings.isSuggestionsEnabled)
setItemVisibility(R.id.nav_feed, settings.isTrackerEnabled)
setNavbarIsLabeled(settings.isNavLabelsVisible)
}.launchIn(lifecycleOwner.lifecycleScope)
}
@@ -207,6 +216,23 @@ class MainNavigationDelegate(
return null
}
private fun setNavbarIsLabeled(value: Boolean) {
if (navBar is BottomNavigationView) {
navBar.minimumHeight = navBar.resources.getDimensionPixelSize(
if (value) {
materialR.dimen.m3_bottom_nav_min_height
} else {
R.dimen.nav_bar_height_compact
},
)
}
navBar.labelVisibilityMode = if (value) {
NavigationBarView.LABEL_VISIBILITY_LABELED
} else {
NavigationBarView.LABEL_VISIBILITY_UNLABELED
}
}
interface OnFragmentChangedListener {
fun onFragmentChanged(fragment: Fragment, fromUser: Boolean)

View File

@@ -44,6 +44,12 @@ class ProtectActivity :
viewBinding.buttonNext.setOnClickListener(this)
viewBinding.buttonCancel.setOnClickListener(this)
viewBinding.editPassword.inputType = if (viewModel.isNumericPassword) {
EditorInfo.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD
} else {
EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
}
viewModel.onError.observeEvent(this, this::onError)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.onUnlockSuccess.observeEvent(this) {

View File

@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
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.parsers.util.isNumeric
import org.koitharu.kotatsu.parsers.util.md5
import javax.inject.Inject
@@ -26,6 +27,9 @@ class ProtectViewModel @Inject constructor(
val isBiometricEnabled
get() = settings.isBiometricProtectionEnabled
val isNumericPassword
get() = settings.isAppPasswordNumeric
fun tryUnlock(password: String) {
if (job?.isActive == true) {
return

View File

@@ -61,6 +61,8 @@ class WelcomeViewModel @Inject constructor(
selectedItems = selectedLocales,
isLoading = false,
)
repository.assimilateNewSources()
commit()
}
}

View File

@@ -0,0 +1,83 @@
package org.koitharu.kotatsu.reader.data
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.reader.domain.TapGridArea
import org.koitharu.kotatsu.reader.ui.tapgrid.TapAction
import javax.inject.Inject
class TapGridSettings @Inject constructor(@ApplicationContext context: Context) {
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
init {
if (!prefs.getBoolean(KEY_INIT, false)) {
initPrefs(withDefaultValues = true)
}
}
fun getTapAction(area: TapGridArea, isLongTap: Boolean): TapAction? {
val key = getPrefKey(area, isLongTap)
return prefs.getEnumValue(key, TapAction::class.java)
}
fun setTapAction(area: TapGridArea, isLongTap: Boolean, action: TapAction?) {
val key = getPrefKey(area, isLongTap)
prefs.edit { putEnumValue(key, action) }
}
fun reset() {
initPrefs(withDefaultValues = true)
}
fun disableAll() {
initPrefs(withDefaultValues = false)
}
fun observe() = prefs.observe().flowOn(Dispatchers.IO)
private fun initPrefs(withDefaultValues: Boolean) {
prefs.edit {
clear()
if (withDefaultValues) {
initDefaultActions(this)
}
putBoolean(KEY_INIT, true)
}
}
private fun getPrefKey(area: TapGridArea, isLongTap: Boolean): String = if (isLongTap) {
area.name + SUFFIX_LONG
} else {
area.name
}
private fun initDefaultActions(editor: SharedPreferences.Editor) {
editor.putEnumValue(getPrefKey(TapGridArea.TOP_LEFT, false), TapAction.PAGE_PREV)
editor.putEnumValue(getPrefKey(TapGridArea.TOP_CENTER, false), TapAction.PAGE_PREV)
editor.putEnumValue(getPrefKey(TapGridArea.CENTER_LEFT, false), TapAction.PAGE_PREV)
editor.putEnumValue(getPrefKey(TapGridArea.BOTTOM_LEFT, false), TapAction.PAGE_PREV)
editor.putEnumValue(getPrefKey(TapGridArea.CENTER, false), TapAction.TOGGLE_UI)
editor.putEnumValue(getPrefKey(TapGridArea.CENTER, true), TapAction.SHOW_MENU)
editor.putEnumValue(getPrefKey(TapGridArea.TOP_RIGHT, false), TapAction.PAGE_NEXT)
editor.putEnumValue(getPrefKey(TapGridArea.CENTER_RIGHT, false), TapAction.PAGE_NEXT)
editor.putEnumValue(getPrefKey(TapGridArea.BOTTOM_CENTER, false), TapAction.PAGE_NEXT)
editor.putEnumValue(getPrefKey(TapGridArea.BOTTOM_RIGHT, false), TapAction.PAGE_NEXT)
}
private companion object {
private const val PREFS_NAME = "tap_grid"
private const val KEY_INIT = "_init"
private const val SUFFIX_LONG = "_long"
}
}

View File

@@ -13,11 +13,11 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.InputStream
import java.util.zip.ZipFile
import javax.inject.Inject

View File

@@ -47,6 +47,7 @@ import org.koitharu.kotatsu.core.util.ext.ramAvailable
import org.koitharu.kotatsu.core.util.ext.withProgress
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -203,20 +204,23 @@ class PageLoader @Inject constructor(
val pageUrl = getPageUrl(page)
check(pageUrl.isNotBlank()) { "Cannot obtain full image url for $page" }
val uri = Uri.parse(pageUrl)
return if (uri.isZipUri()) {
if (uri.scheme == URI_SCHEME_ZIP) {
return when {
uri.isZipUri() -> if (uri.scheme == URI_SCHEME_ZIP) {
uri
} else { // legacy uri
uri.buildUpon().scheme(URI_SCHEME_ZIP).build()
}
} else {
val request = createPageRequest(page, pageUrl)
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
val body = checkNotNull(response.body) { "Null response body" }
body.withProgress(progress).use {
cache.put(pageUrl, it.source())
}
}.toUri()
uri.isFileUri() -> uri
else -> {
val request = createPageRequest(page, pageUrl)
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
val body = checkNotNull(response.body) { "Null response body" }
body.withProgress(progress).use {
cache.put(pageUrl, it.source())
}
}.toUri()
}
}
}

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.reader.domain
enum class TapGridArea {
TOP_LEFT,
TOP_CENTER,
TOP_RIGHT,
CENTER_LEFT,
CENTER,
CENTER_RIGHT,
BOTTOM_LEFT,
BOTTOM_CENTER,
BOTTOM_RIGHT;
}

View File

@@ -8,6 +8,8 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
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.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
@@ -15,9 +17,13 @@ import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.databinding.SheetChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.mapChapters
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.details.ui.withVolumeHeaders
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.parsers.model.MangaChapter
import java.time.Instant
import javax.inject.Inject
import kotlin.math.roundToInt
@@ -37,32 +43,48 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(),
override fun onViewBindingCreated(binding: SheetChaptersBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val chapters = viewModel.manga?.allChapters
if (chapters.isNullOrEmpty()) {
val manga = viewModel.manga
if (manga == null) {
dismissAllowingStateLoss()
return
}
val currentId = viewModel.getCurrentState()?.chapterId ?: 0L
val currentPosition = chapters.indexOfFirst { it.id == currentId }
val items = chapters.mapIndexed { index, chapter ->
chapter.toListItem(
isCurrent = index == currentPosition,
isUnread = index > currentPosition,
isNew = false,
isDownloaded = false,
isBookmarked = false,
)
val state = viewModel.getCurrentState()
val currentChapter = state?.let { manga.allChapters.findById(it.chapterId) }
val chapters = manga.mapChapters(
history = state?.let {
MangaHistory(
createdAt = Instant.now(),
updatedAt = Instant.now(),
chapterId = it.chapterId,
page = it.page,
scroll = it.scroll,
percent = PROGRESS_NONE,
)
},
newCount = 0,
branch = currentChapter?.branch,
bookmarks = listOf(),
).withVolumeHeaders(binding.root.context)
if (chapters.isEmpty()) {
dismissAllowingStateLoss()
return
}
val currentPosition = if (currentChapter != null) {
chapters.indexOfFirst { it is ChapterListItem && it.chapter.id == currentChapter.id }
} else {
-1
}
binding.recyclerView.addItemDecoration(TypedListSpacingDecoration(binding.recyclerView.context, true))
binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter ->
if (currentPosition >= 0) {
val targetPosition = (currentPosition - 1).coerceAtLeast(0)
val offset =
(resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
adapter.setItems(
items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset),
chapters, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset),
)
} else {
adapter.items = items
adapter.items = chapters
}
}
}

View File

@@ -8,19 +8,26 @@ import android.provider.DocumentsContract
import android.webkit.MimeTypeMap
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import java.io.File
class PageSaveContract : ActivityResultContracts.CreateDocument("image/*") {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
val intent = super.createIntent(context, input.substringAfterLast(File.separatorChar))
intent.type = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(input.substringAfterLast('.')) ?: "image/*"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val defaultUri = input.toUriOrNull()?.run {
path?.let { p ->
buildUpon().path(p.substringBeforeLast('/')).build()
}
}
intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toUri(),
defaultUri ?: Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toUri(),
)
}
return intent
}
}
}

View File

@@ -15,6 +15,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.IOException
import okio.buffer
import okio.sink
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
@@ -30,7 +31,8 @@ private const val MAX_FILENAME_LENGTH = 10
private const val EXTENSION_FALLBACK = "png"
class PageSaveHelper @Inject constructor(
@ApplicationContext context: Context,
@ApplicationContext private val context: Context,
private val settings: AppSettings,
) {
private var continuation: Continuation<Uri>? = null
@@ -44,14 +46,7 @@ class PageSaveHelper @Inject constructor(
val pageUrl = pageLoader.getPageUrl(page)
val pageUri = pageLoader.loadPage(page, force = false)
val proposedName = getProposedFileName(pageUrl, pageUri)
val destination = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { cont ->
continuation = cont
saveLauncher.launch(proposedName)
}.also {
continuation = null
}
}
val destination = getDefaultFileUri(proposedName) ?: pickFileUri(saveLauncher, proposedName)
runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.sink()?.buffer()
}?.use { output ->
@@ -62,12 +57,35 @@ class PageSaveHelper @Inject constructor(
return destination
}
private fun getDefaultFileUri(proposedName: String): Uri? {
if (settings.isPagesSavingAskEnabled) {
return null
}
return settings.getPagesSaveDir(context)?.let {
val ext = proposedName.substringAfterLast('.', "")
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return null
it.createFile(mime, proposedName.substringBeforeLast('.'))?.uri
}
}
private suspend fun pickFileUri(saveLauncher: ActivityResultLauncher<String>, proposedName: String): Uri {
val defaultUri = settings.getPagesSaveDir(context)?.uri?.buildUpon()?.appendPath(proposedName)?.toString()
return withContext(Dispatchers.Main) {
suspendCancellableCoroutine { cont ->
continuation = cont
saveLauncher.launch(defaultUri ?: proposedName)
}.also {
continuation = null
}
}
}
fun onActivityResult(uri: Uri): Boolean = continuation?.apply {
resume(uri)
} != null
private suspend fun getProposedFileName(url: String, fileUri: Uri): String {
var name = if (url.startsWith("cbz://")) {
var name = if (url.startsWith("cbz:")) {
requireNotNull(url.toUri().fragment)
} else {
url.toHttpUrl().pathSegments.last()

View File

@@ -10,11 +10,11 @@ import android.transition.TransitionManager
import android.transition.TransitionSet
import android.view.Gravity
import android.view.KeyEvent
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager
import androidx.activity.result.ActivityResultCallback
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
@@ -41,7 +41,6 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.ui.BaseFullscreenActivity
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
import org.koitharu.kotatsu.core.util.GridTouchHelper
import org.koitharu.kotatsu.core.util.IdlingDetector
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.hasGlobalPoint
@@ -56,12 +55,13 @@ import org.koitharu.kotatsu.databinding.ActivityReaderBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.reader.data.TapGridSettings
import org.koitharu.kotatsu.reader.domain.TapGridArea
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.reader.ui.tapgrid.TapGridDispatcher
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.settings.SettingsActivity
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -69,18 +69,23 @@ import javax.inject.Inject
class ReaderActivity :
BaseFullscreenActivity<ActivityReaderBinding>(),
ChaptersSheet.OnChapterChangeListener,
GridTouchHelper.OnGridTouchListener,
TapGridDispatcher.OnGridTouchListener,
OnPageSelectListener,
ReaderConfigSheet.Callback,
ReaderControlDelegate.OnInteractionListener,
OnApplyWindowInsetsListener,
IdlingDetector.Callback,
ActivityResultCallback<Uri?>,
ZoomControl.ZoomControlListener {
@Inject
lateinit var settings: AppSettings
@Inject
lateinit var tapGridSettings: TapGridSettings
private val idlingDetector = IdlingDetector(TimeUnit.SECONDS.toMillis(10), this)
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private val viewModel: ReaderViewModel by viewModels()
@@ -97,7 +102,7 @@ class ReaderActivity :
lateinit var scrollTimerFactory: ScrollTimer.Factory
private lateinit var scrollTimer: ScrollTimer
private lateinit var touchHelper: GridTouchHelper
private lateinit var touchHelper: TapGridDispatcher
private lateinit var controlDelegate: ReaderControlDelegate
private var gestureInsets: Insets = Insets.NONE
private lateinit var readerManager: ReaderManager
@@ -106,12 +111,11 @@ class ReaderActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityReaderBinding.inflate(layoutInflater))
readerManager = ReaderManager(supportFragmentManager, R.id.container)
readerManager = ReaderManager(supportFragmentManager, viewBinding.container, settings)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this)
touchHelper = TapGridDispatcher(this, this)
scrollTimer = scrollTimerFactory.create(this, this)
controlDelegate = ReaderControlDelegate(resources, settings, this, this)
viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
controlDelegate = ReaderControlDelegate(resources, settings, tapGridSettings, this)
viewBinding.slider.setLabelFormatter(PageLabelFormatter())
viewBinding.zoomControl.listener = this
ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider)
@@ -144,7 +148,7 @@ class ReaderActivity :
viewModel.isScreenshotsBlockEnabled.observe(this, this::setWindowSecure)
viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn)
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
viewModel.isBookmarkAdded.observe(this, this::onBookmarkStateChanged)
viewModel.isBookmarkAdded.observe(this, MenuInvalidator(viewBinding.toolbarBottom))
viewModel.onShowToast.observeEvent(this) { msgId ->
Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT)
.setAnchorView(viewBinding.appbarBottom)
@@ -154,6 +158,11 @@ class ReaderActivity :
viewBinding.zoomControl.isVisible = it
}
addMenuProvider(ReaderTopMenuProvider(this, viewModel))
viewBinding.toolbarBottom.addMenuProvider(ReaderBottomMenuProvider(this, readerManager, viewModel))
}
override fun onActivityResult(result: Uri?) {
viewModel.onActivityResult(result)
}
override fun getParentActivityIntent(): Intent? {
@@ -167,6 +176,11 @@ class ReaderActivity :
idlingDetector.onUserInteraction()
}
override fun onPause() {
super.onPause()
viewModel.onPause()
}
override fun onIdle() {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
}
@@ -192,41 +206,6 @@ class ReaderActivity :
viewBinding.slider.isRtl = mode == ReaderMode.REVERSED
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings -> {
startActivity(SettingsActivity.newReaderSettingsIntent(this))
}
R.id.action_pages_thumbs -> {
val state = viewModel.getCurrentState() ?: return false
PagesThumbnailsSheet.show(
supportFragmentManager,
viewModel.manga?.toManga() ?: return false,
state.chapterId,
state.page,
)
}
R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value) {
viewModel.removeBookmark()
} else {
viewModel.addBookmark()
}
}
R.id.action_options -> {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
val currentMode = readerManager.currentMode ?: return false
ReaderConfigSheet.show(supportFragmentManager, currentMode)
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
private fun onLoadingStateChanged(isLoading: Boolean) {
val hasPages = viewModel.content.value.pages.isNotEmpty()
viewBinding.layoutLoading.isVisible = isLoading && !hasPages
@@ -235,13 +214,17 @@ class ReaderActivity :
} else {
viewBinding.toastView.hide()
}
val menu = viewBinding.toolbarBottom.menu
menu.findItem(R.id.action_bookmark).isVisible = hasPages
menu.findItem(R.id.action_pages_thumbs).isVisible = hasPages
viewBinding.toolbarBottom.invalidateMenu()
}
override fun onGridTouch(area: Int) {
controlDelegate.onGridTouch(area, viewBinding.container)
override fun onGridTouch(area: TapGridArea): Boolean {
return isReaderResumed() && controlDelegate.onGridTouch(area)
}
override fun onGridLongTouch(area: TapGridArea) {
if (isReaderResumed()) {
controlDelegate.onGridLongTouch(area)
}
}
override fun onProcessTouch(rawX: Int, rawY: Int): Boolean {
@@ -267,7 +250,7 @@ class ReaderActivity :
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return controlDelegate.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
return controlDelegate.onKeyDown(keyCode) || super.onKeyDown(keyCode, event)
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
@@ -297,6 +280,10 @@ class ReaderActivity :
viewModel.switchMode(mode)
}
override fun onDoubleModeChanged(isEnabled: Boolean) {
readerManager.setDoubleReaderMode(isEnabled)
}
private fun onPageSaved(uri: Uri?) {
if (uri != null) {
Snackbar.make(viewBinding.container, R.string.page_saved, Snackbar.LENGTH_LONG)
@@ -337,10 +324,12 @@ class ReaderActivity :
}
TransitionManager.beginDelayedTransition(viewBinding.root, transition)
}
val isFullscreen = settings.isReaderFullscreenEnabled
viewBinding.appbarTop.isVisible = isUiVisible
viewBinding.appbarBottom?.isVisible = isUiVisible
viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value)
systemUiController.setSystemUiVisible(isUiVisible)
viewBinding.infoBar.isTimeVisible = isFullscreen
systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen)
}
}
@@ -357,6 +346,9 @@ class ReaderActivity :
rightMargin = systemBars.right + topMargin
leftMargin = systemBars.left + topMargin
}
viewBinding.infoBar.updatePadding(
top = systemBars.top,
)
return WindowInsetsCompat.Builder(insets)
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
.build()
@@ -368,6 +360,16 @@ class ReaderActivity :
readerManager.currentReader?.switchPageBy(delta)
}
override fun switchChapterBy(delta: Int) {
viewModel.switchChapterBy(delta)
}
override fun openMenu() {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
val currentMode = readerManager.currentMode ?: return
ReaderConfigSheet.show(supportFragmentManager, currentMode)
}
override fun scrollBy(delta: Int, smooth: Boolean): Boolean {
return readerManager.currentReader?.scrollBy(delta, smooth) ?: false
}
@@ -381,14 +383,13 @@ class ReaderActivity :
return reader.isResumed && supportFragmentManager.fragments.lastOrNull() === reader
}
private fun onReaderBarChanged(isBarEnabled: Boolean) {
viewBinding.infoBar.isVisible = isBarEnabled && viewBinding.appbarTop.isGone
override fun onSavePageClick() {
val page = viewModel.getCurrentPage() ?: return
viewModel.saveCurrentPage(page, savePageRequest)
}
private fun onBookmarkStateChanged(isAdded: Boolean) {
val menuItem = viewBinding.toolbarBottom.menu.findItem(R.id.action_bookmark) ?: return
menuItem.setTitle(if (isAdded) R.string.bookmark_remove else R.string.bookmark_add)
menuItem.setIcon(if (isAdded) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark)
private fun onReaderBarChanged(isBarEnabled: Boolean) {
viewBinding.infoBar.isVisible = isBarEnabled && viewBinding.appbarTop.isGone
}
private fun onUiStateChanged(pair: Pair<ReaderUiState?, ReaderUiState?>) {

View File

@@ -0,0 +1,74 @@
package org.koitharu.kotatsu.reader.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.fragment.app.FragmentActivity
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.settings.SettingsActivity
class ReaderBottomMenuProvider(
private val activity: FragmentActivity,
private val readerManager: ReaderManager,
private val viewModel: ReaderViewModel,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_reader_bottom, menu)
onPrepareMenu(menu) // fix, not called in toolbar
}
override fun onPrepareMenu(menu: Menu) {
val hasPages = viewModel.content.value.pages.isNotEmpty()
menu.findItem(R.id.action_pages_thumbs).isVisible = hasPages
val bookmarkItem = menu.findItem(R.id.action_bookmark) ?: return
bookmarkItem.isVisible = hasPages
if (hasPages) {
val hasBookmark = viewModel.isBookmarkAdded.value
bookmarkItem.setTitle(if (hasBookmark) R.string.bookmark_remove else R.string.bookmark_add)
bookmarkItem.setIcon(if (hasBookmark) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark)
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_settings -> {
activity.startActivity(SettingsActivity.newReaderSettingsIntent(activity))
true
}
R.id.action_pages_thumbs -> {
val state = viewModel.getCurrentState() ?: return false
PagesThumbnailsSheet.show(
activity.supportFragmentManager,
viewModel.manga?.toManga() ?: return false,
state.chapterId,
state.page,
)
true
}
R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value) {
viewModel.removeBookmark()
} else {
viewModel.addBookmark()
}
true
}
R.id.action_options -> {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
val currentMode = readerManager.currentMode ?: return false
ReaderConfigSheet.show(activity.supportFragmentManager, currentMode)
true
}
else -> false
}
}
}

View File

@@ -1,83 +1,49 @@
package org.koitharu.kotatsu.reader.ui
import android.content.SharedPreferences
import android.content.res.Resources
import android.view.KeyEvent
import android.view.SoundEffectConstants
import android.view.View
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.GridTouchHelper
import org.koitharu.kotatsu.reader.data.TapGridSettings
import org.koitharu.kotatsu.reader.domain.TapGridArea
import org.koitharu.kotatsu.reader.ui.tapgrid.TapAction
class ReaderControlDelegate(
resources: Resources,
private val settings: AppSettings,
private val tapGridSettings: TapGridSettings,
private val listener: OnInteractionListener,
owner: LifecycleOwner,
) : DefaultLifecycleObserver, SharedPreferences.OnSharedPreferenceChangeListener {
) {
private var isTapSwitchEnabled: Boolean = true
private var isVolumeKeysSwitchEnabled: Boolean = false
private var isReaderTapsAdaptive: Boolean = true
private var minScrollDelta = resources.getDimensionPixelSize(R.dimen.reader_scroll_delta_min)
init {
owner.lifecycle.addObserver(this)
settings.subscribe(this)
updateSettings()
fun onGridTouch(area: TapGridArea): Boolean {
val action = tapGridSettings.getTapAction(
area = area,
isLongTap = false,
) ?: return false
processAction(action)
return true
}
override fun onDestroy(owner: LifecycleOwner) {
settings.unsubscribe(this)
owner.lifecycle.removeObserver(this)
super.onDestroy(owner)
fun onGridLongTouch(area: TapGridArea) {
val action = tapGridSettings.getTapAction(
area = area,
isLongTap = true,
) ?: return
processAction(action)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
updateSettings()
}
fun onGridTouch(area: Int, view: View) {
when (area) {
GridTouchHelper.AREA_CENTER -> {
listener.toggleUiVisibility()
view.playSoundEffect(SoundEffectConstants.CLICK)
}
GridTouchHelper.AREA_TOP -> if (isTapSwitchEnabled) {
listener.switchPageBy(-1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_UP)
}
GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) {
listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT)
}
GridTouchHelper.AREA_BOTTOM -> if (isTapSwitchEnabled) {
listener.switchPageBy(1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN)
}
GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) {
listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
view.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT)
}
}
}
fun onKeyDown(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean = when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) {
fun onKeyDown(keyCode: Int): Boolean = when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> if (settings.isReaderVolumeButtonsEnabled) {
listener.switchPageBy(-1)
true
} else {
false
}
KeyEvent.KEYCODE_VOLUME_DOWN -> if (isVolumeKeysSwitchEnabled) {
KeyEvent.KEYCODE_VOLUME_DOWN -> if (settings.isReaderVolumeButtonsEnabled) {
listener.switchPageBy(1)
true
} else {
@@ -132,21 +98,23 @@ class ReaderControlDelegate(
}
fun onKeyUp(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean {
return (
isVolumeKeysSwitchEnabled &&
(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP)
)
return (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP)
&& settings.isReaderVolumeButtonsEnabled
}
private fun updateSettings() {
val switch = settings.readerPageSwitch
isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in switch
isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in switch
isReaderTapsAdaptive = settings.isReaderTapsAdaptive
private fun processAction(action: TapAction) {
when (action) {
TapAction.PAGE_NEXT -> listener.switchPageBy(1)
TapAction.PAGE_PREV -> listener.switchPageBy(-1)
TapAction.CHAPTER_NEXT -> listener.switchChapterBy(1)
TapAction.CHAPTER_PREV -> listener.switchChapterBy(-1)
TapAction.TOGGLE_UI -> listener.toggleUiVisibility()
TapAction.SHOW_MENU -> listener.openMenu()
}
}
private fun isReaderTapsReversed(): Boolean {
return isReaderTapsAdaptive && listener.readerMode == ReaderMode.REVERSED
return settings.isReaderControlAlwaysLTR && listener.readerMode == ReaderMode.REVERSED
}
interface OnInteractionListener {
@@ -155,10 +123,14 @@ class ReaderControlDelegate(
fun switchPageBy(delta: Int)
fun switchChapterBy(delta: Int)
fun scrollBy(delta: Int, smooth: Boolean): Boolean
fun toggleUiVisibility()
fun openMenu()
fun isReaderResumed(): Boolean
}
}

View File

@@ -9,19 +9,22 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.BatteryManager
import android.util.AttributeSet
import android.view.View
import android.view.WindowInsets
import androidx.annotation.AttrRes
import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.withScale
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.measureDimension
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import java.time.LocalTime
@@ -29,6 +32,7 @@ import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import com.google.android.material.R as materialR
class ReaderInfoBarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@@ -38,7 +42,7 @@ class ReaderInfoBarView @JvmOverloads constructor(
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textBounds = Rect()
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
private val timeReceiver = TimeReceiver()
private val systemStateReceiver = SystemStateReceiver()
private var insetLeft: Int = 0
private var insetRight: Int = 0
private var insetTop: Int = 0
@@ -52,9 +56,12 @@ class ReaderInfoBarView @JvmOverloads constructor(
context.getThemeColor(materialR.attr.colorSurface, Color.WHITE),
200,
)
private val batteryIcon = ContextCompat.getDrawable(context, R.drawable.ic_battery_outline)
private var timeText = timeFormat.format(LocalTime.now())
private var batteryText = ""
private var text: String = ""
private var prevTextHeight: Int = 0
private val innerHeight
get() = height - paddingTop - paddingBottom - insetTop
@@ -62,8 +69,17 @@ class ReaderInfoBarView @JvmOverloads constructor(
private val innerWidth
get() = width - paddingLeft - paddingRight - insetLeft - insetRight
var isTimeVisible: Boolean = true
set(value) {
field = value
invalidate()
}
init {
paint.strokeWidth = context.resources.resolveDp(2f)
context.withStyledAttributes(attrs, R.styleable.ReaderInfoBarView, defStyleAttr) {
paint.strokeWidth = getDimension(R.styleable.ReaderInfoBarView_android_strokeWidth, 2f)
paint.textSize = getDimension(R.styleable.ReaderInfoBarView_android_textSize, 16f)
}
val insetCorner = getSystemUiDimensionOffset("rounded_corner_content_padding")
val fallbackInset = resources.getDimensionPixelOffset(R.dimen.reader_bar_inset_fallback)
val insetStart = getSystemUiDimensionOffset("status_bar_padding_start", fallbackInset) + insetCorner
@@ -76,7 +92,10 @@ class ReaderInfoBarView @JvmOverloads constructor(
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight + insetLeft + insetRight
val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom + insetTop
val desiredHeight = maxOf(
computeTextHeight().also { prevTextHeight = it },
suggestedMinimumHeight,
) + paddingTop + paddingBottom + insetTop
setMeasuredDimension(
measureDimension(desiredWidth, widthMeasureSpec),
measureDimension(desiredHeight, heightMeasureSpec),
@@ -85,25 +104,43 @@ class ReaderInfoBarView @JvmOverloads constructor(
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val ty = innerHeight / 2f + textBounds.height() / 2f - textBounds.bottom
computeTextHeight()
val h = innerHeight.toFloat()
val ty = h / 2f + textBounds.height() / 2f - textBounds.bottom
paint.textAlign = Paint.Align.LEFT
canvas.drawTextOutline(
text,
(paddingLeft + insetLeft + cutoutInsetLeft).toFloat(),
paddingTop + insetTop + ty,
)
paint.textAlign = Paint.Align.RIGHT
canvas.drawTextOutline(
timeText,
(width - paddingRight - insetRight - cutoutInsetRight).toFloat(),
paddingTop + insetTop + ty,
)
if (isTimeVisible) {
paint.textAlign = Paint.Align.RIGHT
var endX = (width - paddingRight - insetRight - cutoutInsetRight).toFloat()
canvas.drawTextOutline(timeText, endX, paddingTop + insetTop + ty)
if (batteryText.isNotEmpty()) {
paint.getTextBounds(timeText, 0, timeText.length, textBounds)
endX -= textBounds.width()
endX -= h * 0.6f
canvas.drawTextOutline(batteryText, endX, paddingTop + insetTop + ty)
batteryIcon?.let {
paint.getTextBounds(batteryText, 0, batteryText.length, textBounds)
endX -= textBounds.width()
val iconCenter = paddingTop + insetTop + textBounds.height() / 2
it.setBounds(
(endX - h).toInt(),
(iconCenter - h / 2).toInt(),
endX.toInt(),
(iconCenter + h / 2).toInt(),
)
it.drawWithOutline(canvas)
}
}
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
updateCutoutInsets(ViewCompat.getRootWindowInsets(this))
updateTextSize()
}
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
@@ -115,8 +152,11 @@ class ReaderInfoBarView @JvmOverloads constructor(
super.onAttachedToWindow()
ContextCompat.registerReceiver(
context,
timeReceiver,
IntentFilter(Intent.ACTION_TIME_TICK),
systemStateReceiver,
IntentFilter().apply {
addAction(Intent.ACTION_TIME_TICK)
addAction(Intent.ACTION_BATTERY_CHANGED)
},
ContextCompat.RECEIVER_EXPORTED,
)
updateCutoutInsets(ViewCompat.getRootWindowInsets(this))
@@ -124,7 +164,7 @@ class ReaderInfoBarView @JvmOverloads constructor(
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
context.unregisterReceiver(timeReceiver)
context.unregisterReceiver(systemStateReceiver)
}
fun update(state: ReaderUiState?) {
@@ -143,17 +183,18 @@ class ReaderInfoBarView @JvmOverloads constructor(
} else {
""
}
updateTextSize()
val newHeight = computeTextHeight()
if (newHeight != prevTextHeight) {
prevTextHeight = newHeight
requestLayout()
}
invalidate()
}
private fun updateTextSize() {
val str = text + timeText
val testTextSize = 48f
paint.textSize = testTextSize
paint.getTextBounds(str, 0, str.length, textBounds)
paint.textSize = testTextSize * innerHeight / textBounds.height()
private fun computeTextHeight(): Int {
val str = text + batteryText + timeText
paint.getTextBounds(str, 0, str.length, textBounds)
return textBounds.height()
}
private fun Canvas.drawTextOutline(text: String, x: Float, y: Float) {
@@ -165,6 +206,20 @@ class ReaderInfoBarView @JvmOverloads constructor(
drawText(text, x, y, paint)
}
private fun Drawable.drawWithOutline(canvas: Canvas) {
var requiredScale = (bounds.width() + paint.strokeWidth * 2f) / bounds.width().toFloat()
setTint(colorOutline)
canvas.withScale(requiredScale, requiredScale, bounds.exactCenterX(), bounds.exactCenterY()) {
draw(canvas)
}
requiredScale = 1f / requiredScale
canvas.withScale(requiredScale, requiredScale, bounds.exactCenterX(), bounds.exactCenterY()) {
draw(canvas)
}
setTint(colorText)
draw(canvas)
}
private fun updateCutoutInsets(insetsCompat: WindowInsetsCompat?) {
val cutouts = (insetsCompat ?: return).displayCutout?.boundingRects.orEmpty()
cutoutInsetLeft = 0
@@ -179,11 +234,19 @@ class ReaderInfoBarView @JvmOverloads constructor(
}
}
private inner class TimeReceiver : BroadcastReceiver() {
private inner class SystemStateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
if (level != -1 && scale != -1) {
batteryText = context.getString(R.string.percent_string_pattern, (level * 100 / scale).toString())
}
override fun onReceive(context: Context?, intent: Intent?) {
timeText = timeFormat.format(LocalTime.now())
invalidate()
if (isTimeVisible) {
invalidate()
}
}
}

View File

@@ -1,49 +1,74 @@
package org.koitharu.kotatsu.reader.ui
import androidx.annotation.IdRes
import android.content.res.Configuration
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.ext.findKeyByValue
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.doublepage.DoubleReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.doublereversed.ReversedDoubleReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.vertical.VerticalReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
import java.util.EnumMap
class ReaderManager(
private val fragmentManager: FragmentManager,
@IdRes private val containerResId: Int,
private val container: FragmentContainerView,
private val settings: AppSettings,
) {
private val modeMap = EnumMap<ReaderMode, Class<out BaseReaderFragment<*>>>(ReaderMode::class.java)
init {
modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java
modeMap[ReaderMode.REVERSED] = ReversedReaderFragment::class.java
modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
val useDoublePages = isLandscape() && settings.isReaderDoubleOnLandscape
invalidateTypesMap(useDoublePages)
}
val currentReader: BaseReaderFragment<*>?
get() = fragmentManager.findFragmentById(containerResId) as? BaseReaderFragment<*>
get() = fragmentManager.findFragmentById(container.id) as? BaseReaderFragment<*>
val currentMode: ReaderMode?
get() {
val readerClass = currentReader?.javaClass ?: return null
return modeMap.entries.find { it.value == readerClass }?.key
return modeMap.findKeyByValue(readerClass)
}
fun replace(newMode: ReaderMode) {
val readerClass = requireNotNull(modeMap[newMode])
fragmentManager.commit {
setReorderingAllowed(true)
replace(containerResId, readerClass, null, null)
replace(container.id, readerClass, null, null)
}
}
fun replace(reader: BaseReaderFragment<*>) {
fragmentManager.commit {
setReorderingAllowed(true)
replace(containerResId, reader)
fun setDoubleReaderMode(isEnabled: Boolean) {
val prevMode = currentMode
invalidateTypesMap(isEnabled && isLandscape())
val newMode = currentMode ?: return
if (newMode != prevMode) {
replace(newMode)
}
}
private fun invalidateTypesMap(useDoublePages: Boolean) {
modeMap[ReaderMode.STANDARD] = if (useDoublePages) {
DoubleReaderFragment::class.java
} else {
PagerReaderFragment::class.java
}
modeMap[ReaderMode.REVERSED] = if (useDoublePages) {
ReversedDoubleReaderFragment::class.java
} else {
ReversedReaderFragment::class.java
}
modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
modeMap[ReaderMode.VERTICAL] = VerticalReaderFragment::class.java
}
private fun isLandscape() = container.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}

View File

@@ -4,12 +4,13 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
class ReaderTopMenuProvider(
private val activity: ReaderActivity,
private val activity: FragmentActivity,
private val viewModel: ReaderViewModel,
) : MenuProvider {

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