Compare commits

..

643 Commits
v5.0 ... v6.2.5

Author SHA1 Message Date
Koitharu
4bb0d52217 Fix downloading 2023-10-28 16:39:43 +03:00
Koitharu
66de4bd49e Translated using Weblate (Russian)
Currently translated at 100.0% (508 of 508 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (507 of 507 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
Bai
ff12d63696 Translated using Weblate (Turkish)
Currently translated at 100.0% (507 of 507 strings)

Co-authored-by: Bai <batuhanakkurt000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
InfinityDouki56
c168a841f3 Translated using Weblate (Filipino)
Currently translated at 88.9% (451 of 507 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
pro maxime
8bfb676e6a Translated using Weblate (Arabic)
Currently translated at 36.0% (183 of 507 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: pro maxime <promaxime45@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-10-28 16:16:39 +03:00
gallegonovato
d5c0ce280e Translated using Weblate (Spanish)
Currently translated at 100.0% (507 of 507 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
Vinícius Saturnino
b34627c361 Translated using Weblate (Portuguese)
Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Vinícius Saturnino <saturninodepaulavinicius62@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
Paulo Oliveira
cbc3be056a Translated using Weblate (Portuguese)
Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Paulo Oliveira <junior.literasas@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
Koitharu
d9acc4ec18 Fix periodical backups to external directory 2023-10-28 16:14:47 +03:00
Koitharu
577cc848ee Scroll lists to top atomatically 2023-10-28 15:26:22 +03:00
Koitharu
8a64c88a07 (Temporary) remove chapters list from downloads 2023-10-28 14:44:58 +03:00
Koitharu
1cd7745e38 Update parsers 2023-10-28 13:26:02 +03:00
Koitharu
395b3f7200 Fix proguard rules 2023-10-27 17:27:40 +03:00
Koitharu
b8db4c81d8 Handle up navigation from reader 2023-10-27 16:44:40 +03:00
Koitharu
98bd42f3ae Remove deletions from sync process 2023-10-27 15:02:10 +03:00
Koitharu
db8835a7b8 Fix history restoring 2023-10-27 14:18:14 +03:00
Koitharu
afe50a9ed6 Fixes 2023-10-27 13:58:04 +03:00
Koitharu
beba818f57 Periodic backups 2023-10-26 17:24:11 +03:00
Koitharu
beb17ef442 Pause autoscroll while touch down 2023-10-26 16:13:30 +03:00
Koitharu
24f1546019 Fix pagination 2023-10-26 12:45:32 +03:00
ngocanhtve
1b0fed5c56 Translated using Weblate (Vietnamese)
Currently translated at 84.1% (419 of 498 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/vi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-10-26 12:29:29 +03:00
Koitharu
3d32bd9d58 Fix warnings 2023-10-25 15:42:00 +03:00
Koitharu
590120433c Update dependencies 2023-10-25 15:42:00 +03:00
Koitharu
4bd7656681 Fix loading footer in lists 2023-10-25 15:41:59 +03:00
Koitharu
2c7438e64d Add error reporting to import local manga 2023-10-25 15:41:59 +03:00
InfinityDouki56
665bebaa7b Translated using Weblate (Filipino)
Currently translated at 88.9% (443 of 498 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-10-25 12:05:52 +03:00
return_null
6ed5994726 Translated using Weblate (Chinese (Simplified))
Currently translated at 98.5% (491 of 498 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-10-25 12:05:52 +03:00
Dpper
311ed865b7 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-10-25 12:05:52 +03:00
Bai
b59fb678fe Translated using Weblate (Turkish)
Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Bai <batuhanakkurt000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-10-25 12:05:52 +03:00
Koitharu
ed9ebdcc55 Handle kotatsu scheme links 2023-10-23 17:20:44 +03:00
Koitharu
74569615e3 Fix splash background 2023-10-18 10:55:01 +03:00
Koitharu
f3c320a90f Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2023-10-18 10:02:13 +03:00
gallegonovato
a3012ab458 Translated using Weblate (Spanish)
Currently translated at 100.0% (498 of 498 strings)

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

Translated using Weblate (Belarusian)

Currently translated at 100.0% (498 of 498 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (498 of 498 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (498 of 498 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-10-18 09:59:52 +03:00
Koitharu
571cf08c53 Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2023-10-18 09:50:52 +03:00
Koitharu
fca53eee7a Improve downloads list 2023-10-18 09:40:31 +03:00
Zakhar Timoshenko
ed9e2eb4d2 ActionMode and NavBar colors fix 2023-10-17 18:04:34 +03:00
Koitharu
c0e94f8415 Show chapters list in downloads 2023-10-17 12:19:44 +03:00
Koitharu
e172d619a1 Fix description expanding 2023-10-17 11:13:16 +03:00
Koitharu
d6c64fc638 Action to open online version of saved manga 2023-10-17 11:06:16 +03:00
Koitharu
37404cb9a6 UI improvements 2023-10-17 10:32:30 +03:00
Koitharu
9d5271ff26 Fix default branch selection #527 #528 2023-10-17 10:24:36 +03:00
Koitharu
5f59432e48 Handle NPE during network requests 2023-10-17 10:01:58 +03:00
Koitharu
5c082b5cdb Update parsers 2023-10-17 09:59:26 +03:00
Koitharu
32133d3358 Bump version 2023-10-16 17:48:25 +03:00
Koitharu
366e4f0da8 Disable mirror switching by default 2023-10-16 12:53:21 +03:00
Koitharu
3ef033c700 Update parsers 2023-10-16 12:46:39 +03:00
Koitharu
bef8e4652f Update acra credentials 2023-10-16 12:46:39 +03:00
Koitharu
8bfdf07a2f Fixes 2023-10-16 12:46:38 +03:00
Allan Nordhøy
f3e597275b Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/nb_NO/
Translation: Kotatsu/plurals
2023-10-16 12:40:38 +03:00
Koitharu
11feaae216 Translated using Weblate (Russian)
Currently translated at 100.0% (497 of 497 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
InfinityDouki56
fe2c1f9634 Translated using Weblate (Filipino)
Currently translated at 89.1% (443 of 497 strings)

Translated using Weblate (Filipino)

Currently translated at 89.2% (441 of 494 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
plum7x
0c7c6dc48a Translated using Weblate (Chinese (Traditional))
Currently translated at 99.5% (492 of 494 strings)

Co-authored-by: plum7x <plumgift@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Bai
503652f024 Translated using Weblate (Turkish)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (494 of 494 strings)

Co-authored-by: Bai <batuhanakkurt000@gmail.com>
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
2023-10-16 12:40:38 +03:00
ngocanhtve
0c4adc67ea Translated using Weblate (Vietnamese)
Currently translated at 85.1% (417 of 490 strings)

Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
BlackSpectrum
c7f5ce30b5 Translated using Weblate (Hindi)
Currently translated at 26.5% (130 of 490 strings)

Added translation using Weblate (Gujarati)

Co-authored-by: BlackSpectrum <tittan5000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
return_null
59d538824f Translated using Weblate (Chinese (Simplified))
Currently translated at 99.1% (486 of 490 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Nayuki
de79f39d16 Translated using Weblate (Thai)
Currently translated at 71.2% (349 of 490 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Clxff H3r4ld0
9792da3a5c Translated using Weblate (Indonesian)
Currently translated at 98.3% (482 of 490 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Макар Разин
c2407e6e41 Translated using Weblate (Czech)
Currently translated at 90.1% (448 of 497 strings)

Translated using Weblate (Serbian)

Currently translated at 30.3% (151 of 497 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (497 of 497 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (497 of 497 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (497 of 497 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (490 of 490 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (490 of 490 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (490 of 490 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/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
gallegonovato
7321eeaed9 Translated using Weblate (Spanish)
Currently translated at 100.0% (497 of 497 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (493 of 494 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (490 of 490 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Cookies
9876adf676 Translated using Weblate (Vietnamese)
Currently translated at 81.3% (398 of 489 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Koitharu
d29e979fbf Add option to order favorites by reading progress 2023-10-13 16:31:51 +03:00
Koitharu
35baf4b58d Add option to order history and favorites by new chapters 2023-10-13 16:26:30 +03:00
Koitharu
97524d66f2 Fix pages thumbnails loading 2023-10-13 15:58:05 +03:00
Koitharu
5b53f8c27d Improve list options configuring 2023-10-13 14:21:28 +03:00
Koitharu
d4588570e6 Add option to disable new sources tip 2023-10-12 13:34:35 +03:00
Koitharu
cc2f9d4529 Improve chapters mapping 2023-10-12 13:24:51 +03:00
Koitharu
3def71ccc1 Merge branch 'feature/32-bit' into devel 2023-10-12 13:03:58 +03:00
Koitharu
b313c64648 Apply color config on-the-fly 2023-10-12 12:18:27 +03:00
Koitharu
f7e7c84317 Apply color config on-the-fly 2023-10-12 11:31:28 +03:00
Koitharu
ee1c532d53 Update progress 2023-10-12 10:42:28 +03:00
Koitharu
6993cec85e Fix new chapters counter in details screen 2023-10-12 10:42:28 +03:00
Koitharu
0b19f56215 Optimize finding saved manga for remote one 2023-10-12 10:42:28 +03:00
Zakhar Timoshenko
817ce7e8df 32-bit colors mode implementing 2023-10-11 21:18:13 +03:00
Zakhar Timoshenko
2b2498cb38 UI tweaks 2023-10-11 19:23:51 +03:00
Koitharu
e4efd0f696 Refactor manga details loading 2023-10-10 11:56:23 +03:00
Koitharu
fbb267e11c Update parsers 2023-10-09 10:10:55 +03:00
Koitharu
5740af05fa Update dependencies 2023-10-06 09:44:53 +03:00
Koitharu
ae2cc1dffc Add support for Dropped manga state 2023-10-04 15:48:28 +03:00
Koitharu
a5b9712e9f Update typography 2023-10-04 15:43:32 +03:00
Koitharu
c013e6e4f4 Adjust keyboard incognito mode 2023-10-04 15:35:14 +03:00
Koitharu
0249faa3f6 Remove bold from feed 2023-10-04 15:29:10 +03:00
Koitharu
9c52423dc0 Fix statusbar color 2023-10-04 15:26:14 +03:00
Eduardo Malaspina
1f7e5458ae Translated using Weblate (Spanish)
Currently translated at 100.0% (489 of 489 strings)

Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-10-04 15:12:03 +03:00
Макар Разин
b4d487b398 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (489 of 489 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (489 of 489 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (489 of 489 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-10-04 15:12:03 +03:00
Koitharu
0281f1eadb Incognito mode indicator 2023-10-04 15:00:46 +03:00
Koitharu
1bd9b655f9 Update parsers 2023-10-04 14:07:14 +03:00
Koitharu
ed87292921 Adaptive tags suggestion 2023-10-04 12:25:09 +03:00
Koitharu
861be7614e Fix back navigation 2023-10-04 11:44:49 +03:00
Koitharu
717fe8748a Fix suggestion notification text 2023-10-04 11:37:39 +03:00
Koitharu
c7a1312cd6 Fix check updates for saved manga #506 2023-10-02 16:49:20 +03:00
Koitharu
b2927854d4 Make keep screen on in reader optional 2023-10-02 16:39:14 +03:00
Koitharu
cfda150630 Fix crash on request pin shortcut 2023-10-02 15:22:35 +03:00
Koitharu
4fa1382ce9 Fix crash on download update 2023-10-02 15:15:44 +03:00
Koitharu
43075c52d1 Improve automatic mirror switching 2023-10-02 14:49:45 +03:00
Koitharu
87942747fc Update parsers 2023-10-02 13:34:40 +03:00
Koitharu
bb6cd73acd Update parsers 2023-09-30 17:44:13 +03:00
kuragehime
6790e5b0d4 Translated using Weblate (Japanese)
Currently translated at 100.0% (487 of 487 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-09-30 17:16:27 +03:00
Макар Разин
845c356a73 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (487 of 487 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (487 of 487 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/uk/
Translation: Kotatsu/Strings
2023-09-30 17:16:27 +03:00
return_null
34499ea77d Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (484 of 487 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-09-30 17:16:27 +03:00
InfinityDouki56
6210864280 Translated using Weblate (Filipino)
Currently translated at 89.3% (435 of 487 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-09-30 17:16:27 +03:00
Crono
19084419c7 Translated using Weblate (Portuguese)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (487 of 487 strings)

Added translation using Weblate (Portuguese)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (487 of 487 strings)

Translated using Weblate (Portuguese)

Currently translated at 90.1% (439 of 487 strings)

Co-authored-by: Crono <cronoreader@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-09-30 17:16:27 +03:00
J. Lavoie
84ce4c508c Translated using Weblate (French)
Currently translated at 100.0% (487 of 487 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2023-09-20 12:34:26 +03:00
gallegonovato
0db8fafe61 Translated using Weblate (Spanish)
Currently translated at 100.0% (487 of 487 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-09-20 12:34:26 +03:00
Koitharu
fed241215e Update parsers 2023-09-20 12:34:12 +03:00
Koitharu
761f24daf9 Fix crashes 2023-09-20 09:40:20 +03:00
Koitharu
a435435496 Fix webtoon zoom controls visibility 2023-09-18 13:57:07 +03:00
Koitharu
81e8c25563 Reorder reader settings items 2023-09-18 13:51:02 +03:00
Koitharu
e3504c3b1e Translated using Weblate (Russian)
Currently translated at 100.0% (487 of 487 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-09-18 13:48:30 +03:00
Макар Разин
2601c12348 Translated using Weblate (Russian)
Currently translated at 100.0% (483 of 483 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (483 of 483 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-09-18 13:48:30 +03:00
Koitharu
138cf44e37 Fix crash with ActivityNotFoundException 2023-09-18 13:42:28 +03:00
Koitharu
65d83e0921 Fix search action #495 2023-09-18 13:34:08 +03:00
Koitharu
6e1cd05fa8 Zoom control buttons in reader 2023-09-18 13:25:53 +03:00
Koitharu
8398c01929 Improve keyboard control in reader 2023-09-18 12:49:37 +03:00
Koitharu
835c49ae79 Download updates directly 2023-09-15 13:34:13 +03:00
Koitharu
36065ccf6c Pin source shortcuts 2023-09-15 12:12:06 +03:00
Koitharu
4ab40566f7 Fix sync server address configuration 2023-09-15 11:14:06 +03:00
return_null
bf01a4d1ab Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (480 of 483 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-09-14 13:41:58 +03:00
Koitharu
8dce9dcc3f Fix pages thumbnails loading 2023-09-14 13:38:02 +03:00
Koitharu
d872044252 Improve mouse interaction 2023-09-14 13:31:18 +03:00
Koitharu
f4313525c2 Update dependencies 2023-09-14 09:05:50 +03:00
Koitharu
4eb4ec7de0 Fix nsfw sources filtering 2023-09-12 19:33:49 +03:00
Koitharu
ecb4dd87d9 Update parsers 2023-09-12 19:28:45 +03:00
Nayuki
3d0f5f75cd Translated using Weblate (Thai)
Currently translated at 63.3% (306 of 483 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
Bander AL-shreef
c5462e8454 Translated using Weblate (Arabic)
Currently translated at 37.4% (181 of 483 strings)

Co-authored-by: Bander AL-shreef <bander.alshreef@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
return_null
5039e324fb Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (480 of 483 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 93.1% (450 of 483 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 92.5% (447 of 483 strings)

Co-authored-by: return_null <demolang@dismail.de>
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
2023-09-12 18:31:26 +03:00
Макар Разин
b251b3e654 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (483 of 483 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (483 of 483 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (483 of 483 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
gallegonovato
5f10070564 Translated using Weblate (Spanish)
Currently translated at 100.0% (483 of 483 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
Clxff H3r4ld0
3da6f80eb6 Translated using Weblate (Indonesian)
Currently translated at 100.0% (481 of 481 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
InfinityDouki56
4b2cfdb972 Translated using Weblate (Filipino)
Currently translated at 89.7% (428 of 477 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
kuragehime
51387ace7e Translated using Weblate (Japanese)
Currently translated at 100.0% (483 of 483 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (481 of 481 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (477 of 477 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-09-12 18:31:26 +03:00
Koitharu
2bdb83ff28 Fix navigation reordering 2023-09-12 13:38:03 +03:00
Koitharu
a1b85433ec Fix bookmarks crash #492 2023-09-12 13:38:03 +03:00
Isira Seneviratne
ca5207c658 Use ancestors and descendants extensions 2023-09-09 17:52:12 +03:00
Koitharu
81de6124f0 Rethrow CancellationException from TrackWorkers #489 2023-09-09 17:42:47 +03:00
Koitharu
a93bc0ed5b Increase max autoscroll speed 2023-09-08 13:48:34 +03:00
Koitharu
a1b96ebbb5 Update parsers 2023-09-08 13:30:26 +03:00
Koitharu
6b93e49f56 Improve loading both local and remote manga 2023-09-08 13:07:26 +03:00
Koitharu
c88a9dff36 Handle enter press in search view 2023-09-08 09:17:04 +03:00
Koitharu
ca47c475d3 Avoid passing manga chapters via extras 2023-09-07 18:15:48 +03:00
Koitharu
8df7fa2729 Fix crashes 2023-09-07 16:40:07 +03:00
Koitharu
ea34abb1d7 Fix categories reordering 2023-09-07 14:06:52 +03:00
Koitharu
c4ff37350c Option to move manga source to top 2023-09-07 13:27:13 +03:00
Koitharu
95547a8d03 Configurable main navigation 2023-09-06 14:42:00 +03:00
Koitharu
4c2197aa5d Option to retry captcha resolving 2023-09-05 11:26:57 +03:00
Koitharu
a679b6775d Exclude captcha actvity from recent 2023-09-05 10:35:32 +03:00
Koitharu
d3e4e97c6f Fix tracker operations parallelism 2023-09-05 10:30:20 +03:00
Koitharu
d1b0af85c4 Update parsers 2023-09-05 10:30:20 +03:00
Koitharu
ce95e0657b Translated using Weblate (Russian)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-09-05 10:30:08 +03:00
Nayuki
6bb159a6d9 Translated using Weblate (Thai)
Currently translated at 57.5% (274 of 476 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-09-05 10:30:08 +03:00
Макар Разин
a75583f750 Translated using Weblate (Belarusian)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2023-09-05 10:30:08 +03:00
Koitharu
fff9df9609 Fix categories and sources reordering 2023-09-03 17:39:52 +03:00
Koitharu
f9609edea5 Fallback to old systemUiVisibility in reader 2023-09-03 17:06:37 +03:00
Koitharu
f1245742c0 Merge branch 'File_creation_time' of github.com:Isira-Seneviratne/Kotatsu into Isira-Seneviratne-File_creation_time 2023-09-01 13:43:58 +03:00
Koitharu
42d933ba83 Bump version 2023-09-01 13:17:07 +03:00
Koitharu
4df644e21f Fix branch prediction 2023-09-01 12:02:31 +03:00
ViAnh
e4ba738c00 Use WeakHashMap to store views 2023-08-31 19:35:53 +03:00
ViAnh
b7f09243aa Avoid unnecessary child layout in webtoon recycler 2023-08-31 19:35:53 +03:00
ViAnh
50d4c41855 Fix webtoon under scale 2023-08-31 19:35:53 +03:00
Koitharu
67adc8b681 Fix widgets in dark theme 2023-08-31 19:28:25 +03:00
Koitharu
34fb4af9fe Fix color scheme preference 2023-08-31 19:11:29 +03:00
Koitharu
05241f73d9 Improve categories managing 2023-08-31 19:11:29 +03:00
Koitharu
d666e4b967 Fix small webtoon pages 2023-08-31 19:11:29 +03:00
Koitharu
b4bf607d3a Merge pull request #470 from Isira-Seneviratne/Data_classes 2023-08-31 09:17:03 +03:00
Isira Seneviratne
a417d5aaa9 Apply requested changes 2023-08-30 19:27:34 +05:30
Koitharu
4b6b2c3e12 Fix favorites selector 2023-08-30 14:43:57 +03:00
Koitharu
51300e30bd Improve favicon loading 2023-08-30 14:41:44 +03:00
Koitharu
399ac07fb3 Fix storage usage calculation 2023-08-30 14:21:09 +03:00
Eryk Michalak
eeba161235 Translated using Weblate (Polish)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Polish)

Currently translated at 95.5% (455 of 476 strings)

Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-30 12:45:08 +03:00
Nayuki
088a388812 Translated using Weblate (Thai)
Currently translated at 49.5% (236 of 476 strings)

Translated using Weblate (Thai)

Currently translated at 42.8% (204 of 476 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-08-30 12:45:08 +03:00
gallegonovato
943bba3ee8 Translated using Weblate (Spanish)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-08-30 12:45:08 +03:00
Koitharu
18c3229200 Handle TooManyRequestsException during downloading 2023-08-29 20:23:20 +03:00
Koitharu
9b6f511ac6 Do not discard image requests in onViewRecycled 2023-08-29 17:35:00 +03:00
Isira Seneviratne
ad3b5dde91 Convert more classes to data classes 2023-08-27 07:10:32 +05:30
Isira Seneviratne
ded7cdb71e Obtain file creation time 2023-08-27 06:19:07 +05:30
Koitharu
74ca19a931 Improve widgets ui #457 2023-08-26 18:35:49 +03:00
Koitharu
2684a7384e Restore covers using interceptor 2023-08-26 16:44:09 +03:00
Koitharu
2c561824ef Fix default reader mode option #468 #466 2023-08-25 13:26:48 +03:00
Koitharu
61e9796269 Fix amoled theme 2023-08-24 14:43:49 +03:00
Koitharu
54597eb8f0 Udpate parsers 2023-08-24 14:43:49 +03:00
Dpper
e07ea0552f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (476 of 476 strings)

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

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (475 of 475 strings)

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

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

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

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

Translated using Weblate (Russian)

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (473 of 473 strings)

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

Translated using Weblate (Arabic)

Currently translated at 28.5% (2 of 7 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (473 of 473 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (7 of 7 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (473 of 473 strings)

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

Translated using Weblate (Thai)

Currently translated at 100.0% (7 of 7 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (472 of 472 strings)

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

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (471 of 471 strings)

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

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

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

Translated using Weblate (Thai)

Currently translated at 20.8% (98 of 470 strings)

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

Added translation using Weblate (Hungarian)

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

Translated using Weblate (Italian)

Currently translated at 78.9% (371 of 470 strings)

Translated using Weblate (German)

Currently translated at 99.3% (467 of 470 strings)

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

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

Translated using Weblate (Thai)

Currently translated at 71.4% (5 of 7 strings)

Added translation using Weblate (Thai)

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

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

Translated using Weblate (Filipino)

Currently translated at 91.4% (428 of 468 strings)

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

Translated using Weblate (German)

Currently translated at 93.8% (441 of 470 strings)

Translated using Weblate (German)

Currently translated at 100.0% (7 of 7 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (468 of 468 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
JA Sunny
d48c3fbe1b Translated using Weblate (Bengali)
Currently translated at 18.4% (86 of 467 strings)

Translated using Weblate (Bengali)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: JA Sunny <ayasbinshams2003@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/bn/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/bn/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
kuragehime
6720474667 Translated using Weblate (Japanese)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (467 of 467 strings)

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

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (7 of 7 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (467 of 467 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (467 of 467 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (467 of 467 strings)

Translated using Weblate (Korean)

Currently translated at 76.3% (356 of 466 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (466 of 466 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (466 of 466 strings)

Translated using Weblate (Japanese)

Currently translated at 96.3% (449 of 466 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (466 of 466 strings)

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

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

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

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

Translated using Weblate (Korean)

Currently translated at 79.6% (356 of 447 strings)

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

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

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

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

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

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

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

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

Translated using Weblate (Czech)

Currently translated at 100.0% (447 of 447 strings)

Added translation using Weblate (Czech)

Added translation using Weblate (Czech)

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

Co-authored-by: Nguyễn Mạnh Hùng <hungmn13@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Hosted Weblate
48b9c1236d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
plum7x
c69d293caa Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (447 of 447 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.5% (436 of 447 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.3% (435 of 447 strings)

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

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

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

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

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

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

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

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (445 of 447 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (447 of 447 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
Koitharu
d696606ef9 Misc fixes 2023-06-27 10:28:47 +03:00
Koitharu
0a6e106a1d Filter local manga files 2023-06-24 13:18:09 +03:00
Koitharu
de1a7f0ca8 Fix IndexOutOfBoundsException in RemoteViewsFactory 2023-06-24 09:38:13 +03:00
Koitharu
9d31e76cc7 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)

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

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

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

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

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

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (436 of 436 strings)

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

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

Translated using Weblate (Polish)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Korean)

Currently translated at 75.1% (327 of 435 strings)

Translated using Weblate (Greek)

Currently translated at 19.3% (84 of 435 strings)

Translated using Weblate (Serbian)

Currently translated at 28.2% (123 of 435 strings)

Translated using Weblate (Arabic)

Currently translated at 18.1% (79 of 435 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Italian)

Currently translated at 85.2% (371 of 435 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/el/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Koitharu
4a7be70898 Update queries in manifest 2023-06-23 16:14:41 +03:00
Koitharu
2bcba1eb21 Configure manga directories 2023-06-22 13:45:29 +03:00
Koitharu
feca7ba3fc Support for custom directories for manga 2023-06-22 10:11:11 +03:00
Koitharu
745b349e5e Ability to remove item from updates 2023-06-21 15:27:20 +03:00
Koitharu
13946783a5 Fix crashes 2023-06-21 15:06:01 +03:00
Koitharu
84e5400522 Download options dialog 2023-06-21 14:54:11 +03:00
Koitharu
02c9a933d2 Fix offline manga details 2023-06-20 17:06:18 +03:00
Koitharu
92af851d3b Option to clear single source cookies 2023-06-20 13:43:09 +03:00
Koitharu
009eb9fe44 Fix recursive sync 2023-06-17 18:34:08 +03:00
Koitharu
fc8a5ccd9f Fix Continue button in offline mode 2023-06-17 18:20:57 +03:00
Koitharu
91f46de547 Fix crashes 2023-06-17 18:11:09 +03:00
Koitharu
d548993e14 Move syncronization to main process 2023-06-17 17:36:12 +03:00
Koitharu
4f32664b33 Respect system PowerSave mode 2023-06-17 16:12:14 +03:00
Koitharu
71b14a3aa8 Refactor FilterOwner 2023-06-17 16:05:08 +03:00
Isira Seneviratne
183a61272e Use ParcelCompat methods. 2023-06-17 15:50:08 +03:00
Koitharu
f1f208ad15 Merge branch 'master' into devel 2023-06-16 10:45:12 +03:00
Koitharu
c6983d794c Fix tablet portrait layout 2023-06-16 10:42:19 +03:00
Koitharu
8228153c83 Fix tablet portrait layout 2023-06-16 10:41:32 +03:00
Koitharu
844bd13a07 Fix filter lifecycle 2023-06-16 10:23:40 +03:00
Koitharu
60a5620134 Schedule workers only on demand 2023-06-16 09:50:02 +03:00
qrynill
dd09a39077 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 82.0% (357 of 435 strings)

Co-authored-by: qrynill <tryvseu@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translation: Kotatsu/Strings
2023-06-15 09:45:35 +03:00
MaSHiNiK
1511bd3279 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: MaSHiNiK <infinitymashinik456@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-15 09:45:35 +03:00
Макар Разин
259c335607 Translated using Weblate (Turkish)
Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (French)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Portuguese)

Currently translated at 86.2% (375 of 435 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-06-15 09:45:07 +03:00
Nick New
86367b6d3b Added translation using Weblate (Thai)
Co-authored-by: Nick New <newblackseries@gmail.com>
2023-06-15 09:45:07 +03:00
Koitharu
19b893738d Update parsers 2023-06-15 09:44:35 +03:00
Koitharu
d817ae0394 Fix Cloudflare bypass 2023-06-15 09:43:14 +03:00
Koitharu
d81c22b586 Fix crashes 2023-06-14 10:49:33 +03:00
Koitharu
cd23b044df Merge branch 'devel' 2023-06-13 19:08:45 +03:00
Koitharu
4922881343 Update parsers 2023-06-13 18:41:32 +03:00
Koitharu
ff0d04bea6 Add current screen info to crash reports 2023-06-13 18:32:11 +03:00
Koitharu
97de629c3b Fix BottomSheet duplication 2023-06-13 10:45:29 +03:00
Koitharu
7b482e5bcf Fix shortcuts icons size 2023-06-12 16:16:33 +03:00
Koitharu
fd575b8131 Merge branch 'devel' 2023-06-12 12:57:29 +03:00
Koitharu
c77e023bef Fix color filter preview 2023-06-12 12:54:24 +03:00
Koitharu
a3cf52859b Reader refactoring 2023-06-12 12:47:05 +03:00
Koitharu
5e55bce529 Use indirect image url in bookmarks 2023-06-12 10:54:12 +03:00
Koitharu
b1ba70bf77 Fix settings issues 2023-06-10 15:48:30 +03:00
Koitharu
b930272221 Fix settings background 2023-06-09 16:34:29 +03:00
Koitharu
75305c0b94 Fix fast scroll issues 2023-06-09 16:18:58 +03:00
Koitharu
24b16e2ce2 Fix debug/release sources packages 2023-06-08 21:15:32 +03:00
qrynill
0ccbba6787 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 81.8% (356 of 435 strings)

Co-authored-by: qrynill <tryvseu@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translation: Kotatsu/Strings
2023-06-08 18:53:35 +03:00
Reza Almanda
ca314867f2 Translated using Weblate (Indonesian)
Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-08 18:53:35 +03:00
gallegonovato
236e284360 Translated using Weblate (Spanish)
Currently translated at 100.0% (435 of 435 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-08 18:53:35 +03:00
ViAnh
9e1be337ed Translated using Weblate (Vietnamese)
Currently translated at 78.3% (341 of 435 strings)

Added translation using Weblate (Vietnamese)

Co-authored-by: ViAnh <polaris140698@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-06-08 18:53:35 +03:00
kuragehime
104f2ebfae Translated using Weblate (Japanese)
Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (434 of 434 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-06-08 18:53:35 +03:00
Koitharu
6a2e12dc29 Read button tip in DetailsActivity 2023-06-06 12:35:36 +03:00
Koitharu
9587cb439c Android 5 fixes 2023-06-06 10:10:24 +03:00
Koitharu
c42d0824b0 Add summaries to settings root 2023-06-05 13:54:49 +03:00
Koitharu
09f6dd9b4e Remove unused resources 2023-06-05 13:53:52 +03:00
Zakhar Timoshenko
b494c96e31 Translated using Weblate (Russian)
Currently translated at 99.7% (433 of 434 strings)

Co-authored-by: Zakhar Timoshenko <vp1984tanki@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-06-05 13:46:13 +03:00
Koitharu
0f6d56ee2d Merge remote-tracking branch 'weblate/devel' into devel 2023-06-05 13:23:14 +03:00
Koitharu
8d15691e17 Remove unused resources 2023-06-05 13:04:26 +03:00
GpixeL
bd8b251934 Translated using Weblate (Indonesian)
Currently translated at 100.0% (430 of 430 strings)

Co-authored-by: GpixeL <gamesfire313@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
frablock
2f1b74e45a Translated using Weblate (French)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (French)

Currently translated at 100.0% (430 of 430 strings)

Co-authored-by: frablock <franon1.frablock29@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
2023-06-05 12:56:26 +03:00
FateXBlood
73217b8e11 Translated using Weblate (Nepali)
Currently translated at 34.1% (144 of 422 strings)

Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
wr131
759df969c9 Translated using Weblate (Chinese (Traditional))
Currently translated at 23.4% (99 of 422 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (419 of 422 strings)

Co-authored-by: wr131 <wr13173893699@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
Clxff H3r4ld0
466e35fffa Translated using Weblate (Indonesian)
Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (422 of 422 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
Reza Almanda
f44db3dbff Translated using Weblate (Indonesian)
Currently translated at 100.0% (421 of 421 strings)

Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
Макар Разин
315870abcb Translated using Weblate (Ukrainian)
Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (French)

Currently translated at 100.0% (421 of 421 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/fr/
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
2023-06-05 12:56:26 +03:00
Bai
3e46b3957c Translated using Weblate (Turkish)
Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (416 of 417 strings)

Co-authored-by: Bai <batuhanakkurt000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
gallegonovato
6dc81468d2 Translated using Weblate (Spanish)
Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (417 of 417 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-06-05 12:56:26 +03:00
Subham Jena
56bc0dbf07 Translated using Weblate (Odia)
Currently translated at 7.3% (31 of 421 strings)

Translated using Weblate (Odia)

Currently translated at 5.5% (23 of 416 strings)

Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/or/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
tryvseu
7bc33adca8 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 85.5% (356 of 416 strings)

Co-authored-by: tryvseu <tryvseu@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
InfinityDouki56
c8794d59f7 Translated using Weblate (Filipino)
Currently translated at 93.0% (387 of 416 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
Insopitus
9c2a57812e Translated using Weblate (Chinese (Simplified))
Currently translated at 99.2% (413 of 416 strings)

Co-authored-by: Insopitus <jessefor@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
kuragehime
6bd5033858 Translated using Weblate (Japanese)
Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Japanese)

Currently translated at 91.8% (382 of 416 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
J. Lavoie
e7c2a76219 Translated using Weblate (French)
Currently translated at 100.0% (416 of 416 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
Макар Разин
0934363298 Translated using Weblate (Belarusian)
Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (416 of 416 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2023-06-05 12:56:26 +03:00
GpixeL
de29527805 Translated using Weblate (Indonesian)
Currently translated at 100.0% (430 of 430 strings)

Co-authored-by: GpixeL <gamesfire313@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-05 11:53:43 +02:00
frablock
f11e964f0b Translated using Weblate (French)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (French)

Currently translated at 100.0% (430 of 430 strings)

Co-authored-by: frablock <franon1.frablock29@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
2023-06-05 11:53:42 +02:00
FateXBlood
61a98f54b9 Translated using Weblate (Nepali)
Currently translated at 34.1% (144 of 422 strings)

Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2023-06-05 11:53:42 +02:00
wr131
50e67daea4 Translated using Weblate (Chinese (Traditional))
Currently translated at 23.4% (99 of 422 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (419 of 422 strings)

Co-authored-by: wr131 <wr13173893699@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2023-06-05 11:53:41 +02:00
Clxff H3r4ld0
0030706226 Translated using Weblate (Indonesian)
Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (422 of 422 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-05 11:53:41 +02:00
Reza Almanda
056ef5433d Translated using Weblate (Indonesian)
Currently translated at 100.0% (421 of 421 strings)

Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-05 11:53:40 +02:00
Макар Разин
c14b2ceeff Translated using Weblate (Ukrainian)
Currently translated at 100.0% (431 of 431 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (431 of 431 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (431 of 431 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (French)

Currently translated at 100.0% (421 of 421 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/fr/
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
2023-06-05 11:53:40 +02:00
Bai
ff2cf9d18a Translated using Weblate (Turkish)
Currently translated at 100.0% (431 of 431 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (416 of 417 strings)

Co-authored-by: Bai <batuhanakkurt000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-06-05 11:53:39 +02:00
gallegonovato
96b6900c70 Translated using Weblate (Spanish)
Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (417 of 417 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-06-05 11:53:39 +02:00
Subham Jena
c6228d3fe1 Translated using Weblate (Odia)
Currently translated at 7.3% (31 of 421 strings)

Translated using Weblate (Odia)

Currently translated at 5.5% (23 of 416 strings)

Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/or/
Translation: Kotatsu/Strings
2023-06-05 11:53:38 +02:00
tryvseu
8ac95e1608 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 85.5% (356 of 416 strings)

Co-authored-by: tryvseu <tryvseu@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translation: Kotatsu/Strings
2023-06-05 11:53:38 +02:00
InfinityDouki56
69a9ec354b Translated using Weblate (Filipino)
Currently translated at 93.0% (387 of 416 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-06-05 11:53:37 +02:00
Insopitus
0639d3e6c1 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.2% (413 of 416 strings)

Co-authored-by: Insopitus <jessefor@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-06-05 11:53:37 +02:00
kuragehime
ae5cebd42d Translated using Weblate (Japanese)
Currently translated at 100.0% (431 of 431 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Japanese)

Currently translated at 91.8% (382 of 416 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-06-05 11:53:36 +02:00
J. Lavoie
cd8381cbfb Translated using Weblate (French)
Currently translated at 100.0% (416 of 416 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2023-06-05 11:53:36 +02:00
Макар Разин
3132049a63 Translated using Weblate (Belarusian)
Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (416 of 416 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2023-06-05 11:53:35 +02:00
Koitharu
bc3a7fc211 Reorganize settings 2023-06-05 12:53:12 +03:00
Koitharu
e794f84c6f Get rid of SlidingPaneLayout in settings 2023-06-05 11:52:56 +03:00
Koitharu
76709dda21 Remove replacement characters from manga description 2023-06-05 10:57:35 +03:00
Koitharu
6dc460bc20 Replace CloudFlareDialog with activity 2023-06-05 10:45:23 +03:00
Koitharu
c2ee548f0a Show errors in global search results 2023-06-05 09:34:56 +03:00
Koitharu
1847759ec3 Refactor list extra provider 2023-06-04 17:37:54 +03:00
Koitharu
02d5dfb375 Check if page file valid before page display 2023-06-04 16:55:51 +03:00
Koitharu
12d8d3e2d1 Set AppProxySelector as default 2023-06-04 16:04:36 +03:00
Koitharu
b5705b45df Add port number validation 2023-06-04 16:03:51 +03:00
Koitharu
46b797fc67 Fix chapters bottom sheet header size 2023-06-04 16:03:51 +03:00
Zakhar Timoshenko
5ec7fbed94 Set style for drag handle globally 2023-06-04 16:02:38 +03:00
Koitharu
b48c6d7d38 Fix pages bottom sheet scroll 2023-06-04 15:42:26 +03:00
Koitharu
da4aedca97 Fix proxy authentication 2023-06-04 15:42:26 +03:00
Zakhar Timoshenko
32695f9816 Bottom sheet adjustments 2023-06-04 13:55:06 +03:00
Koitharu
bece4cc15d Manage title visibility on DetailsActivity 2023-06-02 20:25:36 +03:00
Koitharu
548c41fbf9 Proxy authenticaton support #376 2023-06-02 19:48:39 +03:00
Koitharu
ef9b16da0b Fix favourite categories covers loading 2023-06-02 17:07:26 +03:00
Koitharu
5d1ef983e9 Check available ram before pages prefetch 2023-06-02 17:06:37 +03:00
Koitharu
eb78a776cf Update parsers 2023-06-02 17:04:46 +03:00
Koitharu
661e502003 Fix favourite categories covers loading 2023-06-02 16:56:57 +03:00
Koitharu
8c5c7d6b04 Color inversion in pages color filter #372 2023-06-02 16:16:34 +03:00
Koitharu
b1187c611a Check available ram before pages prefetch 2023-06-02 15:29:55 +03:00
Koitharu
893ba37c86 Images proxy #357 2023-06-02 13:28:49 +03:00
Koitharu
b1bc94b1e9 Improve tablet ui 2023-06-02 09:03:05 +03:00
Koitharu
2e3be00e26 Update chapters list ui, add bookmark indicator 2023-06-01 17:56:19 +03:00
Koitharu
84f41810c5 Update filter header ui 2023-06-01 11:55:08 +03:00
Koitharu
f0a4fa4e95 Update bottom sheets 2023-05-31 15:17:24 +03:00
Koitharu
0c132a521e Use SideSheet instead of BottomSheet on landscape 2023-05-30 20:27:38 +03:00
Koitharu
3d05541f61 Update reader activity ui 2023-05-30 08:49:21 +03:00
Koitharu
2442e7cbe1 Fix warnings 2023-05-29 13:06:50 +03:00
Koitharu
4522c478cb Merge branch 'master' into devel 2023-05-29 11:57:22 +03:00
Koitharu
6881c22453 Update parsers 2023-05-27 17:54:31 +03:00
Koitharu
5a0c54e00f Replace LiveData with StateFlow 2023-05-27 12:25:49 +03:00
Koitharu
47f346b42c Respect system DataSaver 2023-05-24 14:27:04 +03:00
Koitharu
dc358ae6a2 Refactor manga loading 2023-05-24 13:50:18 +03:00
Koitharu
bfa9feaef0 Merge branch 'master' into devel 2023-05-23 13:34:29 +03:00
Koitharu
b5cd92fb5f Update parsers 2023-05-23 13:05:37 +03:00
Koitharu
08e5c148fd Limit cache max-age and action to clear cache manually 2023-05-23 09:07:56 +03:00
Koitharu
8323d399ff Fix focus changes on sync authorization screen 2023-05-23 09:07:16 +03:00
Koitharu
5108f45111 Limit lifetime of memory content cache 2023-05-23 09:06:22 +03:00
Koitharu
bf0d34e9cf Validate header value in settings 2023-05-23 09:04:57 +03:00
Koitharu
c3216871ed Move sources from java to kotlin dir 2023-05-22 18:20:37 +03:00
Koitharu
a8f5714b35 Refactor chapters mapping 2023-05-22 18:08:05 +03:00
Koitharu
84567767a0 Limit lifetime of memory content cache 2023-05-20 15:47:47 +03:00
Koitharu
eb7efaaac9 Add proxy support #376 2023-05-20 15:32:09 +03:00
Koitharu
3729b5f2f0 Share OkHttpClients 2023-05-20 08:34:22 +03:00
Koitharu
e4c2797f06 Fix focus changes on sync authorization screen 2023-05-19 15:31:14 +03:00
Koitharu
e02899c3f2 Limit cache max-age and action to clear cache manually 2023-05-19 14:52:07 +03:00
Koitharu
96c89a716e Refactoring 2023-05-19 14:19:02 +03:00
Koitharu
65ed5c7e6b Merge branch 'feature/thumbnails' into devel 2023-05-19 11:41:28 +03:00
gallegonovato
3778a9e1d4 Translated using Weblate (Spanish)
Currently translated at 100.0% (416 of 416 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-05-17 18:51:56 +03:00
Макар Разин
71ecd9d8e2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (416 of 416 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (416 of 416 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (416 of 416 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-05-17 18:51:56 +03:00
Subham Jena
7cba8d2dc7 Translated using Weblate (Odia)
Currently translated at 3.8% (16 of 416 strings)

Translated using Weblate (Odia)

Currently translated at 2.4% (10 of 415 strings)

Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/or/
Translation: Kotatsu/Strings
2023-05-17 18:51:56 +03:00
GpixeL
79c2927da2 Translated using Weblate (Indonesian)
Currently translated at 96.8% (402 of 415 strings)

Co-authored-by: GpixeL <gamesfire313@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-05-17 18:51:56 +03:00
ctntt
a4a28c7342 Translated using Weblate (German)
Currently translated at 99.2% (413 of 416 strings)

Translated using Weblate (German)

Currently translated at 99.5% (413 of 415 strings)

Co-authored-by: ctntt <pavlov_mainstreamed@slmail.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2023-05-17 18:51:56 +03:00
Koitharu
3f96f34b8e Improve pages thumbnails sheet 2023-05-17 18:50:06 +03:00
Koitharu
43a92bdf08 Improve sync logging 2023-05-17 17:00:42 +03:00
Koitharu
51ff1ff7b7 Fix concurrent chapters loading in reader 2023-05-17 16:17:18 +03:00
Koitharu
2e0eb5de54 Fix handling special characters in local manga filenames 2023-05-16 13:07:11 +03:00
Koitharu
4f68e7d0e6 Handle WebView unavailability 2023-05-16 07:56:05 +03:00
ctntt
c6d303980b Translated using Weblate (German)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (German)

Currently translated at 99.5% (412 of 414 strings)

Co-authored-by: ctntt <pavlov_mainstreamed@slmail.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-05-15 18:51:27 +03:00
Koitharu
18e6ee1453 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (414 of 414 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (414 of 414 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-05-15 18:51:27 +03:00
J. Lavoie
09144d7f55 Translated using Weblate (French)
Currently translated at 100.0% (414 of 414 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2023-05-15 18:51:27 +03:00
gallegonovato
05583504ee Translated using Weblate (Spanish)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (414 of 414 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-05-15 18:51:27 +03:00
GpixeL
8dc02967fc Translated using Weblate (Indonesian)
Currently translated at 97.1% (402 of 414 strings)

Co-authored-by: GpixeL <gamesfire313@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-05-15 18:51:27 +03:00
Макар Разин
4c206746c9 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (414 of 414 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (414 of 414 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/uk/
Translation: Kotatsu/Strings
2023-05-15 18:51:27 +03:00
Subham Jena
e9b0e9f740 Translated using Weblate (Odia)
Currently translated at 2.1% (9 of 414 strings)

Translated using Weblate (Odia)

Currently translated at 85.7% (6 of 7 strings)

Added translation using Weblate (Odia)

Translated using Weblate (Odia)

Currently translated at 14.2% (1 of 7 strings)

Added translation using Weblate (Odia)

Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/or/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/or/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-05-15 18:51:27 +03:00
Koitharu
6d0cd49db3 Improve branch selection 2023-05-15 18:37:25 +03:00
Koitharu
e69964d1f5 Fix obtaining manga output 2023-05-12 15:56:50 +03:00
Koitharu
f368277d73 Fix lint warnings 2023-05-12 13:42:31 +03:00
Hosted Weblate
f3a8eb3216 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
Translation: Kotatsu/Strings
2023-05-11 19:07:36 +03:00
Koitharu
f7a585ef55 Translated using Weblate (Arabic)
Currently translated at 19.0% (79 of 414 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (414 of 414 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-05-11 19:07:36 +03:00
Koitharu
96d6f8d8e6 Option to read in incognito mode instantly 2023-05-11 18:48:51 +03:00
Koitharu
67bbd3e6d3 Resources cleanup 2023-05-11 17:22:08 +03:00
Koitharu
e7afe1c802 Translated using Weblate (Russian)
Currently translated at 100.0% (461 of 461 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-05-11 17:13:41 +03:00
Abay Emes
f63beba8af Translated using Weblate (Kazakh)
Currently translated at 34.0% (153 of 450 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
2023-05-11 17:13:41 +03:00
J. Lavoie
4ddeb1acce Translated using Weblate (French)
Currently translated at 100.0% (450 of 450 strings)

Translated using Weblate (German)

Currently translated at 99.5% (448 of 450 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2023-05-11 17:13:41 +03:00
Boris
10b178aed3 Translated using Weblate (Russian)
Currently translated at 100.0% (450 of 450 strings)

Co-authored-by: Boris <no4nick@no4nick.ru>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-05-11 17:13:41 +03:00
Koitharu
514594edb0 Added translation using Weblate (Kazakh)
Co-authored-by: Koitharu <nvasya95@gmail.com>
2023-05-11 17:13:41 +03:00
gallegonovato
b2a9f7d594 Translated using Weblate (Spanish)
Currently translated at 100.0% (450 of 450 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (448 of 448 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (448 of 448 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (448 of 448 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-05-11 17:13:41 +03:00
ctntt
30ad67ef52 Translated using Weblate (German)
Currently translated at 98.8% (436 of 441 strings)

Co-authored-by: ctntt <pavlov_mainstreamed@slmail.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2023-05-11 17:13:41 +03:00
Koitharu
810434fef5 Fix Referer header for non-ascii domains 2023-05-11 17:06:03 +03:00
Koitharu
26a7a7a2e8 Show suggestions on the shelf 2023-05-11 16:47:36 +03:00
Koitharu
4d8da40885 Merge branch 'feature/suggestions_v2' into devel 2023-05-11 11:47:21 +03:00
Koitharu
248bf8ed03 UI improvements 2023-05-11 11:46:45 +03:00
Koitharu
090f7a5858 Update random manga selecting 2023-05-10 20:01:15 +03:00
Koitharu
5f38b01fd1 Suggestions enable tip 2023-05-10 19:39:38 +03:00
Koitharu
2169ee7a5b Tune suggestions 2023-05-10 19:16:17 +03:00
Koitharu
d633204efa Merge branch 'devel' into feature/suggestions_v2 2023-05-10 13:49:34 +03:00
Koitharu
2b12dbd8d7 Disallow multiple sync accounts 2023-05-10 13:38:15 +03:00
Koitharu
8bfdc73072 Fix sync via https 2023-05-10 13:09:26 +03:00
Koitharu
be666b7854 Two buttons alert dialog 2023-05-10 12:14:03 +03:00
Koitharu
52655cad2c New suggestions algorithm 2023-05-09 18:35:33 +03:00
Zakhar Timoshenko
8e856211aa Use system color for notifications 2023-05-09 17:29:02 +03:00
Zakhar Timoshenko
e1f82d147c Fix ActionMode background on Android Lollipop 2023-05-09 17:28:15 +03:00
Koitharu
023605e246 Refactor skipping checkable state animation 2023-05-08 19:46:17 +03:00
Koitharu
c0544e25af Fix status bar colors 2023-05-08 19:35:06 +03:00
Koitharu
235b02870b Improve download notifications 2023-05-08 19:25:34 +03:00
Koitharu
25e7ab2d8e Improve workers notifications 2023-05-08 17:12:43 +03:00
Koitharu
2d171657dc Merge branch 'feature/downloads_worker' into devel 2023-05-08 17:00:32 +03:00
Koitharu
ac9680b5c0 Refactor downloading-related classes 2023-05-08 16:55:10 +03:00
Koitharu
42df607f52 Resume download on network becomes available 2023-05-08 16:06:43 +03:00
Koitharu
fc1755612b Option to disable automatic mirror switching 2023-05-07 19:19:57 +03:00
Koitharu
1ead369ee2 Made synchronization server address configurable 2023-05-07 19:02:52 +03:00
Koitharu
07aa04aa4d Fix incognito mode switcher state 2023-05-07 10:42:32 +03:00
Koitharu
7b8bbf9fe1 Fix incognito mode switcher state 2023-05-07 10:42:19 +03:00
Koitharu
8883e73971 Migrate to Okio somewhere 2023-05-07 10:38:50 +03:00
Koitharu
0cb1238143 Update download settings 2023-05-07 09:53:22 +03:00
Koitharu
f4628f7ab5 Merge branch 'devel' into feature/downloads_worker 2023-05-06 18:30:45 +03:00
Koitharu
3d0b69024d Fix badges offsets 2023-05-06 18:29:51 +03:00
Koitharu
12e9fb5aab Fix download error with empty largeCoverUrl 2023-05-06 17:41:15 +03:00
Koitharu
5fc08d9ecb Update parsers 2023-05-06 17:34:46 +03:00
InfinityDouki56
e7eb61e3e5 Translated using Weblate (Filipino)
Currently translated at 91.4% (398 of 435 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-05-06 17:34:29 +03:00
vividly
dae3982e67 Translated using Weblate (Japanese)
Currently translated at 97.0% (422 of 435 strings)

Co-authored-by: vividly <vividly@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-05-06 17:34:29 +03:00
Макар Разин
45b2d2bebe Translated using Weblate (Belarusian)
Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2023-05-06 17:34:29 +03:00
Zakhar Timoshenko
77fa34835f Fix remaining incorrect sample data 2023-05-06 17:23:28 +03:00
Zakhar Timoshenko
cee68069d6 Remove sample data 2023-05-06 17:19:41 +03:00
CakesTwix
48afc8624b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: CakesTwix <cakestwix1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-05-06 16:07:11 +03:00
Koitharu
b9b41ed491 Fix local manga list update on deletion 2023-05-06 16:05:39 +03:00
Koitharu
632b42ea86 Improve downloads list 2023-05-06 15:48:24 +03:00
Zakhar Timoshenko
427272aac1 Update Material components to 1.9.0 2023-05-05 19:19:41 +03:00
Koitharu
41ac50c76a Manage download states 2023-05-05 17:03:52 +03:00
Koitharu
f05bb20428 Use WorkManager for downloads 2023-05-01 16:09:16 +03:00
Koitharu
78f417ebe1 Fix downloading chapters with the same names 2023-05-01 12:52:35 +03:00
Koitharu
3fd6bec433 Update parsers 2023-04-28 17:24:21 +03:00
Koitharu
262e26a0cc Fix crashes 2023-04-28 17:23:29 +03:00
Koitharu
1b64c2a330 Update parsers 2023-04-28 17:23:29 +03:00
Koitharu
5ea0ecbd12 Permormance improvements 2023-04-28 17:23:29 +03:00
Dawid Jarubas
f9a1d1617e Translated using Weblate (Polish)
Currently translated at 91.4% (398 of 435 strings)

Co-authored-by: Dawid Jarubas <jarubas.dawid@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2023-04-28 17:22:58 +03:00
gallegonovato
10d8365fc1 Translated using Weblate (Spanish)
Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-04-28 17:22:58 +03:00
1395 changed files with 39812 additions and 25403 deletions

View File

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

View File

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

1
.idea/.gitignore generated vendored
View File

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

2
.idea/gradle.xml generated
View File

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

3
.weblate Normal file
View File

@@ -0,0 +1,3 @@
[weblate]
url = https://hosted.weblate.org/api/
translation = kotatsu/strings

11
CONTRIBUTING.md Normal file
View File

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

View File

@@ -39,6 +39,10 @@ Kotatsu is a free and open source manga reader for Android.
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
### Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the guidelines.
### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

View File

@@ -2,28 +2,30 @@ plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'com.google.devtools.ksp'
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
}
android {
compileSdk = 33
buildToolsVersion = '33.0.2'
compileSdk = 34
buildToolsVersion = '34.0.0'
namespace = 'org.koitharu.kotatsu'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 538
versionName '5.0'
minSdk = 21
targetSdk = 34
versionCode = 593
versionName = '6.2.5'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
arguments {
arg 'room.schemaLocation', "$projectDir/schemas".toString()
}
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {
// arg("room.generateKotlin", "true") TODO: enable later
arg("room.schemaLocation", "$projectDir/schemas")
}
androidResources {
generateLocaleConfig true
}
}
buildTypes {
@@ -39,9 +41,11 @@ android {
}
buildFeatures {
viewBinding true
buildConfig true
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
main.java.srcDirs += 'src/main/kotlin/'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -59,7 +63,7 @@ android {
}
lint {
abortOnError true
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged'
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled'
}
testOptions {
unitTests.includeAndroidResources true
@@ -78,73 +82,80 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:306d46ea93') {
implementation('com.github.KotatsuApp:kotatsu-parsers:4ca3a492b0') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.10'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.activity:activity-ktx:1.7.1'
implementation 'androidx.fragment:fragment-ktx:1.5.7'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-service:2.6.1'
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.activity:activity-ktx:1.8.0'
implementation 'androidx.fragment:fragment-ktx:1.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.8.1'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
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.8.0'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2'
implementation 'androidx.room:room-runtime:2.5.1'
implementation 'androidx.room:room-ktx:2.5.1'
kapt 'androidx.room:room-compiler:2.5.1'
// TODO https://issuetracker.google.com/issues/254846063
implementation 'androidx.work:work-runtime-ktx:2.8.1'
//noinspection GradleDependency
implementation('com.google.guava:guava:32.0.1-android') {
exclude group: 'com.google.guava', module: 'failureaccess'
exclude group: 'org.checkerframework', module: 'checker-qual'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
}
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
implementation 'com.squareup.okio:okio:3.3.0'
implementation 'androidx.room:room-runtime:2.6.0'
implementation 'androidx.room:room-ktx:2.6.0'
ksp 'androidx.room:room-compiler:2.6.0'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
implementation 'com.squareup.okio:okio:3.6.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.45'
kapt 'com.google.dagger:hilt-compiler:2.45'
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.3.0'
implementation 'io.coil-kt:coil-svg:2.3.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
implementation 'io.coil-kt:coil-base:2.4.0'
implementation 'io.coil-kt:coil-svg:2.4.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:cf089a264d'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2'
implementation 'ch.acra:acra-http:5.9.7'
implementation 'ch.acra:acra-dialog:5.9.7'
implementation 'ch.acra:acra-http:5.11.3'
implementation 'ch.acra:acra-dialog:5.11.3'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20230227'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
testImplementation 'org.json:json:20230618'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
androidTestImplementation 'androidx.room:room-testing:2.5.1'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
androidTestImplementation 'androidx.room:room-testing:2.6.0'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.45'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.45'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.48.1'
}

View File

@@ -1,4 +1,5 @@
-optimizationpasses 8
-dontobfuscate
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void checkExpressionValueIsNotNull(...);
public static void checkNotNullExpressionValue(...);
@@ -7,7 +8,7 @@
public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...);
}
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
-keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.**
@@ -17,3 +18,4 @@
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
-keep class org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment { *; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

View File

@@ -1,10 +0,0 @@
Slice of Life, Mystery
Slice of Life, Mystery
Psychological, Romance, Comedy, Slice of Life, Supernatural
Sci-Fi, Comedy
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Adventure, Slice of Life, Mystery
Adventure, Slice of Life, Mystery

View File

@@ -1,10 +0,0 @@
Forget-me-not Vol. 1
Forget-me-not Vol. 2
La Pomme Prisoinniere
Momo Kanchou no Himitsu Kichi
Omoide Emanon
Sasurai Emanon Vol. 1
Sasurai Emanon Vol. 2
Sasurai Emanon Vol. 3
Wandering Island Vol. 1
Wandering Island Vol. 2

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -19,11 +18,12 @@ import org.junit.runner.RunWith
import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.awaitForIdle
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.data.HistoryRepository
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class ShortcutsUpdaterTest {
class AppShortcutManagerTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@@ -32,7 +32,7 @@ class ShortcutsUpdaterTest {
lateinit var historyRepository: HistoryRepository
@Inject
lateinit var shortcutsUpdater: ShortcutsUpdater
lateinit var appShortcutManager: AppShortcutManager
@Inject
lateinit var database: MangaDatabase
@@ -48,6 +48,7 @@ class ShortcutsUpdaterTest {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return@runTest
}
database.invalidationTracker.addObserver(appShortcutManager)
awaitUpdate()
assertTrue(getShortcuts().isEmpty())
historyRepository.addOrUpdate(
@@ -72,6 +73,6 @@ class ShortcutsUpdaterTest {
private suspend fun awaitUpdate() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
instrumentation.awaitForIdle()
shortcutsUpdater.await()
appShortcutManager.await()
}
}

View File

@@ -5,8 +5,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
@@ -19,7 +17,9 @@ import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.data.HistoryRepository
import java.io.File
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@@ -52,6 +52,7 @@ class AppBackupAgentTest {
title = SampleData.favouriteCategory.title,
sortOrder = SampleData.favouriteCategory.order,
isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled,
isVisibleOnShelf = SampleData.favouriteCategory.isVisibleInLibrary,
)
favouritesRepository.addToCategory(categoryId = category.id, mangas = listOf(SampleData.manga))
historyRepository.addOrUpdate(
@@ -81,7 +82,7 @@ class AppBackupAgentTest {
assertEquals(history, historyRepository.getOne(SampleData.manga))
assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags()
val allTags = database.getTagsDao().findTags(SampleData.tag.source.name).toMangaTags()
assertTrue(SampleData.tag in allTags)
}

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.tracker.domain
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import junit.framework.TestCase.*
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -11,8 +10,9 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,170 +0,0 @@
package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import androidx.room.withTransaction
import dagger.Reusable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.toEntities
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import java.io.File
import java.io.InputStream
import java.util.zip.ZipFile
import javax.inject.Inject
import kotlin.math.roundToInt
private const val MIN_WEBTOON_RATIO = 2
@Reusable
class MangaDataRepository @Inject constructor(
private val okHttpClient: OkHttpClient,
private val db: MangaDatabase,
) {
suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
db.withTransaction {
storeManga(manga)
val entity = db.preferencesDao.find(manga.id) ?: newEntity(manga.id)
db.preferencesDao.upsert(entity.copy(mode = mode.id))
}
}
suspend fun saveColorFilter(manga: Manga, colorFilter: ReaderColorFilter?) {
db.withTransaction {
storeManga(manga)
val entity = db.preferencesDao.find(manga.id) ?: newEntity(manga.id)
db.preferencesDao.upsert(
entity.copy(
cfBrightness = colorFilter?.brightness ?: 0f,
cfContrast = colorFilter?.contrast ?: 0f,
),
)
}
}
suspend fun getReaderMode(mangaId: Long): ReaderMode? {
return db.preferencesDao.find(mangaId)?.let { ReaderMode.valueOf(it.mode) }
}
suspend fun getColorFilter(mangaId: Long): ReaderColorFilter? {
return db.preferencesDao.find(mangaId)?.getColorFilterOrNull()
}
fun observeColorFilter(mangaId: Long): Flow<ReaderColorFilter?> {
return db.preferencesDao.observe(mangaId)
.map { it?.getColorFilterOrNull() }
.distinctUntilChanged()
}
suspend fun findMangaById(mangaId: Long): Manga? {
return db.mangaDao.find(mangaId)?.toManga()
}
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga
intent.mangaId != 0L -> findMangaById(intent.mangaId)
else -> null // TODO resolve uri
}
suspend fun storeManga(manga: Manga) {
val tags = manga.tags.toEntities()
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga.toEntity(), tags)
}
}
suspend fun findTags(source: MangaSource): Set<MangaTag> {
return db.tagsDao.findTags(source.name).toMangaTags()
}
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
return if (cfBrightness != 0f || cfContrast != 0f) {
ReaderColorFilter(cfBrightness, cfContrast)
} else {
null
}
}
/**
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
suspend fun determineMangaIsWebtoon(repository: MangaRepository, pages: List<MangaPage>): Boolean {
val pageIndex = (pages.size * 0.3).roundToInt()
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
val url = repository.getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
}
} else {
val request = Request.Builder()
.url(url)
.get()
.tag(MangaSource::class.java, page.source)
.cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE)
.build()
okHttpClient.newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
}
return size.width * MIN_WEBTOON_RATIO < size.height
}
private fun newEntity(mangaId: Long) = MangaPrefsEntity(
mangaId = mangaId,
mode = -1,
cfBrightness = 0f,
cfContrast = 0f,
)
companion object {
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
options.outMimeType
}
private fun getBitmapSize(input: InputStream?): Size {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)?.recycle()
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}
}

View File

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

View File

@@ -1,56 +0,0 @@
package org.koitharu.kotatsu.base.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
@Suppress("LeakingThis")
abstract class BaseFragment<B : ViewBinding> :
Fragment(),
WindowInsetsDelegate.WindowInsetsListener {
private var viewBinding: B? = null
protected val binding: B
get() = checkNotNull(viewBinding)
@JvmField
protected val exceptionResolver = ExceptionResolver(this)
@JvmField
protected val insetsDelegate = WindowInsetsDelegate(this)
protected val actionModeDelegate: ActionModeDelegate
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = onInflateView(inflater, container)
viewBinding = binding
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
insetsDelegate.onViewCreated(view)
}
override fun onDestroyView() {
viewBinding = null
insetsDelegate.onDestroyView()
super.onDestroyView()
}
protected fun bindingOrNull() = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}

View File

@@ -1,59 +0,0 @@
package org.koitharu.kotatsu.base.ui
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.viewbinding.ViewBinding
@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@Suppress("DEPRECATION")
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
abstract class BaseFullscreenActivity<B : ViewBinding> :
BaseActivity<B>(),
View.OnSystemUiVisibilityChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
with(window) {
statusBarColor = Color.TRANSPARENT
navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
}
showSystemUI()
}
@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
@Deprecated("Deprecated in Java")
final override fun onSystemUiVisibilityChange(visibility: Int) {
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
}
// TODO WindowInsetsControllerCompat works incorrect
@Suppress("DEPRECATION")
protected fun hideSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
}
@Suppress("DEPRECATION")
protected fun showSystemUI() {
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
}
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
}

View File

@@ -1,5 +0,0 @@
package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.LifecycleService
abstract class BaseService : LifecycleService()

View File

@@ -1,57 +0,0 @@
package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
abstract class BaseViewModel : ViewModel() {
@JvmField
protected val loadingCounter = CountedBooleanLiveData()
@JvmField
protected val errorEvent = SingleLiveEvent<Throwable>()
val onError: LiveData<Throwable>
get() = errorEvent
val isLoading: LiveData<Boolean>
get() = loadingCounter
protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
protected fun launchLoadingJob(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
loadingCounter.increment()
try {
block()
} finally {
loadingCounter.decrement()
}
}
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
throwable.printStackTraceDebug()
if (throwable !is CancellationException) {
errorEvent.postCall(throwable)
}
}
}

View File

@@ -1,29 +0,0 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.graphics.Color
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetDialog
class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
/**
* https://github.com/material-components/material-components-android/issues/2582
*/
@Suppress("DEPRECATION")
override fun onAttachedToWindow() {
val window = window
val initialSystemUiVisibility = window?.decorView?.systemUiVisibility ?: 0
super.onAttachedToWindow()
if (window != null) {
// If the navigation bar is translucent at all, the BottomSheet should be edge to edge
val drawEdgeToEdge = edgeToEdgeEnabled && Color.alpha(window.navigationBarColor) < 0xFF
if (drawEdgeToEdge) {
// Copied from super.onAttachedToWindow:
val edgeToEdgeFlags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// Fix super-class's window flag bug by respecting the initial system UI visibility:
window.decorView.systemUiVisibility = edgeToEdgeFlags or initialSystemUiVisibility
}
}
}
}

View File

@@ -1,101 +0,0 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.data.LocalStorageManager
import java.io.File
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {
fun show() = delegate.show()
class Builder(context: Context, storageManager: LocalStorageManager, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(storageManager)
private val delegate = MaterialAlertDialogBuilder(context)
init {
if (adapter.isEmpty) {
delegate.setMessage(R.string.cannot_find_available_storage)
} else {
val defaultValue = runBlocking {
storageManager.getDefaultWriteableDir()
}
adapter.selectedItemPosition = adapter.volumes.indexOfFirst {
it.first.canonicalPath == defaultValue?.canonicalPath
}
delegate.setAdapter(adapter) { d, i ->
listener.onStorageSelected(adapter.getItem(i).first)
d.dismiss()
}
}
}
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
return this
}
fun setTitle(title: CharSequence): Builder {
delegate.setTitle(title)
return this
}
fun setNegativeButton(@StringRes textId: Int): Builder {
delegate.setNegativeButton(textId, null)
return this
}
fun create() = StorageSelectDialog(delegate.create())
}
private class VolumesAdapter(storageManager: LocalStorageManager) : BaseAdapter() {
var selectedItemPosition: Int = -1
val volumes = getAvailableVolumes(storageManager)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.item_storage, parent, false)
val binding = (view.tag as? ItemStorageBinding) ?: ItemStorageBinding.bind(view).also {
view.tag = it
}
val item = volumes[position]
binding.imageViewIndicator.isChecked = selectedItemPosition == position
binding.textViewTitle.text = item.second
binding.textViewSubtitle.text = item.first.path
return view
}
override fun getItem(position: Int): Pair<File, String> = volumes[position]
override fun getItemId(position: Int) = position.toLong()
override fun getCount() = volumes.size
override fun hasStableIds() = true
private fun getAvailableVolumes(storageManager: LocalStorageManager): List<Pair<File, String>> {
return runBlocking {
storageManager.getWriteableDirs().map {
it to storageManager.getStorageDisplayName(it)
}
}
}
}
fun interface OnStorageSelectListener {
fun onStorageSelected(file: File)
}
}

View File

@@ -1,87 +0,0 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
import androidx.core.content.res.getColorOrThrow
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.R as materialR
@SuppressLint("PrivateResource")
abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val bounds = Rect()
private val thickness: Int
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
paint.style = Paint.Style.FILL
val ta = context.obtainStyledAttributes(
null,
materialR.styleable.MaterialDivider,
materialR.attr.materialDividerStyle,
materialR.style.Widget_Material3_MaterialDivider,
)
paint.color = ta.getColorOrThrow(materialR.styleable.MaterialDivider_dividerColor)
thickness = ta.getDimensionPixelSize(
materialR.styleable.MaterialDivider_dividerThickness,
context.resources.getDimensionPixelSize(materialR.dimen.material_divider_thickness),
)
ta.recycle()
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State,
) {
outRect.set(0, thickness, 0, 0)
}
// TODO implement for horizontal lists on demand
override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) {
if (parent.layoutManager == null || thickness == 0) {
return
}
canvas.save()
val left: Float
val right: Float
if (parent.clipToPadding) {
left = parent.paddingLeft.toFloat()
right = (parent.width - parent.paddingRight).toFloat()
canvas.clipRect(
left,
parent.paddingTop.toFloat(),
right,
(parent.height - parent.paddingBottom).toFloat()
)
} else {
left = 0f
right = parent.width.toFloat()
}
var previous: RecyclerView.ViewHolder? = null
for (child in parent.children) {
val holder = parent.getChildViewHolder(child)
if (previous != null && shouldDrawDivider(previous, holder)) {
parent.getDecoratedBoundsWithMargins(child, bounds)
val top: Float = bounds.top + child.translationY
val bottom: Float = top + thickness
canvas.drawRect(left, top, right, bottom, paint)
}
previous = holder
}
canvas.restore()
}
protected abstract fun shouldDrawDivider(
above: RecyclerView.ViewHolder,
below: RecyclerView.ViewHolder,
): Boolean
}

View File

@@ -1,35 +0,0 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Rect
import android.util.SparseIntArray
import android.view.View
import androidx.core.util.getOrDefault
import androidx.core.util.set
import androidx.recyclerview.widget.RecyclerView
class TypedSpacingItemDecoration(
vararg spacingMapping: Pair<Int, Int>,
private val fallbackSpacing: Int = 0,
) : RecyclerView.ItemDecoration() {
private val mapping = SparseIntArray(spacingMapping.size)
init {
spacingMapping.forEach { (k, v) -> mapping[k] = v }
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val itemType = parent.getChildViewHolder(view)?.itemViewType
val spacing = if (itemType == null) {
fallbackSpacing
} else {
mapping.getOrDefault(itemType, fallbackSpacing)
}
outRect.set(spacing, spacing, spacing, spacing)
}
}

View File

@@ -1,31 +0,0 @@
package org.koitharu.kotatsu.base.ui.util
import androidx.annotation.AnyThread
import androidx.lifecycle.LiveData
import java.util.concurrent.atomic.AtomicInteger
class CountedBooleanLiveData : LiveData<Boolean>(false) {
private val counter = AtomicInteger(0)
@AnyThread
fun increment() {
if (counter.getAndIncrement() == 0) {
postValue(true)
}
}
@AnyThread
fun decrement() {
if (counter.decrementAndGet() == 0) {
postValue(false)
}
}
@AnyThread
fun reset() {
if (counter.getAndSet(0) != 0) {
postValue(false)
}
}
}

View File

@@ -1,312 +0,0 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import androidx.annotation.AttrRes
import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.withStyledAttributes
import androidx.core.view.*
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.LayoutSheetHeaderBinding
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
import org.koitharu.kotatsu.utils.ext.getThemeDrawable
import org.koitharu.kotatsu.utils.ext.parents
import java.util.*
import com.google.android.material.R as materialR
private const val THROTTLE_DELAY = 200L
class BottomSheetHeaderBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = materialR.attr.appBarLayoutStyle,
) : AppBarLayout(context, attrs, defStyleAttr), MenuHost {
private val binding = LayoutSheetHeaderBinding.inflate(LayoutInflater.from(context), this)
private val closeDrawable = context.getThemeDrawable(materialR.attr.actionModeCloseDrawable)
private val bottomSheetCallback = Callback()
private val adjustStateRunnable = Runnable { adjustState() }
private var bottomSheetBehavior: BottomSheetBehavior<*>? = null
private val locationBuffer = IntArray(2)
private val expansionListeners = LinkedList<OnExpansionChangeListener>()
private var fitStatusBar = false
private val minHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_min)
private val maxHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_max)
private var isLayoutSuppressedCompat = false
private var isLayoutCalledWhileSuppressed = false
private var isBsExpanded = false
private var stateAdjustedAt = 0L
@Deprecated("")
val toolbar: MaterialToolbar
get() = binding.toolbar
val menu: Menu
get() = binding.toolbar.menu
var title: CharSequence?
get() = binding.toolbar.title
set(value) {
binding.toolbar.title = value
}
var subtitle: CharSequence?
get() = binding.toolbar.subtitle
set(value) {
binding.toolbar.subtitle = value
}
init {
setBackgroundResource(R.drawable.sheet_toolbar_background)
layoutTransition = LayoutTransition().apply {
setDuration(context.getAnimationDuration(R.integer.config_tinyAnimTime))
}
context.withStyledAttributes(attrs, R.styleable.BottomSheetHeaderBar, defStyleAttr) {
binding.toolbar.title = getString(R.styleable.BottomSheetHeaderBar_title)
fitStatusBar = getBoolean(R.styleable.BottomSheetHeaderBar_fitStatusBar, fitStatusBar)
val menuResId = getResourceId(R.styleable.BottomSheetHeaderBar_menu, 0)
if (menuResId != 0) {
binding.toolbar.inflateMenu(menuResId)
}
}
binding.toolbar.setNavigationOnClickListener(bottomSheetCallback)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
setBottomSheetBehavior(findParentBottomSheetBehavior())
}
override fun onDetachedFromWindow() {
setBottomSheetBehavior(null)
super.onDetachedFromWindow()
}
override fun addView(child: View?, index: Int) {
if (shouldAddView(child)) {
super.addView(child, index)
} else {
binding.toolbar.addView(child, index)
}
}
override fun addView(child: View?, width: Int, height: Int) {
if (shouldAddView(child)) {
super.addView(child, width, height)
} else {
binding.toolbar.addView(child, width, height)
}
}
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
if (shouldAddView(child)) {
super.addView(child, index, params)
} else {
binding.toolbar.addView(child, index, convertLayoutParams(params))
}
}
override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets {
dispatchInsets(if (insets != null) WindowInsetsCompat.toWindowInsetsCompat(insets) else null)
return super.onApplyWindowInsets(insets)
}
override fun addMenuProvider(provider: MenuProvider) {
binding.toolbar.addMenuProvider(provider)
}
override fun addMenuProvider(provider: MenuProvider, owner: LifecycleOwner) {
binding.toolbar.addMenuProvider(provider, owner)
}
override fun addMenuProvider(provider: MenuProvider, owner: LifecycleOwner, state: Lifecycle.State) {
binding.toolbar.addMenuProvider(provider, owner, state)
}
override fun removeMenuProvider(provider: MenuProvider) {
binding.toolbar.removeMenuProvider(provider)
}
override fun invalidateMenu() {
binding.toolbar.invalidateMenu()
}
fun inflateMenu(@MenuRes resId: Int) {
binding.toolbar.inflateMenu(resId)
}
fun setNavigationOnClickListener(onClickListener: OnClickListener) {
binding.toolbar.setNavigationOnClickListener(onClickListener)
}
fun addOnExpansionChangeListener(listener: OnExpansionChangeListener) {
expansionListeners.add(listener)
}
fun removeOnExpansionChangeListener(listener: OnExpansionChangeListener) {
expansionListeners.remove(listener)
}
fun setTitle(@StringRes resId: Int) {
binding.toolbar.setTitle(resId)
}
fun setSubtitle(@StringRes resId: Int) {
binding.toolbar.setSubtitle(resId)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (isLayoutSuppressedCompat) {
isLayoutCalledWhileSuppressed = true
} else {
super.onLayout(changed, l, t, r, b)
}
}
private fun setBottomSheetBehavior(behavior: BottomSheetBehavior<*>?) {
bottomSheetBehavior?.removeBottomSheetCallback(bottomSheetCallback)
bottomSheetBehavior = behavior
if (behavior != null) {
onBottomSheetStateChanged(behavior.state)
behavior.addBottomSheetCallback(bottomSheetCallback)
}
}
private fun onBottomSheetStateChanged(newState: Int) {
val expanded = newState == BottomSheetBehavior.STATE_EXPANDED && isOnTopOfScreen()
if (isBsExpanded != expanded) {
isBsExpanded = expanded
postAdjustState()
}
}
private fun suppressLayoutCompat(suppress: Boolean) {
if (suppress == isLayoutSuppressedCompat) return
isLayoutSuppressedCompat = suppress
if (!suppress && isLayoutCalledWhileSuppressed) {
requestLayout()
}
isLayoutCalledWhileSuppressed = false
}
private fun dispatchInsets(insets: WindowInsetsCompat?) {
if (!fitStatusBar) {
return
}
val isExpanded = binding.dragHandle.isGone
val topInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
if (isExpanded) {
updatePadding(top = topInset)
} else {
updatePadding(top = 0)
binding.dragHandle.updateLayoutParams {
height = topInset.coerceIn(minHandleHeight, maxHandleHeight)
}
}
}
private fun findParentBottomSheetBehavior(): BottomSheetBehavior<*>? {
for (p in parents) {
val layoutParams = (p as? View)?.layoutParams
if (layoutParams is CoordinatorLayout.LayoutParams) {
val behavior = layoutParams.behavior
if (behavior is BottomSheetBehavior<*>) {
return behavior
}
}
}
return null
}
private fun isOnTopOfScreen(): Boolean {
getLocationInWindow(locationBuffer)
val topInset = ViewCompat.getRootWindowInsets(this)
?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
val zeroTop = (layoutParams as? MarginLayoutParams)?.topMargin ?: 0
return (locationBuffer[1] - topInset) <= zeroTop
}
private fun dismissBottomSheet() {
val behavior = bottomSheetBehavior ?: return
if (behavior.isHideable) {
behavior.state = BottomSheetBehavior.STATE_HIDDEN
} else {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
private fun shouldAddView(child: View?): Boolean {
if (child == null) {
return true
}
val viewId = child.id
return viewId == R.id.dragHandle || viewId == R.id.toolbar
}
private fun convertLayoutParams(params: ViewGroup.LayoutParams?): Toolbar.LayoutParams? {
return when (params) {
null -> null
is MarginLayoutParams -> {
val lp = Toolbar.LayoutParams(params)
if (params is LayoutParams) {
lp.gravity = params.gravity
}
lp
}
else -> Toolbar.LayoutParams(params)
}
}
private fun postAdjustState() {
removeCallbacks(adjustStateRunnable)
val now = System.currentTimeMillis()
if (stateAdjustedAt + THROTTLE_DELAY < now) {
adjustState()
} else {
postDelayed(adjustStateRunnable, THROTTLE_DELAY)
}
}
private fun adjustState() {
suppressLayoutCompat(true)
binding.toolbar.navigationIcon = (if (isBsExpanded) closeDrawable else null)
binding.dragHandle.isGone = isBsExpanded
expansionListeners.forEach { it.onExpansionStateChanged(this, isBsExpanded) }
dispatchInsets(ViewCompat.getRootWindowInsets(this))
stateAdjustedAt = System.currentTimeMillis()
suppressLayoutCompat(false)
}
private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), OnClickListener {
override fun onStateChanged(bottomSheet: View, newState: Int) {
onBottomSheetStateChanged(newState)
}
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
override fun onClick(v: View?) {
dismissBottomSheet()
}
}
fun interface OnExpansionChangeListener {
fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean)
}
}

View File

@@ -1,33 +0,0 @@
package org.koitharu.kotatsu.bookmarks.data
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
@Dao
abstract class BookmarksDao {
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow<BookmarkEntity?>
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC")
abstract fun observe(mangaId: Long): Flow<List<BookmarkEntity>>
@Transaction
@Query(
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at"
)
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
@Insert
abstract suspend fun insert(entity: BookmarkEntity)
@Delete
abstract suspend fun delete(entity: BookmarkEntity)
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
abstract suspend fun delete(mangaId: Long, pageId: Long)
}

View File

@@ -1,46 +0,0 @@
package org.koitharu.kotatsu.bookmarks.domain
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
class Bookmark(
val manga: Manga,
val pageId: Long,
val chapterId: Long,
val page: Int,
val scroll: Int,
val imageUrl: String,
val createdAt: Date,
val percent: Float,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Bookmark
if (manga != other.manga) return false
if (pageId != other.pageId) return false
if (chapterId != other.chapterId) return false
if (page != other.page) return false
if (scroll != other.scroll) return false
if (imageUrl != other.imageUrl) return false
if (createdAt != other.createdAt) return false
if (percent != other.percent) return false
return true
}
override fun hashCode(): Int {
var result = manga.hashCode()
result = 31 * result + pageId.hashCode()
result = 31 * result + chapterId.hashCode()
result = 31 * result + page
result = 31 * result + scroll
result = 31 * result + imageUrl.hashCode()
result = 31 * result + createdAt.hashCode()
result = 31 * result + percent.hashCode()
return result
}
}

View File

@@ -1,199 +0,0 @@
package org.koitharu.kotatsu.bookmarks.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.bookmarks.data.ids
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
import javax.inject.Inject
@AndroidEntryPoint
class BookmarksFragment :
BaseFragment<FragmentListSimpleBinding>(),
ListStateHolderListener,
OnListItemClickListener<Bookmark>,
SectionedSelectionController.Callback<Manga>,
FastScroller.FastScrollListener {
@Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<BookmarksViewModel>()
private var adapter: BookmarksGroupAdapter? = null
private var selectionController: SectionedSelectionController<Manga>? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
return FragmentListSimpleBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
selectionController = SectionedSelectionController(
activity = requireActivity(),
owner = this,
callback = this,
)
adapter = BookmarksGroupAdapter(
lifecycleOwner = viewLifecycleOwner,
coil = coil,
listener = this,
selectionController = checkNotNull(selectionController),
bookmarkClickListener = this,
groupClickListener = OnGroupClickListener(),
)
binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
val spacingDecoration = SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
binding.recyclerView.addItemDecoration(spacingDecoration)
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
}
override fun onDestroyView() {
super.onDestroyView()
adapter = null
selectionController = null
}
override fun onItemClick(item: Bookmark, view: View) {
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
val intent = ReaderActivity.newIntent(view.context, item)
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}
}
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
return selectionController?.onItemLongClick(item.manga, item.pageId) ?: false
}
override fun onRetryClick(error: Throwable) = Unit
override fun onEmptyActionClick() = Unit
override fun onFastScrollStart(fastScroller: FastScroller) {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
}
override fun onFastScrollStop(fastScroller: FastScroller) = Unit
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
binding.recyclerView.invalidateNestedItemDecorations()
}
override fun onCreateActionMode(
controller: SectionedSelectionController<Manga>,
mode: ActionMode,
menu: Menu,
): Boolean {
mode.menuInflater.inflate(R.menu.mode_bookmarks, menu)
return true
}
override fun onActionItemClicked(
controller: SectionedSelectionController<Manga>,
mode: ActionMode,
item: MenuItem,
): Boolean {
return when (item.itemId) {
R.id.action_remove -> {
val ids = selectionController?.snapshot() ?: return false
viewModel.removeBookmarks(ids)
mode.finish()
true
}
else -> false
}
}
override fun onCreateItemDecoration(
controller: SectionedSelectionController<Manga>,
section: Manga,
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding(
bottom = insets.bottom,
)
binding.recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
}
private fun onListChanged(list: List<ListModel>) {
adapter?.items = list
}
private fun onActionDone(action: ReversibleAction) {
val handle = action.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar = Snackbar.make((activity as SnackbarOwner).snackbarHost, action.stringResId, length)
if (handle != null) {
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
}
snackbar.show()
}
private inner class OnGroupClickListener : OnListItemClickListener<BookmarksGroup> {
override fun onItemClick(item: BookmarksGroup, view: View) {
val controller = selectionController
if (controller != null && controller.count > 0) {
if (controller.getSectionCount(item.manga) == item.bookmarks.size) {
controller.clearSelection(item.manga)
} else {
controller.addToSelection(item.manga, item.bookmarks.ids())
}
return
}
val intent = DetailsActivity.newIntent(view.context, item.manga)
startActivity(intent)
}
override fun onItemLongClick(item: BookmarksGroup, view: View): Boolean {
return selectionController?.addToSelection(item.manga, item.bookmarks.ids()) ?: false
}
}
companion object {
fun newInstance() = BookmarksFragment()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,126 +0,0 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isInvisible
import androidx.fragment.app.setFragmentResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.Headers
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
@AndroidEntryPoint
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
private lateinit var url: String
private val pendingResult = Bundle(1)
@Inject
lateinit var cookieJar: MutableCookieJar
private var onBackPressedCallback: WebViewBackPressedCallback? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
url = requireArguments().getString(ARG_URL).orEmpty()
}
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?,
) = FragmentCloudflareBinding.inflate(inflater, container, false)
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding.webView.settings) {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
domStorageEnabled = true
databaseEnabled = true
userAgentString = arguments?.getString(ARG_UA) ?: CommonHeadersInterceptor.userAgentChrome
}
binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
if (url.isEmpty()) {
dismissAllowingStateLoss()
} else {
binding.webView.loadUrl(url)
}
}
override fun onDestroyView() {
binding.webView.stopLoading()
binding.webView.destroy()
onBackPressedCallback = null
super.onDestroyView()
}
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
return super.onBuildDialog(builder).setNegativeButton(android.R.string.cancel, null)
}
override fun onDialogCreated(dialog: AlertDialog) {
super.onDialogCreated(dialog)
onBackPressedCallback = WebViewBackPressedCallback(binding.webView).also {
dialog.onBackPressedDispatcher.addCallback(it)
}
}
override fun onResume() {
super.onResume()
binding.webView.onResume()
}
override fun onPause() {
binding.webView.onPause()
super.onPause()
}
override fun onDismiss(dialog: DialogInterface) {
setFragmentResult(TAG, pendingResult)
super.onDismiss(dialog)
}
override fun onPageLoaded() {
bindingOrNull()?.progressBar?.isInvisible = true
}
override fun onCheckPassed() {
pendingResult.putBoolean(EXTRA_RESULT, true)
dismissAllowingStateLoss()
}
override fun onHistoryChanged() {
onBackPressedCallback?.onHistoryChanged()
}
companion object {
const val TAG = "CloudFlareDialog"
const val EXTRA_RESULT = "result"
private const val ARG_URL = "url"
private const val ARG_UA = "ua"
fun newInstance(url: String, headers: Headers?) = CloudFlareDialog().withArgs(2) {
putString(ARG_URL, url)
headers?.get(CommonHeaders.USER_AGENT)?.let {
putString(ARG_UA, it)
}
}
}
}

View File

@@ -1,5 +0,0 @@
package org.koitharu.kotatsu.core.cache
import androidx.collection.LruCache
class DeferredLruCache<T>(maxSize: Int) : LruCache<ContentCache.Key, SafeDeferred<T>>(maxSize)

View File

@@ -1,34 +0,0 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
class MangaWithTags(
@Embedded val manga: MangaEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "tag_id",
associateBy = Junction(MangaTagsEntity::class)
)
val tags: List<TagEntity>,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaWithTags
if (manga != other.manga) return false
if (tags != other.tags) return false
return true
}
override fun hashCode(): Int {
var result = manga.hashCode()
result = 31 * result + tags.hashCode()
return result
}
}

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration12To13 : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `show_in_lib` INTEGER NOT NULL DEFAULT 1")
database.execSQL("ALTER TABLE favourites ADD COLUMN `sort_key` INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -1,13 +0,0 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration13To14 : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE favourites ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE history ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration14To15 : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_brightness` REAL NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_contrast` REAL NOT NULL DEFAULT 0")
}
}

View File

@@ -1,11 +0,0 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration3To4 : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
}
}

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration5To6 : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_track_logs_manga_id ON track_logs (manga_id)")
}
}

View File

@@ -1,13 +0,0 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration7To8 : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE manga ADD COLUMN nsfw INTEGER NOT NULL DEFAULT 0")
database.execSQL("CREATE TABLE IF NOT EXISTS suggestions (manga_id INTEGER NOT NULL, relevance REAL NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS index_suggestions_manga_id ON suggestions (manga_id)")
}
}

View File

@@ -1,3 +0,0 @@
package org.koitharu.kotatsu.core.exceptions
class WrongPasswordException : SecurityException()

View File

@@ -1,17 +0,0 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.*
@Parcelize
data class FavouriteCategory(
val id: Long,
val title: String,
val sortKey: Int,
val order: SortOrder,
val createdAt: Date,
val isTrackingEnabled: Boolean,
val isVisibleInLibrary: Boolean,
) : Parcelable

View File

@@ -1,47 +0,0 @@
package org.koitharu.kotatsu.core.model
import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.utils.ext.iterator
fun Collection<Manga>.ids() = mapToSet { it.id }
fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
if (size <= 1) {
return size
}
val acc = HashMap<String?, Int>()
for (item in this) {
val branch = item.chapter.branch
acc[branch] = (acc[branch] ?: 0) + 1
}
return acc.values.max()
}
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
val ch = chapters
if (ch.isNullOrEmpty()) {
return null
}
if (history != null) {
val currentChapter = ch.find { it.id == history.chapterId }
if (currentChapter != null) {
return currentChapter.branch
}
}
val groups = ch.groupBy { it.branch }
for (locale in LocaleListCompat.getAdjustedDefault()) {
var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
if (groups.containsKey(language)) {
return language
}
language = locale.getDisplayName(locale).toTitleCase(locale)
if (groups.containsKey(language)) {
return language
}
}
return groups.maxByOrNull { it.value.size }?.key
}

View File

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

View File

@@ -1,53 +0,0 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.Manga
// Limits to avoid TransactionTooLargeException
private const val MAX_SAFE_SIZE = 1024 * 100 // Assume that 100 kb is safe parcel size
private const val MAX_SAFE_CHAPTERS_COUNT = 24 // this is 100% safe
class ParcelableManga(
val manga: Manga,
private val withChapters: Boolean,
) : Parcelable {
constructor(parcel: Parcel) : this(parcel.readManga(), true)
override fun writeToParcel(parcel: Parcel, flags: Int) {
val chapters = manga.chapters
if (!withChapters || chapters == null) {
manga.writeToParcel(parcel, flags, withChapters = false)
return
}
if (chapters.size <= MAX_SAFE_CHAPTERS_COUNT) {
// fast path
manga.writeToParcel(parcel, flags, withChapters = true)
return
}
val tempParcel = Parcel.obtain()
manga.writeToParcel(tempParcel, flags, withChapters = true)
val size = tempParcel.dataSize()
if (size < MAX_SAFE_SIZE) {
parcel.appendFrom(tempParcel, 0, size)
} else {
manga.writeToParcel(parcel, flags, withChapters = false)
}
tempParcel.recycle()
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ParcelableManga> {
override fun createFromParcel(parcel: Parcel): ParcelableManga {
return ParcelableManga(parcel)
}
override fun newArray(size: Int): Array<ParcelableManga?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -1,35 +0,0 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.MangaChapter
class ParcelableMangaChapters(
val chapters: List<MangaChapter>,
) : Parcelable {
constructor(parcel: Parcel) : this(
List(parcel.readInt()) { parcel.readMangaChapter() }
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(chapters.size)
for (chapter in chapters) {
chapter.writeToParcel(parcel)
}
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ParcelableMangaChapters> {
override fun createFromParcel(parcel: Parcel): ParcelableMangaChapters {
return ParcelableMangaChapters(parcel)
}
override fun newArray(size: Int): Array<ParcelableMangaChapters?> {
return arrayOfNulls(size)
}
}
}

View File

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

View File

@@ -1,36 +0,0 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.Set
class ParcelableMangaTags(
val tags: Set<MangaTag>,
) : Parcelable {
constructor(parcel: Parcel) : this(
Set(parcel.readInt()) { parcel.readMangaTag() }
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(tags.size)
for (tag in tags) {
tag.writeToParcel(parcel)
}
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ParcelableMangaTags> {
override fun createFromParcel(parcel: Parcel): ParcelableMangaTags {
return ParcelableMangaTags(parcel)
}
override fun newArray(size: Int): Array<ParcelableMangaTags?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -1,152 +0,0 @@
package org.koitharu.kotatsu.core.os
import android.app.ActivityManager
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ShortcutManager
import android.os.Build
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.room.InvalidationTracker
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireBitmap
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ShortcutsUpdater @Inject constructor(
@ApplicationContext private val context: Context,
private val coil: ImageLoader,
private val historyRepository: HistoryRepository,
private val mangaRepository: MangaDataRepository,
private val settings: AppSettings,
) : InvalidationTracker.Observer(TABLE_HISTORY), SharedPreferences.OnSharedPreferenceChangeListener {
private val iconSize by lazy { getIconSize(context) }
private var shortcutsUpdateJob: Job? = null
init {
settings.subscribe(this)
}
override fun onInvalidated(tables: Set<String>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || !settings.isDynamicShortcutsEnabled) {
return
}
val prevJob = shortcutsUpdateJob
shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) {
prevJob?.join()
updateShortcutsImpl()
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && key == AppSettings.KEY_SHORTCUTS) {
if (settings.isDynamicShortcutsEnabled) {
onInvalidated(emptySet())
} else {
clearShortcuts()
}
}
}
suspend fun requestPinShortcut(manga: Manga): Boolean {
return ShortcutManagerCompat.requestPinShortcut(
context,
buildShortcutInfo(manga).build(),
null,
)
}
@VisibleForTesting
suspend fun await(): Boolean {
return shortcutsUpdateJob?.join() != null
}
fun isDynamicShortcutsAvailable(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return false
}
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
return manager.maxShortcutCountPerActivity > 0
}
@RequiresApi(Build.VERSION_CODES.N_MR1)
private suspend fun updateShortcutsImpl() = runCatchingCancellable {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
.filter { x -> x.title.isNotEmpty() }
.map { buildShortcutInfo(it).build().toShortcutInfo() }
manager.dynamicShortcuts = shortcuts
}.onFailure {
it.printStackTraceDebug()
}
@RequiresApi(Build.VERSION_CODES.N_MR1)
private fun clearShortcuts() {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
try {
manager.removeAllDynamicShortcuts()
} catch (_: IllegalStateException) {
}
}
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
val icon = runCatchingCancellable {
coil.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.size(iconSize.width, iconSize.height)
.tag(manga.source)
.precision(Precision.EXACT)
.scale(Scale.FILL)
.build(),
).requireBitmap()
}.fold(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
)
mangaRepository.storeManga(manga)
return ShortcutInfoCompat.Builder(context, manga.id.toString())
.setShortLabel(manga.title)
.setLongLabel(manga.title)
.setIcon(icon)
.setIntent(
ReaderActivity.newIntent(context, manga.id)
.setAction(ReaderActivity.ACTION_MANGA_READ),
)
}
private fun getIconSize(context: Context): Size {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
(context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager).let {
Size(it.iconMaxWidth, it.iconMaxHeight)
}
} else {
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).launcherLargeIconSize.let {
Size(it, it)
}
}
}
}

View File

@@ -1,37 +0,0 @@
package org.koitharu.kotatsu.core.parser
import android.content.Context
import androidx.annotation.ColorRes
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.model.MangaTag
import javax.inject.Inject
@Reusable
class MangaTagHighlighter @Inject constructor(
@ApplicationContext context: Context,
) {
private val dict by lazy {
context.resources.openRawResource(R.raw.tags_redlist).use {
val set = HashSet<String>()
it.bufferedReader().forEachLine { x ->
val line = x.trim()
if (line.isNotEmpty()) {
set.add(line)
}
}
set
}
}
@ColorRes
fun getTint(tag: MangaTag): Int {
return if (tag.title.lowercase() in dict) {
R.color.warning
} else {
0
}
}
}

View File

@@ -1,15 +0,0 @@
package org.koitharu.kotatsu.core.prefs
import android.content.Context
import androidx.core.content.edit
private const val CATEGORY_ID = "cat_id"
class AppWidgetConfig(context: Context, val widgetId: Int) {
private val prefs = context.getSharedPreferences("appwidget_$widgetId", Context.MODE_PRIVATE)
var categoryId: Long
get() = prefs.getLong(CATEGORY_ID, 0L)
set(value) = prefs.edit { putLong(CATEGORY_ID, value) }
}

View File

@@ -1,6 +0,0 @@
package org.koitharu.kotatsu.details.domain
class BranchComparator : Comparator<String?> {
override fun compare(o1: String?, o2: String?): Int = compareValues(o1, o2)
}

View File

@@ -1,336 +0,0 @@
package org.koitharu.kotatsu.details.ui
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.transition.Slide
import android.transition.TransitionManager
import android.view.Gravity
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.utils.ViewBadge
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.utils.ext.textAndVisible
import javax.inject.Inject
@AndroidEntryPoint
class DetailsActivity :
BaseActivity<ActivityDetailsBinding>(),
View.OnClickListener,
BottomSheetHeaderBar.OnExpansionChangeListener,
NoModalBottomSheetOwner {
override val bsHeader: BottomSheetHeaderBar?
get() = binding.headerChapters
@Inject
lateinit var shortcutsUpdater: ShortcutsUpdater
private lateinit var viewBadge: ViewBadge
private val viewModel: DetailsViewModel by viewModels()
private lateinit var chaptersMenuProvider: ChaptersMenuProvider
private val downloadReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val downloadedManga = DownloadService.getDownloadedManga(intent) ?: return
viewModel.onDownloadComplete(downloadedManga)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityDetailsBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setDisplayShowTitleEnabled(false)
}
binding.buttonRead.setOnClickListener(this)
binding.buttonDropdown.setOnClickListener(this)
viewBadge = ViewBadge(binding.buttonRead, this)
chaptersMenuProvider = if (binding.layoutBottom != null) {
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom))
actionModeDelegate.addListener(bsMediator)
checkNotNull(binding.headerChapters).addOnExpansionChangeListener(bsMediator)
checkNotNull(binding.headerChapters).addOnLayoutChangeListener(bsMediator)
onBackPressedDispatcher.addCallback(bsMediator)
ChaptersMenuProvider(viewModel, bsMediator)
} else {
ChaptersMenuProvider(viewModel, null)
}
viewModel.manga.observe(this, ::onMangaUpdated)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(
this,
SnackbarErrorObserver(
host = binding.containerDetails,
fragment = null,
resolver = exceptionResolver,
onResolved = { isResolved ->
if (isResolved) {
viewModel.reload()
}
},
),
)
viewModel.onShowToast.observe(this) {
makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show()
}
viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranchName.observe(this) {
binding.headerChapters?.subtitle = it
binding.textViewSubtitle?.textAndVisible = it
}
viewModel.isChaptersReversed.observe(this) {
binding.headerChapters?.invalidateMenu() ?: invalidateOptionsMenu()
}
viewModel.favouriteCategories.observe(this) {
invalidateOptionsMenu()
}
viewModel.branches.observe(this) {
binding.buttonDropdown.isVisible = it.size > 1
}
viewModel.chapters.observe(this, PrefetchObserver(this))
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
addMenuProvider(
DetailsMenuProvider(
activity = this,
viewModel = viewModel,
snackbarHost = binding.containerChapters,
shortcutsUpdater = shortcutsUpdater,
),
)
binding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider)
}
override fun onDestroy() {
unregisterReceiver(downloadReceiver)
super.onDestroy()
}
override fun onClick(v: View) {
val manga = viewModel.manga.value ?: return
when (v.id) {
R.id.button_read -> {
val chapterId = viewModel.historyInfo.value?.history?.chapterId
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
showChapterMissingDialog(chapterId)
} else {
startActivity(
ReaderActivity.newIntent(
context = this,
manga = manga,
branch = viewModel.selectedBranchValue,
),
)
}
}
R.id.button_dropdown -> showBranchPopupMenu()
}
}
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
if (isExpanded) {
headerBar.addMenuProvider(chaptersMenuProvider)
} else {
headerBar.removeMenuProvider(chaptersMenuProvider)
}
binding.buttonRead.isGone = isExpanded
}
private fun onMangaUpdated(manga: Manga) {
title = manga.title
val hasChapters = !manga.chapters.isNullOrEmpty()
binding.buttonRead.isEnabled = hasChapters
invalidateOptionsMenu()
showBottomSheet(manga.chapters != null)
binding.groupHeader?.isVisible = hasChapters
}
private fun onMangaRemoved(manga: Manga) {
Toast.makeText(
this,
getString(R.string._s_deleted_from_local_storage, manga.title),
Toast.LENGTH_SHORT,
).show()
finishAfterTransition()
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
if (insets.bottom > 0) {
window.setNavigationBarTransparentCompat(this, binding.layoutBottom?.elevation ?: 0f, 0.9f)
}
}
private fun onHistoryChanged(info: HistoryInfo) {
with(binding.buttonRead) {
if (info.history != null) {
setText(R.string._continue)
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
} else {
setText(R.string.read)
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
}
}
val text = when {
!info.isValid -> getString(R.string.loading_)
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters)
info.totalChapters == 0 -> getString(R.string.no_chapters)
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
}
binding.headerChapters?.title = text
binding.textViewTitle?.text = text
}
private fun onNewChaptersChanged(newChapters: Int) {
viewBadge.counter = newChapters
}
fun showChapterMissingDialog(chapterId: Long) {
val remoteManga = viewModel.getRemoteManga()
if (remoteManga == null) {
val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT)
snackbar.show()
return
}
MaterialAlertDialogBuilder(this).apply {
setMessage(R.string.chapter_is_missing_text)
setTitle(R.string.chapter_is_missing)
setNegativeButton(android.R.string.cancel, null)
setPositiveButton(R.string.read) { _, _ ->
startActivity(
ReaderActivity.newIntent(
context = this@DetailsActivity,
manga = remoteManga,
state = ReaderState(chapterId, 0, 0),
),
)
}
setNeutralButton(R.string.download) { _, _ ->
DownloadService.start(binding.appbar, remoteManga, setOf(chapterId))
}
setCancelable(true)
}.show()
}
private fun showBranchPopupMenu() {
val menu = PopupMenu(this, binding.headerChapters ?: binding.buttonDropdown)
val currentBranch = viewModel.selectedBranchValue
for (branch in viewModel.branches.value ?: return) {
val item = menu.menu.add(R.id.group_branches, Menu.NONE, Menu.NONE, branch)
item.isChecked = branch == currentBranch
}
menu.menu.setGroupCheckable(R.id.group_branches, true, true)
menu.setOnMenuItemClickListener { item ->
viewModel.setSelectedBranch(item.title?.toString())
true
}
menu.show()
}
private fun resolveError(e: Throwable) {
lifecycleScope.launch {
if (exceptionResolver.resolve(e)) {
viewModel.reload()
} else if (viewModel.manga.value == null) {
Toast.makeText(this@DetailsActivity, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition()
}
}
}
private fun isTabletLayout() = binding.layoutBottom == null
private fun showBottomSheet(isVisible: Boolean) {
val view = binding.layoutBottom ?: return
if (view.isVisible == isVisible) return
val transition = Slide(Gravity.BOTTOM)
transition.addTarget(view)
transition.interpolator = AccelerateDecelerateInterpolator()
TransitionManager.beginDelayedTransition(binding.root as ViewGroup, transition)
view.isVisible = isVisible
}
private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar {
val sb = Snackbar.make(binding.containerDetails, text, duration)
if (binding.layoutBottom?.isVisible == true) {
sb.anchorView = binding.headerChapters
}
return sb
}
private class PrefetchObserver(
private val context: Context,
) : Observer<List<ChapterListItem>?> {
private var isCalled = false
override fun onChanged(value: List<ChapterListItem>?) {
if (value.isNullOrEmpty()) {
return
}
if (!isCalled) {
isCalled = true
val item = value.find { it.hasFlag(ChapterListItem.FLAG_CURRENT) } ?: value.first()
MangaPrefetchService.prefetchPages(context, item.chapter)
}
}
}
companion object {
fun newIntent(context: Context, manga: Manga): Intent {
return Intent(context, DetailsActivity::class.java)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
}
fun newIntent(context: Context, mangaId: Long): Intent {
return Intent(context, DetailsActivity::class.java)
.putExtra(MangaIntent.KEY_ID, mangaId)
}
}
}

View File

@@ -1,324 +0,0 @@
package org.koitharu.kotatsu.details.ui
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import coil.ImageLoader
import coil.request.ImageRequest
import coil.util.CoilUtils
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.core.model.countChaptersByBranch
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.computeSize
import org.koitharu.kotatsu.utils.ext.crossfade
import org.koitharu.kotatsu.utils.ext.drawableTop
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty
import org.koitharu.kotatsu.utils.ext.measureHeight
import org.koitharu.kotatsu.utils.ext.resolveDp
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.utils.ext.textAndVisible
import org.koitharu.kotatsu.utils.ext.toFileOrNull
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
import javax.inject.Inject
@AndroidEntryPoint
class DetailsFragment :
BaseFragment<FragmentDetailsBinding>(),
View.OnClickListener,
ChipsView.OnChipClickListener,
OnListItemClickListener<Bookmark> {
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var tagHighlighter: MangaTagHighlighter
private val viewModel by activityViewModels<DetailsViewModel>()
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?,
) = FragmentDetailsBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textViewAuthor.setOnClickListener(this)
binding.imageViewCover.setOnClickListener(this)
binding.infoLayout.textViewSource.setOnClickListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
binding.chipsTags.onChipClickListener = this
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
viewModel.chapters.observe(viewLifecycleOwner, ::onChaptersChanged)
}
override fun onItemClick(item: Bookmark, view: View) {
startActivity(
ReaderActivity.newIntent(view.context, item),
scaleUpActivityOptionsOf(view).toBundle(),
)
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
val menu = PopupMenu(view.context, view)
menu.inflate(R.menu.popup_bookmark)
menu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.action_remove -> viewModel.removeBookmark(item)
}
true
}
menu.show()
return true
}
private fun onMangaUpdated(manga: Manga) {
with(binding) {
// Main
loadCover(manga)
textViewTitle.text = manga.title
textViewSubtitle.textAndVisible = manga.altTitle
textViewAuthor.textAndVisible = manga.author
if (manga.hasRating) {
ratingBar.rating = manga.rating * ratingBar.numStars
ratingBar.isVisible = true
} else {
ratingBar.isVisible = false
}
when (manga.state) {
MangaState.FINISHED -> {
infoLayout.textViewState.apply {
textAndVisible = resources.getString(R.string.state_finished)
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_finished)
}
}
MangaState.ONGOING -> {
infoLayout.textViewState.apply {
textAndVisible = resources.getString(R.string.state_ongoing)
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_ongoing)
}
}
else -> infoLayout.textViewState.isVisible = false
}
if (manga.source == MangaSource.LOCAL) {
infoLayout.textViewSource.isVisible = false
val file = manga.url.toUri().toFileOrNull()
if (file != null) {
viewLifecycleScope.launch {
val size = file.computeSize()
infoLayout.textViewSize.text = FileSize.BYTES.format(requireContext(), size)
infoLayout.textViewSize.isVisible = true
}
} else {
infoLayout.textViewSize.isVisible = false
}
} else {
infoLayout.textViewSource.text = manga.source.title
infoLayout.textViewSource.isVisible = true
infoLayout.textViewSize.isVisible = false
}
infoLayout.textViewNsfw.isVisible = manga.isNsfw
// Chips
bindTags(manga)
}
}
private fun onChaptersChanged(chapters: List<ChapterListItem>?) {
val infoLayout = binding.infoLayout
if (chapters.isNullOrEmpty()) {
infoLayout.textViewChapters.isVisible = false
} else {
val count = chapters.countChaptersByBranch()
infoLayout.textViewChapters.isVisible = true
infoLayout.textViewChapters.text = resources.getQuantityString(R.plurals.chapters, count, count)
}
}
private fun onDescriptionChanged(description: CharSequence?) {
if (description.isNullOrBlank()) {
binding.textViewDescription.setText(R.string.no_description)
} else {
binding.textViewDescription.text = description
}
}
private fun onHistoryChanged(history: HistoryInfo) {
binding.progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true)
}
private fun onLoadingStateChanged(isLoading: Boolean) {
if (isLoading) {
binding.progressBar.show()
} else {
binding.progressBar.hide()
}
}
private fun onBookmarksChanged(bookmarks: List<Bookmark>) {
var adapter = binding.recyclerViewBookmarks.adapter as? BookmarksAdapter
binding.groupBookmarks.isGone = bookmarks.isEmpty()
if (adapter != null) {
adapter.items = bookmarks
} else {
adapter = BookmarksAdapter(coil, viewLifecycleOwner, this)
adapter.items = bookmarks
binding.recyclerViewBookmarks.adapter = adapter
val spacing = resources.getDimensionPixelOffset(R.dimen.bookmark_list_spacing)
binding.recyclerViewBookmarks.addItemDecoration(SpacingItemDecoration(spacing))
}
}
private fun onScrobblingInfoChanged(scrobblings: List<ScrobblingInfo>) {
var adapter = binding.recyclerViewScrobbling.adapter as? ScrollingInfoAdapter
binding.recyclerViewScrobbling.isGone = scrobblings.isEmpty()
if (adapter != null) {
adapter.items = scrobblings
} else {
adapter = ScrollingInfoAdapter(viewLifecycleOwner, coil, childFragmentManager)
adapter.items = scrobblings
binding.recyclerViewScrobbling.adapter = adapter
binding.recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration())
}
}
override fun onClick(v: View) {
val manga = viewModel.manga.value ?: return
when (v.id) {
R.id.textView_author -> {
startActivity(
SearchActivity.newIntent(
context = v.context,
source = manga.source,
query = manga.author ?: return,
),
)
}
R.id.textView_source -> {
startActivity(
MangaListActivity.newIntent(
context = v.context,
source = manga.source,
),
)
}
R.id.imageView_cover -> {
startActivity(
ImageActivity.newIntent(
v.context,
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
manga.source,
),
scaleUpActivityOptionsOf(v).toBundle(),
)
}
}
}
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag ?: return
startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag)))
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding(
bottom = (
(activity as? NoModalBottomSheetOwner)?.bsHeader?.measureHeight()
?.plus(insets.bottom)?.plus(resources.resolveDp(16))
)
?: insets.bottom,
)
}
private fun bindTags(manga: Manga) {
binding.chipsTags.setChips(
manga.tags.map { tag ->
ChipsView.ChipModel(
title = tag.title,
tint = tagHighlighter.getTint(tag),
data = tag,
isCheckable = false,
isChecked = false,
)
},
)
}
private fun loadCover(manga: Manga) {
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
val lastResult = CoilUtils.result(binding.imageViewCover)
if (lastResult?.request?.data == imageUrl) {
return
}
val request = ImageRequest.Builder(context ?: return)
.target(binding.imageViewCover)
.size(CoverSizeResolver(binding.imageViewCover))
.data(imageUrl)
.tag(manga.source)
.crossfade(requireContext())
.lifecycle(viewLifecycleOwner)
.placeholderMemoryCacheKey(manga.coverUrl)
val previousDrawable = lastResult?.drawable
if (previousDrawable != null) {
request.fallback(previousDrawable)
.placeholder(previousDrawable)
.error(previousDrawable)
} else {
request.fallback(R.drawable.ic_placeholder)
.placeholder(R.drawable.ic_placeholder)
.error(R.drawable.ic_error_placeholder)
}
request.enqueueWith(coil)
}
}

View File

@@ -1,311 +0,0 @@
package org.koitharu.kotatsu.details.ui
import android.text.Html
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.core.text.getSpans
import androidx.core.text.parseAsHtml
import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.io.IOException
import javax.inject.Inject
@HiltViewModel
class DetailsViewModel @Inject constructor(
private val historyRepository: HistoryRepository,
favouritesRepository: FavouritesRepository,
private val localMangaRepository: LocalMangaRepository,
trackingRepository: TrackingRepository,
private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings,
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
private val imageGetter: Html.ImageGetter,
private val delegate: MangaDetailsDelegate,
) : BaseViewModel() {
private var loadingJob: Job
val onShowToast = SingleLiveEvent<Int>()
private val history = historyRepository.observeOne(delegate.mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
private val newChapters = settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
.flatMapLatest { isEnabled ->
if (isEnabled) {
trackingRepository.observeNewChaptersCount(delegate.mangaId)
} else {
flowOf(0)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
private val chaptersQuery = MutableStateFlow("")
private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
val historyInfo: LiveData<HistoryInfo> = combine(
delegate.manga,
delegate.selectedBranch,
history,
historyRepository.observeShouldSkip(delegate.manga),
) { m, b, h, im ->
HistoryInfo(m, b, h, im)
}.asFlowLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
defaultValue = HistoryInfo(null, null, null, false),
)
val bookmarks = delegate.manga.flatMapLatest {
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val description = delegate.manga
.distinctUntilChangedBy { it?.description.orEmpty() }
.transformLatest {
val description = it?.description
if (description.isNullOrEmpty()) {
emit(null)
} else {
emit(description.parseAsHtml().filterSpans())
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null)
val onMangaRemoved = SingleLiveEvent<Manga>()
val isScrobblingAvailable: Boolean
get() = scrobblers.any { it.isAvailable }
val scrobblingInfo: LiveData<List<ScrobblingInfo>> = combine(
scrobblers.map { it.observeScrobblingInfo(delegate.mangaId) },
) { scrobblingInfo ->
scrobblingInfo.filterNotNull()
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val branches: LiveData<List<String?>> = delegate.manga.map {
val chapters = it?.chapters ?: return@map emptyList()
chapters.mapToSet { x -> x.branch }.sortedWith(BranchComparator())
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val selectedBranchIndex = combine(
branches.asFlow(),
delegate.selectedBranch,
) { branches, selected ->
branches.indexOf(selected)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, -1)
val selectedBranchName = delegate.selectedBranch
.asFlowLiveData(viewModelScope.coroutineContext, null)
val isChaptersEmpty: LiveData<Boolean> = combine(
delegate.manga,
isLoading.asFlow(),
) { m, loading ->
m != null && m.chapters.isNullOrEmpty() && !loading
}.asFlowLiveData(viewModelScope.coroutineContext, false)
val chapters = combine(
combine(
delegate.manga,
delegate.relatedManga,
history,
delegate.selectedBranch,
newChapters,
) { manga, related, history, branch, news ->
delegate.mapChapters(manga, related, history, news, branch)
},
chaptersReversed,
chaptersQuery,
) { list, reversed, query ->
(if (reversed) list.asReversed() else list).filterSearch(query)
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val selectedBranchValue: String?
get() = delegate.selectedBranch.value
init {
loadingJob = doLoad()
}
fun reload() {
loadingJob.cancel()
loadingJob = doLoad()
}
fun deleteLocal() {
val m = delegate.manga.value
if (m == null) {
onShowToast.call(R.string.file_not_found)
return
}
launchLoadingJob(Dispatchers.Default) {
val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)?.manga
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
val original = localMangaRepository.getRemoteManga(manga)
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
onMangaRemoved.postCall(manga)
}
}
fun removeBookmark(bookmark: Bookmark) {
launchJob {
bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId)
onShowToast.call(R.string.bookmark_removed)
}
}
fun setChaptersReversed(newValue: Boolean) {
settings.chaptersReverse = newValue
}
fun setSelectedBranch(branch: String?) {
delegate.selectedBranch.value = branch
}
fun getRemoteManga(): Manga? {
return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
}
fun performChapterSearch(query: String?) {
chaptersQuery.value = query?.trim().orEmpty()
}
fun onDownloadComplete(downloadedManga: Manga) {
val currentManga = delegate.manga.value ?: return
if (currentManga.id != downloadedManga.id) {
return
}
if (currentManga.source == MangaSource.LOCAL) {
reload()
} else {
viewModelScope.launch(Dispatchers.Default) {
runCatchingCancellable {
localMangaRepository.getDetails(downloadedManga)
}.onSuccess {
delegate.relatedManga.value = it
}.onFailure {
it.printStackTraceDebug()
}
}
}
}
fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) {
val scrobbler = getScrobbler(index) ?: return
launchJob(Dispatchers.Default) {
scrobbler.updateScrobblingInfo(
mangaId = delegate.mangaId,
rating = rating,
status = status,
comment = null,
)
}
}
fun unregisterScrobbling(index: Int) {
val scrobbler = getScrobbler(index) ?: return
launchJob(Dispatchers.Default) {
scrobbler.unregisterScrobbling(
mangaId = delegate.mangaId,
)
}
}
fun markChapterAsCurrent(chapterId: Long) {
launchJob(Dispatchers.Default) {
val manga = checkNotNull(delegate.manga.value)
val chapters = checkNotNull(manga.getChapters(selectedBranchValue))
val chapterIndex = chapters.indexOfFirst { it.id == chapterId }
check(chapterIndex in chapters.indices) { "Chapter not found" }
val percent = chapterIndex / chapters.size.toFloat()
historyRepository.addOrUpdate(manga = manga, chapterId = chapterId, page = 0, scroll = 0, percent = percent)
}
}
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
delegate.doLoad()
}
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
if (query.isEmpty() || this.isEmpty()) {
return this
}
return filter {
it.chapter.name.contains(query, ignoreCase = true)
}
}
private fun Spanned.filterSpans(): CharSequence {
val spannable = SpannableString.valueOf(this)
val spans = spannable.getSpans<ForegroundColorSpan>()
for (span in spans) {
spannable.removeSpan(span)
}
return spannable.trim()
}
private fun getScrobbler(index: Int): Scrobbler? {
val info = scrobblingInfo.value?.getOrNull(index)
val scrobbler = if (info != null) {
scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable }
} else {
null
}
if (scrobbler == null) {
errorEvent.call(IllegalStateException("Scrobbler [$index] is not available"))
}
return scrobbler
}
}

View File

@@ -1,16 +0,0 @@
package org.koitharu.kotatsu.details.ui
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount() = 2
override fun createFragment(position: Int): Fragment = when (position) {
0 -> DetailsFragment()
1 -> ChaptersFragment()
else -> throw IndexOutOfBoundsException("No fragment for position $position")
}
}

View File

@@ -1,162 +0,0 @@
package org.koitharu.kotatsu.details.ui
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
@ViewModelScoped
class MangaDetailsDelegate @Inject constructor(
savedStateHandle: SavedStateHandle,
private val mangaDataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository,
private val localMangaRepository: LocalMangaRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
private val intent = MangaIntent(savedStateHandle)
private val mangaData = MutableStateFlow(intent.manga)
val selectedBranch = MutableStateFlow<String?>(null)
// Remote manga for saved and saved for remote
val relatedManga = MutableStateFlow<Manga?>(null)
val manga: StateFlow<Manga?>
get() = mangaData
val mangaId = intent.manga?.id ?: intent.mangaId
suspend fun doLoad() {
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
mangaData.value = manga
manga = mangaRepositoryFactory.create(manga.source).getDetails(manga)
// find default branch
val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist)
mangaData.value = manga
relatedManga.value = runCatchingCancellable {
if (manga.source == MangaSource.LOCAL) {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
mangaRepositoryFactory.create(m.source).getDetails(m)
} else {
localMangaRepository.findSavedManga(manga)?.manga
}
}.onFailure { error ->
error.printStackTraceDebug()
}.getOrNull()
}
fun mapChapters(
manga: Manga?,
related: Manga?,
history: MangaHistory?,
newCount: Int,
branch: String?,
): List<ChapterListItem> {
val chapters = manga?.chapters ?: return emptyList()
val relatedChapters = related?.chapters
return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
} else {
mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
}
}
private fun mapChapters(
chapters: List<MangaChapter>,
downloadedChapters: List<MangaChapter>?,
currentId: Long?,
newCount: Int,
branch: String?,
): List<ChapterListItem> {
val result = ArrayList<ChapterListItem>(chapters.size)
val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount
val downloadedIds = downloadedChapters?.mapTo(HashSet(downloadedChapters.size)) { it.id }
for (i in chapters.indices) {
val chapter = chapters[i]
if (chapter.branch != branch) {
continue
}
result += chapter.toListItem(
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = false,
isDownloaded = downloadedIds?.contains(chapter.id) == true,
)
}
if (result.size < chapters.size / 2) {
result.trimToSize()
}
return result
}
private fun mapChaptersWithSource(
chapters: List<MangaChapter>,
sourceChapters: List<MangaChapter>,
currentId: Long?,
newCount: Int,
branch: String?,
): List<ChapterListItem> {
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
val result = ArrayList<ChapterListItem>(sourceChapters.size)
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
val firstNewIndex = sourceChapters.size - newCount
for (i in sourceChapters.indices) {
val chapter = sourceChapters[i]
val localChapter = chaptersMap.remove(chapter.id)
if (chapter.branch != branch) {
continue
}
result += localChapter?.toListItem(
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = false,
isDownloaded = false,
) ?: chapter.toListItem(
isCurrent = i == currentIndex,
isUnread = i > currentIndex,
isNew = i >= firstNewIndex,
isMissing = true,
isDownloaded = false,
)
}
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
result.ensureCapacity(result.size + chaptersMap.size)
chaptersMap.values.mapNotNullTo(result) {
if (it.branch == branch) {
it.toListItem(
isCurrent = false,
isUnread = true,
isNew = false,
isMissing = false,
isDownloaded = false,
)
} else {
null
}
}
result.sortBy { it.chapter.number }
}
if (result.size < sourceChapters.size / 2) {
result.trimToSize()
}
return result
}
}

View File

@@ -1,45 +0,0 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.replaceWith
class BranchesAdapter : BaseAdapter() {
private val dataSet = ArrayList<String?>()
override fun getCount(): Int {
return dataSet.size
}
override fun getItem(position: Int): Any? {
return dataSet[position]
}
override fun getItemId(position: Int): Long {
return dataSet[position].hashCode().toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_branch, parent, false)
(view as TextView).text = dataSet[position]
return view
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.item_branch_dropdown, parent, false)
(view as TextView).text = dataSet[position]
return view
}
fun setItems(items: Collection<String?>) {
dataSet.replaceWith(items)
notifyDataSetChanged()
}
}

View File

@@ -1,56 +0,0 @@
package org.koitharu.kotatsu.details.ui.adapter
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemChapterBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun chapterListItemAD(
clickListener: OnListItemClickListener<ChapterListItem>,
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener)
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
bind { payloads ->
if (payloads.isEmpty()) {
binding.textViewTitle.text = item.chapter.name
binding.textViewNumber.text = item.chapter.number.toString()
binding.textViewDescription.textAndVisible = item.description()
}
when (item.status) {
FLAG_UNREAD -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
binding.textViewNumber.setTextColor(context.getThemeColor(com.google.android.material.R.attr.colorOnTertiary))
}
FLAG_CURRENT -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
}
else -> {
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
}
}
val isMissing = item.hasFlag(FLAG_MISSING)
binding.textViewTitle.alpha = if (isMissing) 0.3f else 1f
binding.textViewDescription.alpha = if (isMissing) 0.3f else 1f
binding.textViewNumber.alpha = if (isMissing) 0.3f else 1f
binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
}
}

View File

@@ -1,49 +0,0 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.content.Context
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import kotlin.jvm.internal.Intrinsics
class ChaptersAdapter(
onItemClickListener: OnListItemClickListener<ChapterListItem>,
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()), FastScroller.SectionIndexer {
init {
setHasStableIds(true)
delegatesManager.addDelegate(chapterListItemAD(onItemClickListener))
}
override fun getItemId(position: Int): Long {
return items[position].chapter.id
}
private class DiffCallback : DiffUtil.ItemCallback<ChapterListItem>() {
override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
return oldItem.chapter.id == newItem.chapter.id
}
override fun areContentsTheSame(
oldItem: ChapterListItem,
newItem: ChapterListItem
): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
if (oldItem.flags != newItem.flags && oldItem.chapter == newItem.chapter) {
return newItem.flags
}
return null
}
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
val item = items.getOrNull(position) ?: return null
return item.chapter.number.toString()
}
}

View File

@@ -1,34 +0,0 @@
package org.koitharu.kotatsu.details.ui.scrobbling
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
class ScrollingInfoAdapter(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
fragmentManager: FragmentManager,
) : AsyncListDifferDelegationAdapter<ScrobblingInfo>(DiffCallback()) {
init {
delegatesManager.addDelegate(scrobblingInfoAD(lifecycleOwner, coil, fragmentManager))
}
private class DiffCallback : DiffUtil.ItemCallback<ScrobblingInfo>() {
override fun areItemsTheSame(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Boolean {
return oldItem.scrobbler == newItem.scrobbler
}
override fun areContentsTheSame(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Any {
return Unit
}
}
}

View File

@@ -1,269 +0,0 @@
package org.koitharu.kotatsu.download.domain
import android.app.Service
import android.content.Context
import android.webkit.MimeTypeMap
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ServiceScoped
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.internal.closeQuietly
import okio.IOException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.ui.service.PausingHandle
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
import java.io.File
import javax.inject.Inject
private const val MAX_FAILSAFE_ATTEMPTS = 2
private const val DOWNLOAD_ERROR_DELAY = 500L
private const val SLOWDOWN_DELAY = 150L
@ServiceScoped
class DownloadManager @Inject constructor(
service: Service,
@ApplicationContext private val context: Context,
private val imageLoader: ImageLoader,
private val okHttp: OkHttpClient,
private val cache: PagesCache,
private val localMangaRepository: LocalMangaRepository,
private val settings: AppSettings,
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
private val coverWidth = context.resources.getDimensionPixelSize(
androidx.core.R.dimen.compat_notification_large_icon_max_width,
)
private val coverHeight = context.resources.getDimensionPixelSize(
androidx.core.R.dimen.compat_notification_large_icon_max_height,
)
private val semaphore = Semaphore(settings.downloadsParallelism)
private val coroutineScope = (service as LifecycleService).lifecycleScope
fun downloadManga(
manga: Manga,
chaptersIds: LongArray?,
startId: Int,
): PausingProgressJob<DownloadState> {
val stateFlow = MutableStateFlow<DownloadState>(
DownloadState.Queued(startId = startId, manga = manga, cover = null),
)
val pausingHandle = PausingHandle()
val job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(stateFlow)) {
try {
downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId)
} catch (e: CancellationException) { // handle cancellation if not handled already
val state = stateFlow.value
if (state !is DownloadState.Cancelled) {
stateFlow.value = DownloadState.Cancelled(startId, state.manga, state.cover)
}
throw e
}
}
return PausingProgressJob(job, stateFlow, pausingHandle)
}
private suspend fun downloadMangaImpl(
manga: Manga,
chaptersIds: LongArray?,
outState: MutableStateFlow<DownloadState>,
pausingHandle: PausingHandle,
startId: Int,
) {
@Suppress("NAME_SHADOWING")
var manga = manga
val chaptersIdsSet = chaptersIds?.toMutableSet()
val cover = loadCover(manga)
outState.value = DownloadState.Queued(startId, manga, cover)
withMangaLock(manga) {
semaphore.withPermit {
outState.value = DownloadState.Preparing(startId, manga, null)
val destination = localMangaRepository.getOutputDir(manga)
checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) }
val tempFileName = "${manga.id}_$startId.tmp"
var output: LocalMangaOutput? = null
try {
if (manga.source == MangaSource.LOCAL) {
manga = localMangaRepository.getRemoteManga(manga)
?: error("Cannot obtain remote manga instance")
}
val repo = mangaRepositoryFactory.create(manga.source)
outState.value = DownloadState.Preparing(startId, manga, cover)
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = LocalMangaOutput.getOrCreate(destination, data)
val coverUrl = data.largeCoverUrl ?: data.coverUrl
downloadFile(coverUrl, destination, tempFileName, repo.source).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
}
val chapters = checkNotNull(
if (chaptersIdsSet == null) {
data.chapters
} else {
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
},
) { "Chapters list must not be null" }
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
check(chaptersIdsSet.isNullOrEmpty()) {
"${chaptersIdsSet?.size} of ${chaptersIds?.size} requested chapters not found in manga"
}
for ((chapterIndex, chapter) in chapters.withIndex()) {
val pages = runFailsafe(outState, pausingHandle) {
repo.getPages(chapter)
}
for ((pageIndex, page) in pages.withIndex()) {
runFailsafe(outState, pausingHandle) {
val url = repo.getPageUrl(page)
val file = cache.get(url)
?: downloadFile(url, destination, tempFileName, repo.source)
output.addPage(
chapter = chapter,
file = file,
pageNumber = pageIndex,
ext = MimeTypeMap.getFileExtensionFromUrl(url),
)
}
outState.value = DownloadState.Progress(
startId = startId,
manga = data,
cover = cover,
totalChapters = chapters.size,
currentChapter = chapterIndex,
totalPages = pages.size,
currentPage = pageIndex,
)
if (settings.isDownloadsSlowdownEnabled) {
delay(SLOWDOWN_DELAY)
}
}
output.flushChapter(chapter)
}
outState.value = DownloadState.PostProcessing(startId, data, cover)
output.mergeWithExisting()
output.finish()
val localManga = LocalMangaInput.of(output.rootFile).getManga().manga
outState.value = DownloadState.Done(startId, data, cover, localManga)
} catch (e: CancellationException) {
outState.value = DownloadState.Cancelled(startId, manga, cover)
throw e
} catch (e: Throwable) {
e.printStackTraceDebug()
outState.value = DownloadState.Error(startId, manga, cover, e, false)
} finally {
withContext(NonCancellable) {
output?.closeQuietly()
output?.cleanup()
File(destination, tempFileName).deleteAwait()
}
}
}
}
}
private suspend fun <R> runFailsafe(
outState: MutableStateFlow<DownloadState>,
pausingHandle: PausingHandle,
block: suspend () -> R,
): R {
var countDown = MAX_FAILSAFE_ATTEMPTS
failsafe@ while (true) {
try {
return block()
} catch (e: IOException) {
if (countDown <= 0) {
val state = outState.value
outState.value = DownloadState.Error(state.startId, state.manga, state.cover, e, true)
countDown = MAX_FAILSAFE_ATTEMPTS
pausingHandle.pause()
pausingHandle.awaitResumed()
outState.value = state
} else {
countDown--
delay(DOWNLOAD_ERROR_DELAY)
}
}
}
}
private suspend fun downloadFile(
url: String,
destination: File,
tempFileName: String,
source: MangaSource,
): File {
val request = Request.Builder()
.url(url)
.tag(MangaSource::class.java, source)
.cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE)
.get()
.build()
val call = okHttp.newCall(request)
val file = File(destination, tempFileName)
val response = call.clone().await()
file.outputStream().use { out ->
checkNotNull(response.body).byteStream().copyToSuspending(out)
}
return file
}
private fun errorStateHandler(outState: MutableStateFlow<DownloadState>) =
CoroutineExceptionHandler { _, throwable ->
throwable.printStackTraceDebug()
val prevValue = outState.value
outState.value = DownloadState.Error(
startId = prevValue.startId,
manga = prevValue.manga,
cover = prevValue.cover,
error = throwable,
canRetry = false,
)
}
private suspend fun loadCover(manga: Manga) = runCatchingCancellable {
imageLoader.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.allowHardware(false)
.tag(manga.source)
.size(coverWidth, coverHeight)
.scale(Scale.FILL)
.build(),
).drawable
}.getOrNull()
private suspend inline fun <T> withMangaLock(manga: Manga, block: () -> T) = try {
localMangaRepository.lockManga(manga.id)
block()
} finally {
localMangaRepository.unlockManga(manga.id)
}
}

View File

@@ -1,234 +0,0 @@
package org.koitharu.kotatsu.download.domain
import android.graphics.drawable.Drawable
import org.koitharu.kotatsu.parsers.model.Manga
sealed interface DownloadState {
val startId: Int
val manga: Manga
val cover: Drawable?
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
val isTerminal: Boolean
get() = this is Done || this is Cancelled || (this is Error && !canRetry)
class Queued(
override val startId: Int,
override val manga: Manga,
override val cover: Drawable?,
) : DownloadState {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Queued
if (startId != other.startId) return false
if (manga != other.manga) return false
if (cover != other.cover) return false
return true
}
override fun hashCode(): Int {
var result = startId
result = 31 * result + manga.hashCode()
result = 31 * result + (cover?.hashCode() ?: 0)
return result
}
}
class Preparing(
override val startId: Int,
override val manga: Manga,
override val cover: Drawable?,
) : DownloadState {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Preparing
if (startId != other.startId) return false
if (manga != other.manga) return false
if (cover != other.cover) return false
return true
}
override fun hashCode(): Int {
var result = startId
result = 31 * result + manga.hashCode()
result = 31 * result + (cover?.hashCode() ?: 0)
return result
}
}
class Progress(
override val startId: Int,
override val manga: Manga,
override val cover: Drawable?,
val totalChapters: Int,
val currentChapter: Int,
val totalPages: Int,
val currentPage: Int,
) : DownloadState {
val max: Int = totalChapters * totalPages
val progress: Int = totalPages * currentChapter + currentPage + 1
val percent: Float = progress.toFloat() / max
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Progress
if (startId != other.startId) return false
if (manga != other.manga) return false
if (cover != other.cover) return false
if (totalChapters != other.totalChapters) return false
if (currentChapter != other.currentChapter) return false
if (totalPages != other.totalPages) return false
if (currentPage != other.currentPage) return false
return true
}
override fun hashCode(): Int {
var result = startId
result = 31 * result + manga.hashCode()
result = 31 * result + (cover?.hashCode() ?: 0)
result = 31 * result + totalChapters
result = 31 * result + currentChapter
result = 31 * result + totalPages
result = 31 * result + currentPage
return result
}
}
class Done(
override val startId: Int,
override val manga: Manga,
override val cover: Drawable?,
val localManga: Manga,
) : DownloadState {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Done
if (startId != other.startId) return false
if (manga != other.manga) return false
if (cover != other.cover) return false
if (localManga != other.localManga) return false
return true
}
override fun hashCode(): Int {
var result = startId
result = 31 * result + manga.hashCode()
result = 31 * result + (cover?.hashCode() ?: 0)
result = 31 * result + localManga.hashCode()
return result
}
}
class Error(
override val startId: Int,
override val manga: Manga,
override val cover: Drawable?,
val error: Throwable,
val canRetry: Boolean,
) : DownloadState {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Error
if (startId != other.startId) return false
if (manga != other.manga) return false
if (cover != other.cover) return false
if (error != other.error) return false
if (canRetry != other.canRetry) return false
return true
}
override fun hashCode(): Int {
var result = startId
result = 31 * result + manga.hashCode()
result = 31 * result + (cover?.hashCode() ?: 0)
result = 31 * result + error.hashCode()
result = 31 * result + canRetry.hashCode()
return result
}
}
class Cancelled(
override val startId: Int,
override val manga: Manga,
override val cover: Drawable?,
) : DownloadState {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Cancelled
if (startId != other.startId) return false
if (manga != other.manga) return false
if (cover != other.cover) return false
return true
}
override fun hashCode(): Int {
var result = startId
result = 31 * result + manga.hashCode()
result = 31 * result + (cover?.hashCode() ?: 0)
return result
}
}
class PostProcessing(
override val startId: Int,
override val manga: Manga,
override val cover: Drawable?,
) : DownloadState {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PostProcessing
if (startId != other.startId) return false
if (manga != other.manga) return false
if (cover != other.cover) return false
return true
}
override fun hashCode(): Int {
var result = startId
result = 31 * result + manga.hashCode()
result = 31 * result + (cover?.hashCode() ?: 0)
return result
}
}
}

View File

@@ -1,140 +0,0 @@
package org.koitharu.kotatsu.download.ui
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.onFirst
import org.koitharu.kotatsu.utils.ext.source
fun downloadItemAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
) = adapterDelegateViewBinding<DownloadItem, DownloadItem, ItemDownloadBinding>(
{ inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) },
) {
var job: Job? = null
val percentPattern = context.resources.getString(R.string.percent_string_pattern)
val clickListener = View.OnClickListener { v ->
when (v.id) {
R.id.button_cancel -> item.cancel()
R.id.button_resume -> item.resume()
else -> context.startActivity(
DetailsActivity.newIntent(context, item.progressValue.manga),
)
}
}
binding.buttonCancel.setOnClickListener(clickListener)
binding.buttonResume.setOnClickListener(clickListener)
itemView.setOnClickListener(clickListener)
bind {
job?.cancel()
job = item.progressAsFlow().onFirst { state ->
binding.imageViewCover.newImageRequest(lifecycleOwner, state.manga.coverUrl)?.run {
placeholder(state.cover)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
source(state.manga.source)
allowRgb565(true)
enqueueWith(coil)
}
}.onEach { state ->
binding.textViewTitle.text = state.manga.title
when (state) {
is DownloadState.Cancelled -> {
binding.textViewStatus.setText(R.string.cancelling_)
binding.progressBar.isIndeterminate = true
binding.progressBar.isVisible = true
binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
}
is DownloadState.Done -> {
binding.textViewStatus.setText(R.string.download_complete)
binding.progressBar.isIndeterminate = false
binding.progressBar.isVisible = false
binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
}
is DownloadState.Error -> {
binding.textViewStatus.setText(R.string.error_occurred)
binding.progressBar.isIndeterminate = false
binding.progressBar.isVisible = false
binding.textViewPercent.isVisible = false
binding.textViewDetails.text = state.error.getDisplayMessage(context.resources)
binding.textViewDetails.isVisible = true
binding.buttonCancel.isVisible = state.canRetry
binding.buttonResume.isVisible = state.canRetry
}
is DownloadState.PostProcessing -> {
binding.textViewStatus.setText(R.string.processing_)
binding.progressBar.isIndeterminate = true
binding.progressBar.isVisible = true
binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
}
is DownloadState.Preparing -> {
binding.textViewStatus.setText(R.string.preparing_)
binding.progressBar.isIndeterminate = true
binding.progressBar.isVisible = true
binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
}
is DownloadState.Progress -> {
binding.textViewStatus.setText(R.string.manga_downloading_)
binding.progressBar.isIndeterminate = false
binding.progressBar.isVisible = true
binding.progressBar.max = state.max
binding.progressBar.setProgressCompat(state.progress, true)
binding.textViewPercent.text = percentPattern.format((state.percent * 100f).format(1))
binding.textViewPercent.isVisible = true
binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
}
is DownloadState.Queued -> {
binding.textViewStatus.setText(R.string.queued)
binding.progressBar.isIndeterminate = false
binding.progressBar.isVisible = false
binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
}
}
}.launchIn(lifecycleOwner.lifecycleScope)
}
onViewRecycled {
job?.cancel()
job = null
}
}

View File

@@ -1,58 +0,0 @@
package org.koitharu.kotatsu.download.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
import javax.inject.Inject
@AndroidEntryPoint
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
@Inject
lateinit var coil: ImageLoader
private lateinit var serviceConnection: DownloadsConnection
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val adapter = DownloadsAdapter(this, coil)
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter
serviceConnection = DownloadsConnection(this, this)
serviceConnection.items.observe(this) { items ->
adapter.items = items
binding.textViewHolder.isVisible = items.isNullOrEmpty()
}
serviceConnection.bind()
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
)
binding.toolbar.updatePadding(
left = insets.left,
right = insets.right,
)
}
companion object {
fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java)
}
}

View File

@@ -1,46 +0,0 @@
package org.koitharu.kotatsu.download.ui
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
typealias DownloadItem = PausingProgressJob<DownloadState>
class DownloadsAdapter(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
) : AsyncListDifferDelegationAdapter<DownloadItem>(DiffCallback()) {
init {
delegatesManager.addDelegate(downloadItemAD(lifecycleOwner, coil))
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return items[position].progressValue.startId.toLong()
}
private class DiffCallback : DiffUtil.ItemCallback<DownloadItem>() {
override fun areItemsTheSame(
oldItem: DownloadItem,
newItem: DownloadItem,
): Boolean {
return oldItem.progressValue.startId == newItem.progressValue.startId
}
override fun areContentsTheSame(
oldItem: DownloadItem,
newItem: DownloadItem,
): Boolean {
return oldItem.progressValue == newItem.progressValue && oldItem.isPaused == newItem.isPaused
}
override fun getChangePayload(oldItem: DownloadItem, newItem: DownloadItem): Any {
return Unit
}
}
}

View File

@@ -1,76 +0,0 @@
package org.koitharu.kotatsu.download.ui
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
class DownloadsConnection(
private val context: Context,
private val lifecycleOwner: LifecycleOwner,
) : ServiceConnection {
private var bindingObserver: BindingLifecycleObserver? = null
private var collectJob: Job? = null
private val itemsFlow = MutableStateFlow<List<PausingProgressJob<DownloadState>>>(emptyList())
val items
get() = itemsFlow.asFlowLiveData()
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
collectJob?.cancel()
val binder = (service as? DownloadService.DownloadBinder)
collectJob = if (binder == null) {
null
} else {
lifecycleOwner.lifecycleScope.launch {
binder.downloads.collect {
itemsFlow.value = it
}
}
}
}
override fun onServiceDisconnected(name: ComponentName?) {
collectJob?.cancel()
collectJob = null
itemsFlow.value = itemsFlow.value.filter { it.progressValue.isTerminal }
}
fun bind() {
if (bindingObserver != null) {
return
}
bindingObserver = BindingLifecycleObserver().also {
lifecycleOwner.lifecycle.addObserver(it)
}
context.bindService(Intent(context, DownloadService::class.java), this, 0)
}
fun unbind() {
bindingObserver?.let {
lifecycleOwner.lifecycle.removeObserver(it)
}
bindingObserver = null
context.unbindService(this)
}
private inner class BindingLifecycleObserver : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
unbind()
}
}
}

View File

@@ -1,356 +0,0 @@
package org.koitharu.kotatsu.download.ui.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import android.text.format.DateUtils
import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.text.HtmlCompat
import androidx.core.text.htmlEncode
import androidx.core.text.parseAsHtml
import androidx.core.util.forEach
import androidx.core.util.isNotEmpty
import androidx.core.util.size
import com.google.android.material.R as materialR
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.DownloadsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class DownloadNotification(private val context: Context) {
private val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val states = SparseArray<DownloadState>()
private val groupBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
private val queueIntent = PendingIntentCompat.getActivity(
context,
REQUEST_QUEUE,
DownloadsActivity.newIntent(context),
0,
false,
)
private val localListIntent = PendingIntentCompat.getActivity(
context,
REQUEST_LIST_LOCAL,
MangaListActivity.newIntent(context, MangaSource.LOCAL),
0,
false,
)
init {
groupBuilder.setOnlyAlertOnce(true)
groupBuilder.setDefaults(0)
groupBuilder.color = ContextCompat.getColor(context, R.color.blue_primary)
groupBuilder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
groupBuilder.setSilent(true)
groupBuilder.setGroup(GROUP_ID)
groupBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
groupBuilder.setGroupSummary(true)
groupBuilder.setContentTitle(context.getString(R.string.downloading_manga))
}
fun buildGroupNotification(): Notification {
val style = NotificationCompat.InboxStyle(groupBuilder)
var progress = 0f
var isAllDone = true
var isInProgress = false
groupBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
states.forEach { _, state ->
if (state.manga.isNsfw) {
groupBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
}
val summary = when (state) {
is DownloadState.Cancelled -> {
progress++
context.getString(R.string.cancelling_)
}
is DownloadState.Done -> {
progress++
context.getString(R.string.download_complete)
}
is DownloadState.Error -> {
isAllDone = false
context.getString(R.string.error)
}
is DownloadState.PostProcessing -> {
progress++
isInProgress = true
isAllDone = false
context.getString(R.string.processing_)
}
is DownloadState.Preparing -> {
isAllDone = false
isInProgress = true
context.getString(R.string.preparing_)
}
is DownloadState.Progress -> {
isAllDone = false
isInProgress = true
progress += state.percent
context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
}
is DownloadState.Queued -> {
isAllDone = false
isInProgress = true
context.getString(R.string.queued)
}
}
style.addLine(
context.getString(
R.string.download_summary_pattern,
state.manga.title.ellipsize(16).htmlEncode(),
summary.htmlEncode(),
).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY),
)
}
progress = if (isInProgress) {
progress / states.size.toFloat()
} else {
1f
}
style.setBigContentTitle(
context.getString(if (isAllDone) R.string.download_complete else R.string.downloading_manga),
)
groupBuilder.setContentText(context.resources.getQuantityString(R.plurals.items, states.size, states.size()))
groupBuilder.setNumber(states.size)
groupBuilder.setSmallIcon(
if (isInProgress) android.R.drawable.stat_sys_download else android.R.drawable.stat_sys_download_done,
)
groupBuilder.setContentIntent(if (isAllDone) localListIntent else queueIntent)
groupBuilder.setAutoCancel(isAllDone)
when (progress) {
1f -> groupBuilder.setProgress(0, 0, false)
0f -> groupBuilder.setProgress(1, 0, true)
else -> groupBuilder.setProgress(100, (progress * 100f).toInt(), false)
}
return groupBuilder.build()
}
fun detach() {
if (states.isNotEmpty()) {
val notification = buildGroupNotification()
manager.notify(ID_GROUP_DETACHED, notification)
}
manager.cancel(ID_GROUP)
}
fun newItem(startId: Int) = Item(startId)
inner class Item(
private val startId: Int,
) {
private val builder = NotificationCompat.Builder(context, CHANNEL_ID)
private val cancelAction = NotificationCompat.Action(
materialR.drawable.material_ic_clear_black_24dp,
context.getString(android.R.string.cancel),
PendingIntentCompat.getBroadcast(
context,
startId * 2,
DownloadService.getCancelIntent(startId),
PendingIntent.FLAG_CANCEL_CURRENT,
false,
),
)
private val retryAction = NotificationCompat.Action(
R.drawable.ic_restart_black,
context.getString(R.string.try_again),
PendingIntentCompat.getBroadcast(
context,
startId * 2 + 1,
DownloadService.getResumeIntent(startId),
PendingIntent.FLAG_CANCEL_CURRENT,
false,
),
)
init {
builder.setOnlyAlertOnce(true)
builder.setDefaults(0)
builder.color = ContextCompat.getColor(context, R.color.blue_primary)
builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
builder.setSilent(true)
builder.setGroup(GROUP_ID)
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
}
fun notify(state: DownloadState, timeLeft: Long) {
builder.setContentTitle(state.manga.title)
builder.setContentText(context.getString(R.string.manga_downloading_))
builder.setProgress(1, 0, true)
builder.setSmallIcon(android.R.drawable.stat_sys_download)
builder.setContentIntent(queueIntent)
builder.setStyle(null)
builder.setLargeIcon(state.cover?.toBitmap())
builder.clearActions()
builder.setSubText(null)
builder.setShowWhen(false)
builder.setVisibility(
if (state.manga.isNsfw) {
NotificationCompat.VISIBILITY_PRIVATE
} else {
NotificationCompat.VISIBILITY_PUBLIC
},
)
when (state) {
is DownloadState.Cancelled -> {
builder.setProgress(1, 0, true)
builder.setContentText(context.getString(R.string.cancelling_))
builder.setContentIntent(null)
builder.setStyle(null)
builder.setOngoing(true)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
}
is DownloadState.Done -> {
builder.setProgress(0, 0, false)
builder.setContentText(context.getString(R.string.download_complete))
builder.setContentIntent(createMangaIntent(context, state.localManga))
builder.setAutoCancel(true)
builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
builder.setCategory(null)
builder.setStyle(null)
builder.setOngoing(false)
builder.setShowWhen(true)
builder.setWhen(System.currentTimeMillis())
builder.priority = NotificationCompat.PRIORITY_DEFAULT
}
is DownloadState.Error -> {
val message = state.error.getDisplayMessage(context.resources)
builder.setProgress(0, 0, false)
builder.setSmallIcon(android.R.drawable.stat_notify_error)
builder.setSubText(context.getString(R.string.error))
builder.setContentText(message)
builder.setAutoCancel(!state.canRetry)
builder.setOngoing(state.canRetry)
builder.setCategory(NotificationCompat.CATEGORY_ERROR)
builder.setShowWhen(true)
builder.setWhen(System.currentTimeMillis())
builder.setStyle(NotificationCompat.BigTextStyle().bigText(message))
if (state.canRetry) {
builder.addAction(cancelAction)
builder.addAction(retryAction)
}
builder.priority = NotificationCompat.PRIORITY_DEFAULT
}
is DownloadState.PostProcessing -> {
builder.setProgress(1, 0, true)
builder.setContentText(context.getString(R.string.processing_))
builder.setStyle(null)
builder.setOngoing(true)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
}
is DownloadState.Queued -> {
builder.setProgress(0, 0, false)
builder.setContentText(context.getString(R.string.queued))
builder.setStyle(null)
builder.setOngoing(true)
builder.addAction(cancelAction)
builder.priority = NotificationCompat.PRIORITY_LOW
}
is DownloadState.Preparing -> {
builder.setProgress(1, 0, true)
builder.setContentText(context.getString(R.string.preparing_))
builder.setStyle(null)
builder.setOngoing(true)
builder.addAction(cancelAction)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
}
is DownloadState.Progress -> {
builder.setProgress(state.max, state.progress, false)
val percent = context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
if (timeLeft > 0L) {
val eta = DateUtils.getRelativeTimeSpanString(timeLeft, 0L, DateUtils.SECOND_IN_MILLIS)
builder.setContentText(eta)
builder.setSubText(percent)
} else {
builder.setContentText(percent)
}
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
builder.setStyle(null)
builder.setOngoing(true)
builder.addAction(cancelAction)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
}
}
val notification = builder.build()
states.append(startId, state)
updateGroupNotification()
manager.notify(TAG, startId, notification)
}
fun dismiss() {
manager.cancel(TAG, startId)
states.remove(startId)
updateGroupNotification()
}
}
private fun updateGroupNotification() {
val notification = buildGroupNotification()
manager.notify(ID_GROUP, notification)
}
private fun createMangaIntent(context: Context, manga: Manga) = PendingIntentCompat.getActivity(
context,
manga.hashCode(),
DetailsActivity.newIntent(context, manga),
PendingIntent.FLAG_CANCEL_CURRENT,
false,
)
companion object {
private const val TAG = "download"
private const val CHANNEL_ID = "download"
private const val GROUP_ID = "downloads"
private const val REQUEST_QUEUE = 6
private const val REQUEST_LIST_LOCAL = 7
const val ID_GROUP = 9999
private const val ID_GROUP_DETACHED = 9998
fun createChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = NotificationManagerCompat.from(context)
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
val channel = NotificationChannel(
CHANNEL_ID,
context.getString(R.string.downloads),
NotificationManager.IMPORTANCE_LOW,
)
channel.enableVibration(false)
channel.enableLights(false)
channel.setSound(null, null)
manager.createNotificationChannel(channel)
}
}
}
}
}

View File

@@ -1,276 +0,0 @@
package org.koitharu.kotatsu.download.ui.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Binder
import android.os.IBinder
import android.os.PowerManager
import android.view.View
import androidx.annotation.MainThread
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseService
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.download.domain.DownloadManager
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.DownloadsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.utils.ext.throttle
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
import org.koitharu.kotatsu.utils.progress.ProgressJob
import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.collections.set
@AndroidEntryPoint
class DownloadService : BaseService() {
private lateinit var downloadNotification: DownloadNotification
private lateinit var wakeLock: PowerManager.WakeLock
@Inject
lateinit var downloadManager: DownloadManager
private val jobs = LinkedHashMap<Int, PausingProgressJob<DownloadState>>()
private val jobCount = MutableStateFlow(0)
private val controlReceiver = ControlReceiver()
override fun onCreate() {
super.onCreate()
isRunning = true
downloadNotification = DownloadNotification(this)
wakeLock = (applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
wakeLock.acquire(TimeUnit.HOURS.toMillis(8))
DownloadNotification.createChannel(this)
startForeground(DownloadNotification.ID_GROUP, downloadNotification.buildGroupNotification())
val intentFilter = IntentFilter()
intentFilter.addAction(ACTION_DOWNLOAD_CANCEL)
intentFilter.addAction(ACTION_DOWNLOAD_RESUME)
ContextCompat.registerReceiver(this, controlReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val manga = intent?.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga
val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS)
return if (manga != null) {
jobs[startId] = downloadManga(startId, manga, chapters)
jobCount.value = jobs.size
START_REDELIVER_INTENT
} else {
stopSelfIfIdle()
START_NOT_STICKY
}
}
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return DownloadBinder(this)
}
override fun onDestroy() {
unregisterReceiver(controlReceiver)
if (wakeLock.isHeld) {
wakeLock.release()
}
isRunning = false
super.onDestroy()
}
private fun downloadManga(
startId: Int,
manga: Manga,
chaptersIds: LongArray?,
): PausingProgressJob<DownloadState> {
val job = downloadManager.downloadManga(manga, chaptersIds, startId)
listenJob(job)
return job
}
private fun listenJob(job: ProgressJob<DownloadState>) {
lifecycleScope.launch {
val startId = job.progressValue.startId
val notificationItem = downloadNotification.newItem(startId)
try {
val timeLeftEstimator = TimeLeftEstimator()
notificationItem.notify(job.progressValue, -1L)
job.progressAsFlow()
.onEach { state ->
if (state is DownloadState.Progress) {
timeLeftEstimator.tick(value = state.progress, total = state.max)
} else {
timeLeftEstimator.emptyTick()
}
}
.throttle { state -> if (state is DownloadState.Progress) 400L else 0L }
.whileActive()
.collect { state ->
val timeLeft = timeLeftEstimator.getEstimatedTimeLeft()
notificationItem.notify(state, timeLeft)
}
job.join()
} finally {
(job.progressValue as? DownloadState.Done)?.let {
sendBroadcast(
Intent(ACTION_DOWNLOAD_COMPLETE)
.putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)),
)
}
if (job.isCancelled) {
notificationItem.dismiss()
if (jobs.remove(startId) != null) {
jobCount.value = jobs.size
}
} else {
notificationItem.notify(job.progressValue, -1L)
}
}
}.invokeOnCompletion {
stopSelfIfIdle()
}
}
private fun Flow<DownloadState>.whileActive(): Flow<DownloadState> = transformWhile { state ->
emit(state)
!state.isTerminal
}
@MainThread
private fun stopSelfIfIdle() {
if (jobs.any { (_, job) -> job.isActive }) {
return
}
downloadNotification.detach()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
inner class ControlReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
when (intent?.action) {
ACTION_DOWNLOAD_CANCEL -> {
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
jobs[cancelId]?.cancel()
}
ACTION_DOWNLOAD_RESUME -> {
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
jobs[cancelId]?.resume()
}
}
}
}
class DownloadBinder(service: DownloadService) : Binder(), DefaultLifecycleObserver {
private var downloadsStateFlow = MutableStateFlow<List<PausingProgressJob<DownloadState>>>(emptyList())
init {
service.lifecycle.addObserver(this)
service.jobCount.onEach {
downloadsStateFlow.value = service.jobs.values.toList()
}.launchIn(service.lifecycleScope)
}
override fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
downloadsStateFlow.value = emptyList()
super.onDestroy(owner)
}
val downloads
get() = downloadsStateFlow.asStateFlow()
}
companion object {
var isRunning: Boolean = false
private set
@Deprecated("Use LocalMangaRepository.watchReadableDirs instead")
const val ACTION_DOWNLOAD_COMPLETE = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_COMPLETE"
private const val ACTION_DOWNLOAD_CANCEL = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_CANCEL"
private const val ACTION_DOWNLOAD_RESUME = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_RESUME"
const val EXTRA_MANGA = "manga"
private const val EXTRA_CHAPTERS_IDS = "chapters_ids"
private const val EXTRA_CANCEL_ID = "cancel_id"
fun start(view: View, manga: Manga, chaptersIds: Collection<Long>? = null) {
if (chaptersIds?.isEmpty() == true) {
return
}
val intent = Intent(view.context, DownloadService::class.java)
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
if (chaptersIds != null) {
intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
}
ContextCompat.startForegroundService(view.context, intent)
showStartedSnackbar(view)
}
fun start(view: View, manga: Collection<Manga>) {
if (manga.isEmpty()) {
return
}
for (item in manga) {
val intent = Intent(view.context, DownloadService::class.java)
intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false))
ContextCompat.startForegroundService(view.context, intent)
}
showStartedSnackbar(view)
}
fun confirmAndStart(view: View, items: Set<Manga>) {
MaterialAlertDialogBuilder(view.context)
.setTitle(R.string.save_manga)
.setMessage(R.string.batch_manga_save_confirm)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.save) { _, _ ->
start(view, items)
}.show()
}
fun getCancelIntent(startId: Int) = Intent(ACTION_DOWNLOAD_CANCEL)
.putExtra(EXTRA_CANCEL_ID, startId)
fun getResumeIntent(startId: Int) = Intent(ACTION_DOWNLOAD_RESUME)
.putExtra(EXTRA_CANCEL_ID, startId)
fun getDownloadedManga(intent: Intent?): Manga? {
if (intent?.action == ACTION_DOWNLOAD_COMPLETE) {
return intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga
}
return null
}
private fun showStartedSnackbar(view: View) {
Snackbar.make(view, R.string.download_started, Snackbar.LENGTH_LONG)
.setAction(R.string.details) {
it.context.startActivity(DownloadsActivity.newIntent(it.context))
}.show()
}
}
}

View File

@@ -1,42 +0,0 @@
package org.koitharu.kotatsu.explore.domain
import javax.inject.Inject
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
class ExploreRepository @Inject constructor(
private val settings: AppSettings,
private val historyRepository: HistoryRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
suspend fun findRandomManga(tagsLimit: Int): Manga {
val blacklistTagRegex = settings.getSuggestionsTagsBlacklistRegex()
val allTags = historyRepository.getPopularTags(tagsLimit).filterNot {
blacklistTagRegex?.containsMatchIn(it.title) ?: false
}
val tag = allTags.randomOrNull()
val source = checkNotNull(tag?.source ?: settings.getMangaSources(includeHidden = false).randomOrNull()) {
"No sources found"
}
val repo = mangaRepositoryFactory.create(source)
val list = repo.getList(
offset = 0,
sortOrder = if (SortOrder.UPDATED in repo.sortOrders) SortOrder.UPDATED else null,
tags = setOfNotNull(tag),
).shuffled()
for (item in list) {
if (settings.isSuggestionsExcludeNsfw && item.isNsfw) {
continue
}
if (blacklistTagRegex != null && item.tags.any { x -> blacklistTagRegex.containsMatchIn(x.title) }) {
continue
}
return item
}
return list.random()
}
}

View File

@@ -1,170 +0,0 @@
package org.koitharu.kotatsu.explore.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.base.ui.util.SpanSizeResolver
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter
import org.koitharu.kotatsu.explore.ui.adapter.ExploreListEventListener
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.history.ui.HistoryActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
import org.koitharu.kotatsu.utils.ext.addMenuProvider
import javax.inject.Inject
@AndroidEntryPoint
class ExploreFragment :
BaseFragment<FragmentExploreBinding>(),
RecyclerViewOwner,
ExploreListEventListener,
OnListItemClickListener<ExploreItem.Source> {
@Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<ExploreViewModel>()
private var exploreAdapter: ExploreAdapter? = null
private var paddingHorizontal = 0
override val recyclerView: RecyclerView
get() = binding.recyclerView
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentExploreBinding {
return FragmentExploreBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this)
with(binding.recyclerView) {
adapter = exploreAdapter
setHasFixedSize(true)
SpanSizeResolver(this, resources.getDimensionPixelSize(R.dimen.explore_grid_width)).attach()
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
paddingHorizontal = spacing
}
addMenuProvider(ExploreMenuProvider(view.context, viewModel))
viewModel.content.observe(viewLifecycleOwner) {
exploreAdapter?.items = it
}
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged)
}
override fun onDestroyView() {
super.onDestroyView()
exploreAdapter = null
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding(
bottom = insets.bottom,
)
}
override fun onManageClick(view: View) {
startActivity(SettingsActivity.newManageSourcesIntent(view.context))
}
override fun onClick(v: View) {
val intent = when (v.id) {
R.id.button_history -> HistoryActivity.newIntent(v.context)
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)
R.id.button_bookmarks -> BookmarksActivity.newIntent(v.context)
R.id.button_suggestions -> SuggestionsActivity.newIntent(v.context)
R.id.button_favourites -> FavouriteCategoriesActivity.newIntent(v.context)
R.id.button_random -> {
viewModel.openRandom()
return
}
else -> return
}
startActivity(intent)
}
override fun onItemClick(item: ExploreItem.Source, view: View) {
val intent = MangaListActivity.newIntent(view.context, item.source)
startActivity(intent)
}
override fun onItemLongClick(item: ExploreItem.Source, view: View): Boolean {
val menu = PopupMenu(view.context, view)
menu.inflate(R.menu.popup_source)
menu.setOnMenuItemClickListener(SourceMenuListener(item))
menu.show()
return true
}
override fun onRetryClick(error: Throwable) = Unit
override fun onEmptyActionClick() = onManageClick(requireView())
private fun onOpenManga(manga: Manga) {
val intent = DetailsActivity.newIntent(context ?: return, manga)
startActivity(intent)
}
private fun onGridModeChanged(isGrid: Boolean) {
binding.recyclerView.layoutManager = if (isGrid) {
GridLayoutManager(requireContext(), 4).also { lm ->
lm.spanSizeLookup = ExploreGridSpanSizeLookup(checkNotNull(exploreAdapter), lm)
}
} else {
LinearLayoutManager(requireContext())
}
activity?.invalidateOptionsMenu()
}
private inner class SourceMenuListener(
private val sourceItem: ExploreItem.Source,
) : PopupMenu.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings -> {
startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), sourceItem.source))
}
R.id.action_hide -> {
viewModel.hideSource(sourceItem.source)
}
else -> return false
}
return true
}
}
companion object {
fun newInstance() = ExploreFragment()
}
}

View File

@@ -1,104 +0,0 @@
package org.koitharu.kotatsu.explore.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import javax.inject.Inject
@HiltViewModel
class ExploreViewModel @Inject constructor(
private val settings: AppSettings,
private val exploreRepository: ExploreRepository,
) : BaseViewModel() {
private val gridMode = settings.observeAsStateFlow(
key = AppSettings.KEY_SOURCES_GRID,
scope = viewModelScope + Dispatchers.IO,
valueProducer = { isSourcesGridMode },
)
val onOpenManga = SingleLiveEvent<Manga>()
val onActionDone = SingleLiveEvent<ReversibleAction>()
val isGrid = gridMode.asFlowLiveData(viewModelScope.coroutineContext)
val content: LiveData<List<ExploreItem>> = isLoading.asFlow().flatMapLatest { loading ->
if (loading) {
flowOf(listOf(ExploreItem.Loading))
} else {
createContentFlow()
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(ExploreItem.Loading))
fun openRandom() {
launchLoadingJob(Dispatchers.Default) {
val manga = exploreRepository.findRandomManga(tagsLimit = 8)
onOpenManga.postCall(manga)
}
}
fun hideSource(source: MangaSource) {
launchJob(Dispatchers.Default) {
settings.hiddenSources += source.name
val rollback = ReversibleHandle {
settings.hiddenSources -= source.name
}
onActionDone.postCall(ReversibleAction(R.string.source_disabled, rollback))
}
}
fun setGridMode(value: Boolean) {
settings.isSourcesGridMode = value
}
private fun createContentFlow() = settings.observe()
.filter {
it == AppSettings.KEY_SOURCES_HIDDEN ||
it == AppSettings.KEY_SOURCES_ORDER ||
it == AppSettings.KEY_SUGGESTIONS
}
.onStart { emit("") }
.map { settings.getMangaSources(includeHidden = false) }
.distinctUntilChanged()
.combine(gridMode) { content, grid -> buildList(content, grid) }
private fun buildList(sources: List<MangaSource>, isGrid: Boolean): List<ExploreItem> {
val result = ArrayList<ExploreItem>(sources.size + 3)
result += ExploreItem.Buttons(
isSuggestionsEnabled = settings.isSuggestionsEnabled,
)
result += ExploreItem.Header(R.string.remote_sources, sources.isNotEmpty())
if (sources.isNotEmpty()) {
sources.mapTo(result) { ExploreItem.Source(it, isGrid) }
} else {
result += ExploreItem.EmptyHint(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.no_manga_sources,
textSecondary = R.string.no_manga_sources_text,
actionStringRes = R.string.manage,
)
}
return result
}
}

View File

@@ -1,35 +0,0 @@
package org.koitharu.kotatsu.explore.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
class ExploreAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: ExploreListEventListener,
clickListener: OnListItemClickListener<ExploreItem.Source>,
) : AsyncListDifferDelegationAdapter<ExploreItem>(ExploreDiffCallback()) {
init {
delegatesManager
.addDelegate(ITEM_TYPE_BUTTONS, exploreButtonsAD(listener))
.addDelegate(ITEM_TYPE_HEADER, exploreSourcesHeaderAD(listener))
.addDelegate(ITEM_TYPE_SOURCE_LIST, exploreSourceListItemAD(coil, clickListener, lifecycleOwner))
.addDelegate(ITEM_TYPE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
.addDelegate(ITEM_TYPE_HINT, exploreEmptyHintListAD(listener))
.addDelegate(ITEM_TYPE_LOADING, exploreLoadingAD())
}
companion object {
const val ITEM_TYPE_BUTTONS = 0
const val ITEM_TYPE_HEADER = 1
const val ITEM_TYPE_SOURCE_LIST = 2
const val ITEM_TYPE_SOURCE_GRID = 3
const val ITEM_TYPE_HINT = 4
const val ITEM_TYPE_LOADING = 5
}
}

View File

@@ -1,141 +0,0 @@
package org.koitharu.kotatsu.explore.ui.adapter
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceListBinding
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
import org.koitharu.kotatsu.utils.ext.source
import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable
fun exploreButtonsAD(
clickListener: View.OnClickListener,
) = adapterDelegateViewBinding<ExploreItem.Buttons, ExploreItem, ItemExploreButtonsBinding>(
{ layoutInflater, parent -> ItemExploreButtonsBinding.inflate(layoutInflater, parent, false) },
) {
binding.buttonBookmarks.setOnClickListener(clickListener)
binding.buttonHistory.setOnClickListener(clickListener)
binding.buttonLocal.setOnClickListener(clickListener)
binding.buttonSuggestions.setOnClickListener(clickListener)
binding.buttonFavourites.setOnClickListener(clickListener)
binding.buttonRandom.setOnClickListener(clickListener)
bind {
binding.buttonSuggestions.isVisible = item.isSuggestionsEnabled
}
}
fun exploreSourcesHeaderAD(
listener: ExploreListEventListener,
) = adapterDelegateViewBinding<ExploreItem.Header, ExploreItem, ItemHeaderButtonBinding>(
{ layoutInflater, parent -> ItemHeaderButtonBinding.inflate(layoutInflater, parent, false) },
) {
val listenerAdapter = View.OnClickListener {
listener.onManageClick(itemView)
}
binding.buttonMore.setOnClickListener(listenerAdapter)
bind {
binding.textViewTitle.setText(item.titleResId)
binding.buttonMore.isVisible = item.isButtonVisible
}
}
fun exploreSourceListItemAD(
coil: ImageLoader,
listener: OnListItemClickListener<ExploreItem.Source>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceListBinding>(
{ layoutInflater, parent -> ItemExploreSourceListBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is ExploreItem.Source && !item.isGrid },
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener)
bind {
binding.textViewTitle.text = item.source.title
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
placeholder(fallbackIcon)
error(fallbackIcon)
source(item.source)
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
}
fun exploreSourceGridItemAD(
coil: ImageLoader,
listener: OnListItemClickListener<ExploreItem.Source>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceGridBinding>(
{ layoutInflater, parent -> ItemExploreSourceGridBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is ExploreItem.Source && item.isGrid },
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
binding.root.setOnClickListener(eventListener)
binding.root.setOnLongClickListener(eventListener)
bind {
binding.textViewTitle.text = item.source.title
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
placeholder(fallbackIcon)
error(fallbackIcon)
source(item.source)
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
}
fun exploreEmptyHintListAD(
listener: ListStateHolderListener,
) = adapterDelegateViewBinding<ExploreItem.EmptyHint, ExploreItem, ItemEmptyCardBinding>(
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
) {
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
bind {
binding.icon.setImageResource(item.icon)
binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
}
}
fun exploreLoadingAD() = adapterDelegate<ExploreItem.Loading, ExploreItem>(R.layout.item_loading_state) {}

View File

@@ -1,29 +0,0 @@
package org.koitharu.kotatsu.explore.ui.adapter
import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
class ExploreDiffCallback : DiffUtil.ItemCallback<ExploreItem>() {
override fun areItemsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
return when {
oldItem.javaClass != newItem.javaClass -> false
oldItem is ExploreItem.Buttons && newItem is ExploreItem.Buttons -> true
oldItem is ExploreItem.Loading && newItem is ExploreItem.Loading -> true
oldItem is ExploreItem.EmptyHint && newItem is ExploreItem.EmptyHint -> true
oldItem is ExploreItem.Source && newItem is ExploreItem.Source -> {
oldItem.source == newItem.source && oldItem.isGrid == newItem.isGrid
}
oldItem is ExploreItem.Header && newItem is ExploreItem.Header -> {
oldItem.titleResId == newItem.titleResId
}
else -> false
}
}
override fun areContentsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
return oldItem == newItem
}
}

View File

@@ -1,91 +0,0 @@
package org.koitharu.kotatsu.explore.ui.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface ExploreItem : ListModel {
class Buttons(
val isSuggestionsEnabled: Boolean
) : ExploreItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Buttons
if (isSuggestionsEnabled != other.isSuggestionsEnabled) return false
return true
}
override fun hashCode(): Int {
return isSuggestionsEnabled.hashCode()
}
}
class Header(
@StringRes val titleResId: Int,
val isButtonVisible: Boolean,
) : ExploreItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Header
if (titleResId != other.titleResId) return false
if (isButtonVisible != other.isButtonVisible) return false
return true
}
override fun hashCode(): Int {
var result = titleResId
result = 31 * result + isButtonVisible.hashCode()
return result
}
}
class Source(
val source: MangaSource,
val isGrid: Boolean,
) : ExploreItem {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Source
if (source != other.source) return false
if (isGrid != other.isGrid) return false
return true
}
override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + isGrid.hashCode()
return result
}
}
@Deprecated("")
class EmptyHint(
@DrawableRes icon: Int,
@StringRes textPrimary: Int,
@StringRes textSecondary: Int,
@StringRes actionStringRes: Int,
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
object Loading : ExploreItem {
override fun equals(other: Any?): Boolean = other === Loading
}
}

View File

@@ -1,16 +0,0 @@
package org.koitharu.kotatsu.favourites.data
import org.koitharu.kotatsu.core.db.entity.SortOrder
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.*
fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong()) = FavouriteCategory(
id = id,
title = title,
sortKey = sortKey,
order = SortOrder(order, SortOrder.NEWEST),
createdAt = Date(createdAt),
isTrackingEnabled = track,
isVisibleInLibrary = isVisibleInLibrary,
)

View File

@@ -1,104 +0,0 @@
package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.requireValue
import java.util.Collections
import javax.inject.Inject
@HiltViewModel
class FavouritesCategoriesViewModel @Inject constructor(
private val repository: FavouritesRepository,
private val settings: AppSettings,
) : BaseViewModel() {
private var reorderJob: Job? = null
private val isReorder = MutableStateFlow(false)
val isInReorderMode = isReorder.asLiveData(viewModelScope.coroutineContext)
val allCategories = repository.observeCategories()
.mapItems {
CategoryListModel(
mangaCount = 0,
covers = listOf(),
category = it,
isReorderMode = false,
)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val detalizedCategories = combine(
repository.observeCategoriesWithCovers(),
isReorder,
) { list, reordering ->
list.map { (category, covers) ->
CategoryListModel(
mangaCount = covers.size,
covers = covers.take(3),
category = category,
isReorderMode = reordering,
)
}.ifEmpty {
listOf(
EmptyState(
icon = R.drawable.ic_empty_favourites,
textPrimary = R.string.text_empty_holder_primary,
textSecondary = R.string.empty_favourite_categories,
actionStringRes = 0,
),
)
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
fun deleteCategory(id: Long) {
launchJob {
repository.removeCategory(id)
}
}
fun deleteCategories(ids: Set<Long>) {
launchJob {
repository.removeCategories(ids)
}
}
fun setAllCategoriesVisible(isVisible: Boolean) {
settings.isAllFavouritesVisible = isVisible
}
fun isInReorderMode(): Boolean = isReorder.value
fun isEmpty(): Boolean = detalizedCategories.value?.none { it is CategoryListModel } ?: true
fun setReorderMode(isReorderMode: Boolean) {
isReorder.value = isReorderMode
}
fun reorderCategories(oldPos: Int, newPos: Int) {
val prevJob = reorderJob
reorderJob = launchJob(Dispatchers.Default) {
prevJob?.join()
val items = detalizedCategories.requireValue()
val ids = items.mapNotNullTo(ArrayList(items.size)) {
(it as? CategoryListModel)?.category?.id
}
Collections.swap(ids, oldPos, newPos)
ids.remove(0L)
repository.reorderCategories(ids)
}
}
}

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