Compare commits

...

423 Commits
v5.2 ... v6.2.1

Author SHA1 Message Date
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
803 changed files with 24335 additions and 14963 deletions

View File

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

View File

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

1
.idea/.gitignore generated vendored
View File

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

2
.idea/gradle.xml generated
View File

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

11
CONTRIBUTING.md Normal file
View File

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

View File

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

View File

@@ -2,28 +2,29 @@ plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'com.google.devtools.ksp'
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
}
android {
compileSdk = 33
buildToolsVersion = '33.0.2'
compileSdk = 34
buildToolsVersion = '34.0.0'
namespace = 'org.koitharu.kotatsu'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 552
versionName '5.2'
minSdk = 21
targetSdk = 34
versionCode = 588
versionName = '6.2.1'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
arguments {
arg 'room.schemaLocation', "$projectDir/schemas".toString()
}
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
androidResources {
generateLocaleConfig true
}
}
buildTypes {
@@ -39,6 +40,7 @@ android {
}
buildFeatures {
viewBinding true
buildConfig true
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
@@ -79,80 +81,80 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:f732582d55') {
implementation('com.github.KotatsuApp:kotatsu-parsers:0054d06e6e') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.10'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.activity:activity-ktx:1.7.2'
implementation 'androidx.fragment:fragment-ktx:1.6.0'
implementation 'androidx.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.recyclerview:recyclerview:1.3.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.9.0'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2'
// TODO https://issuetracker.google.com/issues/254846063
implementation 'androidx.work:work-runtime-ktx:2.8.1'
//noinspection GradleDependency
implementation('com.google.guava:guava:32.0.0-android') {
implementation('com.google.guava:guava:32.0.1-android') {
exclude group: 'com.google.guava', module: 'failureaccess'
exclude group: 'org.checkerframework', module: 'checker-qual'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
}
implementation 'androidx.room:room-runtime:2.5.1'
implementation 'androidx.room:room-ktx:2.5.1'
kapt 'androidx.room:room-compiler:2.5.1'
implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.room:room-ktx:2.5.2'
ksp 'androidx.room:room-compiler:2.5.2'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
implementation 'com.squareup.okio:okio:3.3.0'
implementation 'com.squareup.okio:okio:3.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.46.1'
kapt 'com.google.dagger:hilt-compiler:2.46.1'
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.4.0'
implementation 'io.coil-kt:coil-svg:2.4.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view: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.2'
implementation 'ch.acra:acra-dialog:5.11.2'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20230227'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
testImplementation 'org.json:json:20230618'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
androidTestImplementation 'androidx.room:room-testing:2.5.1'
androidTestImplementation 'androidx.room:room-testing:2.5.2'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.48.1'
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
@@ -28,20 +27,16 @@ fun bookmarkListAD(
binding.root.setOnLongClickListener(listener)
bind {
val data: Any = item.directImageUrl ?: item.toMangaPage()
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
size(CoverSizeResolver(binding.imageViewThumb))
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
tag(item)
decodeRegion(item.scroll)
source(item.manga.source)
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewThumb.disposeImageRequest()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,14 +8,15 @@ import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.webkit.CookieManager
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled")
@@ -34,8 +35,9 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
with(viewBinding.webView.settings) {
javaScriptEnabled = true
userAgentString = CommonHeadersInterceptor.userAgentChrome
userAgentString = UserAgents.CHROME_MOBILE
}
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)

View File

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

View File

@@ -3,27 +3,33 @@ package org.koitharu.kotatsu.browser.cloudflare
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.yield
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import javax.inject.Inject
import com.google.android.material.R as materialR
@@ -39,7 +45,13 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
if (!catchingWebViewUnavailability {
setContentView(
ActivityBrowserBinding.inflate(
layoutInflater
)
)
}) {
return
}
supportActionBar?.run {
@@ -49,10 +61,9 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
val url = intent?.dataString.orEmpty()
with(viewBinding.webView.settings) {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
domStorageEnabled = true
databaseEnabled = true
userAgentString = intent?.getStringExtra(ARG_UA) ?: CommonHeadersInterceptor.userAgentFallback
userAgentString = intent?.getStringExtra(ARG_UA) ?: UserAgents.CHROME_MOBILE
}
viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
@@ -88,6 +99,11 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
viewBinding.webView.restoreState(savedInstanceState)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_captcha, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.appbar.updatePadding(
top = insets.top,
@@ -106,6 +122,19 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
true
}
R.id.action_retry -> {
lifecycleScope.launch {
viewBinding.webView.stopLoading()
yield()
val targetUrl = intent?.dataString?.toHttpUrlOrNull()
if (targetUrl != null) {
clearCfCookies(targetUrl)
viewBinding.webView.loadUrl(targetUrl.toString())
}
}
true
}
else -> super.onOptionsItemSelected(item)
}
@@ -143,7 +172,15 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
setTitle(title)
supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
supportActionBar?.subtitle =
subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
}
private suspend fun clearCfCookies(url: HttpUrl) = runInterruptible(Dispatchers.Default) {
cookieJar.removeCookies(url) { cookie ->
val name = cookie.name
name.startsWith("cf_") || name.startsWith("_cf") || name.startsWith("__cf")
}
}
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() {

View File

@@ -6,6 +6,7 @@ import android.provider.SearchRecentSuggestions
import android.text.Html
import androidx.collection.arraySetOf
import androidx.room.InvalidationTracker
import androidx.work.WorkManager
import coil.ComponentRegistry
import coil.ImageLoader
import coil.decode.SvgDecoder
@@ -24,11 +25,13 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.StubContentCache
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.*
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
@@ -38,13 +41,13 @@ import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.AcraScreenLogger
import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
import org.koitharu.kotatsu.core.util.ext.activityManager
import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
@@ -88,6 +91,7 @@ interface AppModule {
mangaRepositoryFactory: MangaRepository.Factory,
imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory,
coverRestoreInterceptor: CoverRestoreInterceptor,
): ImageLoader {
val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir
@@ -104,6 +108,7 @@ interface AppModule {
.diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(context.isLowRamDevice())
.eventListener(CaptchaNotifier(context))
.components(
ComponentRegistry.Builder()
.add(SvgDecoder.Factory())
@@ -111,6 +116,7 @@ interface AppModule {
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
.add(pageFetcherFactory)
.add(imageProxyInterceptor)
.add(coverRestoreInterceptor)
.build(),
).build()
}
@@ -155,7 +161,7 @@ interface AppModule {
fun provideContentCache(
application: Application,
): ContentCache {
return if (application.activityManager?.isLowRamDevice == true) {
return if (application.isLowRamDevice()) {
StubContentCache()
} else {
MemoryContentCache(application)
@@ -172,5 +178,10 @@ interface AppModule {
fun provideLocalStorageChangesFlow(
@LocalStorageChanges flow: MutableSharedFlow<LocalManga?>,
): SharedFlow<LocalManga?> = flow.asSharedFlow()
@Provides
fun provideWorkManager(
@ApplicationContext context: Context,
): WorkManager = WorkManager.getInstance(context)
}
}

View File

@@ -1,35 +1,36 @@
package org.koitharu.kotatsu
package org.koitharu.kotatsu.core
import android.app.Application
import android.content.Context
import android.os.StrictMode
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode
import androidx.hilt.work.HiltWorkerFactory
import androidx.room.InvalidationTracker
import androidx.work.Configuration
import androidx.work.WorkManager
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.acra.ACRA
import org.acra.ReportField
import org.acra.config.dialog
import org.acra.config.httpSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.os.AppValidator
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
import javax.inject.Inject
import javax.inject.Provider
@HiltAndroidApp
class KotatsuApp : Application(), Configuration.Provider {
open class BaseApp : Application(), Configuration.Provider {
@Inject
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer>
@@ -46,18 +47,26 @@ class KotatsuApp : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
@Inject
lateinit var appValidator: AppValidator
@Inject
lateinit var workScheduleManager: WorkScheduleManager
@Inject
lateinit var workManagerProvider: Provider<WorkManager>
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
enableStrictMode()
}
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString())
AppCompatDelegate.setDefaultNightMode(settings.theme)
AppCompatDelegate.setApplicationLocales(settings.appLocales)
setupActivityLifecycleCallbacks()
processLifecycleScope.launch(Dispatchers.Default) {
setupDatabaseObservers()
}
WorkServiceStopHelper(applicationContext).setup()
workScheduleManager.init()
WorkServiceStopHelper(workManagerProvider).setup()
}
override fun attachBaseContext(base: Context?) {
@@ -90,6 +99,7 @@ class KotatsuApp : Application(), Configuration.Provider {
ReportField.CUSTOM_DATA,
ReportField.SHARED_PREFERENCES,
)
dialog {
text = getString(R.string.crash_text)
title = getString(R.string.error_occurred)
@@ -119,31 +129,4 @@ class KotatsuApp : Application(), Configuration.Provider {
registerActivityLifecycleCallbacks(it)
}
}
private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.setClassInstanceLimit(PageLoader::class.java, 1)
.penaltyLog()
.build(),
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
.detectWrongFragmentContainer()
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.detectFragmentTagUsage()
.build()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,10 @@ interface ContentCache {
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>?
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
data class Key(
val source: MangaSource,
val url: String,

View File

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

View File

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

View File

@@ -6,17 +6,20 @@ import androidx.room.InvalidationTracker
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.bookmarks.data.BookmarksDao
import org.koitharu.kotatsu.core.db.dao.MangaDao
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.dao.PreferencesDao
import org.koitharu.kotatsu.core.db.dao.TagsDao
import org.koitharu.kotatsu.core.db.dao.TrackLogsDao
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.db.migrations.Migration10To11
@@ -25,6 +28,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration12To13
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
import org.koitharu.kotatsu.core.db.migrations.Migration16To17
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
@@ -49,14 +53,14 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 16
const val DATABASE_VERSION = 17
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
ScrobblingEntity::class,
ScrobblingEntity::class, MangaSourceEntity::class,
],
version = DATABASE_VERSION,
)
@@ -83,37 +87,39 @@ abstract class MangaDatabase : RoomDatabase() {
abstract val bookmarksDao: BookmarksDao
abstract val scrobblingDao: ScrobblingDao
abstract val sourcesDao: MangaSourcesDao
}
val databaseMigrations: Array<Migration>
get() = arrayOf(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7(),
Migration7To8(),
Migration8To9(),
Migration9To10(),
Migration10To11(),
Migration11To12(),
Migration12To13(),
Migration13To14(),
Migration14To15(),
Migration15To16(),
)
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7(),
Migration7To8(),
Migration8To9(),
Migration9To10(),
Migration10To11(),
Migration11To12(),
Migration12To13(),
Migration13To14(),
Migration14To15(),
Migration15To16(),
Migration16To17(context),
)
fun MangaDatabase(context: Context): MangaDatabase = Room
.databaseBuilder(context, MangaDatabase::class.java, "kotatsu-db")
.addMigrations(*databaseMigrations)
.addMigrations(*getDatabaseMigrations(context))
.addCallback(DatabasePrePopulateCallback(context.resources))
.build()
fun InvalidationTracker.removeObserverAsync(observer: InvalidationTracker.Observer) {
val scope = processLifecycleScope
if (scope.isActive) {
processLifecycleScope.launch(Dispatchers.Default) {
processLifecycleScope.launch(Dispatchers.Default, CoroutineStart.ATOMIC) {
removeObserver(observer)
}
}

View File

@@ -6,3 +6,4 @@ const val TABLE_TAGS = "tags"
const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
const val TABLE_HISTORY = "history"
const val TABLE_MANGA_TAGS = "manga_tags"
const val TABLE_SOURCES = "sources"

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
@@ -12,6 +14,7 @@ abstract class TagsDao {
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE manga_tags.manga_id IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit""",
@@ -21,7 +24,7 @@ abstract class TagsDao {
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source
WHERE tags.source = :source
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit""",
@@ -31,7 +34,7 @@ abstract class TagsDao {
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source AND title LIKE :query
WHERE tags.source = :source AND title LIKE :query
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit""",
@@ -41,13 +44,35 @@ abstract class TagsDao {
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE title LIKE :query
WHERE title LIKE :query AND manga_tags.manga_id IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit""",
)
abstract suspend fun findTags(query: String, limit: Int): List<TagEntity>
@Query(
"""
SELECT tags.* FROM manga_tags
LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id
WHERE manga_tags.manga_id IN (SELECT manga_id FROM manga_tags WHERE tag_id = :tagId)
GROUP BY tags.tag_id
ORDER BY COUNT(manga_id) DESC;
""",
)
abstract suspend fun findRelatedTags(tagId: Long): List<TagEntity>
@Query(
"""
SELECT tags.* FROM manga_tags
LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id
WHERE manga_tags.manga_id IN (SELECT manga_id FROM manga_tags WHERE tag_id IN (:ids))
GROUP BY tags.tag_id
ORDER BY COUNT(manga_id) DESC;
""",
)
abstract suspend fun findRelatedTags(ids: Set<Long>): List<TagEntity>
@Upsert
abstract suspend fun upsert(tags: Iterable<TagEntity>)
}

View File

@@ -19,6 +19,8 @@ fun TagEntity.toMangaTag() = MangaTag(
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
fun Collection<TagEntity>.toMangaTagsList() = map(TagEntity::toMangaTag)
fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
id = this.id,
title = this.title,

View File

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

View File

@@ -4,7 +4,7 @@ import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
class MangaWithTags(
data class MangaWithTags(
@Embedded val manga: MangaEntity,
@Relation(
parentColumn = "manga_id",
@@ -12,23 +12,4 @@ class MangaWithTags(
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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.exceptions
import okio.IOException
import java.util.Date
class TooManyRequestExceptions(
val url: String,
val retryAt: Date?,
) : IOException() {
val retryAfter: Long
get() = if (retryAt == null) 0 else (retryAt.time - System.currentTimeMillis()).coerceAtLeast(0)
}

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,5 @@
package org.koitharu.kotatsu.core.github
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -14,28 +10,22 @@ import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.os.AppValidator
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
import org.koitharu.kotatsu.parsers.util.parseJsonArray
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Singleton
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
private const val CONTENT_TYPE_APK = "application/vnd.android.package-archive"
@Singleton
class AppUpdateRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val appValidator: AppValidator,
private val settings: AppSettings,
@BaseHttpClient private val okHttp: OkHttpClient,
) {
@@ -85,7 +75,7 @@ class AppUpdateRepository @Inject constructor(
}
fun isUpdateSupported(): Boolean {
return BuildConfig.DEBUG || getCertificateSHA1Fingerprint() == CERT_SHA1
return BuildConfig.DEBUG || appValidator.isOriginalApp
}
suspend fun getCurrentVersionChangelog(): String? {
@@ -94,22 +84,6 @@ class AppUpdateRepository @Inject constructor(
return available.find { x -> x.versionId == currentVersion }?.description
}
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
private fun getCertificateSHA1Fingerprint(): String? = runCatching {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
val signatures = requireNotNull(packageInfo?.signatures)
val cert: ByteArray = signatures.first().toByteArray()
val input: InputStream = ByteArrayInputStream(cert)
val cf = CertificateFactory.getInstance("X509")
val c = cf.generateCertificate(input) as X509Certificate
val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.encoded)
return publicKey.byte2HexFormatted()
}.onFailure { error ->
error.printStackTraceDebug()
}.getOrNull()
private inline fun JSONArray.find(predicate: (JSONObject) -> Boolean): JSONObject? {
val size = length()
for (i in 0 until size) {

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.github
import java.util.*
class VersionId(
data class VersionId(
val major: Int,
val minor: Int,
val build: Int,
@@ -30,30 +30,6 @@ class VersionId(
return variantNumber.compareTo(other.variantNumber)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as VersionId
if (major != other.major) return false
if (minor != other.minor) return false
if (build != other.build) return false
if (variantType != other.variantType) return false
if (variantNumber != other.variantNumber) return false
return true
}
override fun hashCode(): Int {
var result = major
result = 31 * result + minor
result = 31 * result + build
result = 31 * result + variantType.hashCode()
result = 31 * result + variantNumber
return result
}
private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) {
"a", "alpha" -> 1
"b", "beta" -> 2

View File

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

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.model
import android.net.Uri
import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.core.util.ext.iterator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
@@ -8,10 +9,16 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
@JvmName("mangaIds")
fun Collection<Manga>.ids() = mapToSet { it.id }
fun Collection<Manga>.distinctById() = distinctBy { it.id }
@JvmName("chaptersIds")
fun Collection<MangaChapter>.ids() = mapToSet { it.id }
fun Collection<MangaChapter>.findById(id: Long) = find { x -> x.id == id }
fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
if (size <= 1) {
return size
@@ -25,7 +32,7 @@ fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
}
fun Manga.findChapter(id: Long): MangaChapter? {
return chapters?.find { it.id == id }
return chapters?.findById(id)
}
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
@@ -34,7 +41,7 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
return null
}
if (history != null) {
val currentChapter = ch.find { it.id == history.chapterId }
val currentChapter = ch.findById(history.chapterId)
if (currentChapter != null) {
return currentChapter.branch
}
@@ -43,10 +50,10 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
if (groups.size == 1) {
return groups.keys.first()
}
val candidates = HashMap<String?, List<MangaChapter>>(groups.size)
for (locale in LocaleListCompat.getAdjustedDefault()) {
val displayLanguage = locale.getDisplayLanguage(locale)
val displayName = locale.getDisplayName(locale)
val candidates = HashMap<String?, List<MangaChapter>>(3)
for (branch in groups.keys) {
if (branch != null && (
branch.contains(displayLanguage, ignoreCase = true) ||
@@ -56,9 +63,19 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
candidates[branch] = groups[branch] ?: continue
}
}
if (candidates.isNotEmpty()) {
return candidates.maxBy { it.value.size }.key
}
}
return candidates.ifEmpty { groups }.maxByOrNull { it.value.size }?.key
return groups.maxByOrNull { it.value.size }?.key
}
val Manga.isLocal: Boolean
get() = source == MangaSource.LOCAL
val Manga.appUrl: Uri
get() = Uri.parse("https://kotatsu.app/manga").buildUpon()
.appendQueryParameter("source", source.name)
.appendQueryParameter("name", title)
.appendQueryParameter("url", url)
.build()

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.model
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.util.Locale
@@ -10,8 +11,10 @@ fun MangaSource.getLocaleTitle(): String? {
}
fun MangaSource(name: String): MangaSource {
MangaSource.values().forEach {
MangaSource.entries.forEach {
if (it.name == name) return it
}
return MangaSource.DUMMY
}
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI

View File

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

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
@Parcelize
data class ParcelableChapter(
val chapter: MangaChapter,
) : Parcelable {
companion object : Parceler<ParcelableChapter> {
override fun create(parcel: Parcel) = ParcelableChapter(
MangaChapter(
id = parcel.readLong(),
name = parcel.readString().orEmpty(),
number = parcel.readInt(),
url = parcel.readString().orEmpty(),
scanlator = parcel.readString(),
uploadDate = parcel.readLong(),
branch = parcel.readString(),
source = parcel.readSerializableCompat() ?: MangaSource.DUMMY,
)
)
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
parcel.writeLong(id)
parcel.writeString(name)
parcel.writeInt(number)
parcel.writeString(url)
parcel.writeString(scanlator)
parcel.writeLong(uploadDate)
parcel.writeString(branch)
parcel.writeSerializable(source)
}
}
}

View File

@@ -2,52 +2,55 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import androidx.core.os.ParcelCompat
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.parsers.model.Manga
// Limits to avoid TransactionTooLargeException
private const val MAX_SAFE_SIZE = 1024 * 100 // Assume that 100 kb is safe parcel size
private const val MAX_SAFE_CHAPTERS_COUNT = 24 // this is 100% safe
class ParcelableManga(
@Parcelize
data class ParcelableManga(
val manga: Manga,
private val withChapters: Boolean,
) : Parcelable {
constructor(parcel: Parcel) : this(parcel.readManga(), true)
companion object : Parceler<ParcelableManga> {
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 ParcelableManga.write(parcel: Parcel, flags: Int) = with(manga) {
parcel.writeLong(id)
parcel.writeString(title)
parcel.writeString(altTitle)
parcel.writeString(url)
parcel.writeString(publicUrl)
parcel.writeFloat(rating)
ParcelCompat.writeBoolean(parcel, isNsfw)
parcel.writeString(coverUrl)
parcel.writeString(largeCoverUrl)
parcel.writeString(description)
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
parcel.writeSerializable(state)
parcel.writeString(author)
parcel.writeSerializable(source)
}
override fun newArray(size: Int): Array<ParcelableManga?> {
return arrayOfNulls(size)
}
override fun create(parcel: Parcel) = ParcelableManga(
Manga(
id = parcel.readLong(),
title = requireNotNull(parcel.readString()),
altTitle = parcel.readString(),
url = requireNotNull(parcel.readString()),
publicUrl = requireNotNull(parcel.readString()),
rating = parcel.readFloat(),
isNsfw = ParcelCompat.readBoolean(parcel),
coverUrl = requireNotNull(parcel.readString()),
largeCoverUrl = parcel.readString(),
description = parcel.readString(),
tags = requireNotNull(parcel.readParcelableCompat<ParcelableMangaTags>()).tags,
state = parcel.readSerializableCompat(),
author = parcel.readString(),
chapters = null,
source = requireNotNull(parcel.readSerializableCompat()),
)
)
}
}

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.network
import okhttp3.Interceptor
import okhttp3.Response
import okio.IOException
import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING
class GZipInterceptor : Interceptor {
@@ -9,6 +10,10 @@ class GZipInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val newRequest = chain.request().newBuilder()
newRequest.addHeader(CONTENT_ENCODING, "gzip")
return chain.proceed(newRequest.build())
return try {
chain.proceed(newRequest.build())
} catch (e: NullPointerException) {
throw IOException(e)
}
}
}

View File

@@ -1,6 +1,9 @@
package org.koitharu.kotatsu.core.network
import androidx.collection.ArraySet
import dagger.Lazy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
@@ -13,6 +16,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.EnumMap
import javax.inject.Inject
import javax.inject.Singleton
@@ -22,9 +26,15 @@ class MirrorSwitchInterceptor @Inject constructor(
private val settings: AppSettings,
) : Interceptor {
private val locks = EnumMap<MangaSource, Any>(MangaSource::class.java)
private val blacklist = EnumMap<MangaSource, MutableSet<String>>(MangaSource::class.java)
val isEnabled: Boolean
get() = settings.isMirrorSwitchingAvailable
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (!settings.isMirrorSwitchingAvailable) {
if (!isEnabled) {
return chain.proceed(request)
}
return try {
@@ -43,6 +53,30 @@ class MirrorSwitchInterceptor @Inject constructor(
}
}
suspend fun trySwitchMirror(repository: RemoteMangaRepository): Boolean = runInterruptible(Dispatchers.Default) {
if (!isEnabled) {
return@runInterruptible false
}
val mirrors = repository.getAvailableMirrors()
if (mirrors.size <= 1) {
return@runInterruptible false
}
synchronized(obtainLock(repository.source)) {
val currentMirror = repository.domain
addToBlacklist(repository.source, currentMirror)
val newMirror = mirrors.firstOrNull { x ->
x != currentMirror && !isBlacklisted(repository.source, x)
} ?: return@synchronized false
repository.domain = newMirror
true
}
}
fun rollback(repository: RemoteMangaRepository, oldMirror: String) = synchronized(obtainLock(repository.source)) {
blacklist[repository.source]?.remove(oldMirror)
repository.domain = oldMirror
}
private fun trySwitchMirror(request: Request, chain: Interceptor.Chain): Response? {
val source = request.tag(MangaSource::class.java) ?: return null
val repository = mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository ?: return null
@@ -50,7 +84,9 @@ class MirrorSwitchInterceptor @Inject constructor(
if (mirrors.isEmpty()) {
return null
}
return tryMirrors(repository, mirrors, chain, request)
return synchronized(obtainLock(repository.source)) {
tryMirrors(repository, mirrors, chain, request)
}
}
private fun tryMirrors(
@@ -66,7 +102,7 @@ class MirrorSwitchInterceptor @Inject constructor(
}
val urlBuilder = url.newBuilder()
for (mirror in mirrors) {
if (mirror == currentDomain) {
if (mirror == currentDomain || isBlacklisted(repository.source, mirror)) {
continue
}
val newHost = hostOf(url.host, mirror) ?: continue
@@ -75,6 +111,7 @@ class MirrorSwitchInterceptor @Inject constructor(
.build()
val response = chain.proceed(newRequest)
if (response.isFailed) {
addToBlacklist(repository.source, mirror)
response.closeQuietly()
} else {
repository.domain = mirror
@@ -104,4 +141,18 @@ class MirrorSwitchInterceptor @Inject constructor(
private fun ResponseBody.copy(): ResponseBody {
return source().readByteArray().toResponseBody(contentType())
}
private fun obtainLock(source: MangaSource): Any = locks.getOrPut(source) {
Any()
}
private fun isBlacklisted(source: MangaSource, domain: String): Boolean {
return blacklist[source]?.contains(domain) == true
}
private fun addToBlacklist(source: MangaSource, domain: String) {
blacklist.getOrPut(source) {
ArraySet(2)
}.add(domain)
}
}

View File

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

View File

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

View File

@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.core.network.cookies
import android.webkit.CookieManager
import androidx.annotation.WorkerThread
import androidx.core.util.Predicate
import okhttp3.Cookie
import okhttp3.HttpUrl
import org.koitharu.kotatsu.core.util.ext.newBuilder
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -30,6 +32,23 @@ class AndroidCookieJar : MutableCookieJar {
}
}
override fun removeCookies(url: HttpUrl, predicate: Predicate<Cookie>?) {
val cookies = loadForRequest(url)
if (cookies.isEmpty()) {
return
}
val urlString = url.toString()
for (c in cookies) {
if (predicate != null && !predicate.test(c)) {
continue
}
val nc = c.newBuilder()
.expiresAt(System.currentTimeMillis() - 100000)
.build()
cookieManager.setCookie(urlString, nc.toString())
}
}
override suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
cookieManager.removeAllCookies(continuation::resume)
}

View File

@@ -8,7 +8,7 @@ import java.io.ObjectInputStream
import java.io.ObjectOutputStream
class CookieWrapper(
data class CookieWrapper(
val cookie: Cookie,
) {
@@ -66,19 +66,4 @@ class CookieWrapper(
fun key(): String {
return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CookieWrapper
if (cookie != other.cookie) return false
return true
}
override fun hashCode(): Int {
return cookie.hashCode()
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.network.cookies
import androidx.annotation.WorkerThread
import androidx.core.util.Predicate
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
@@ -13,5 +14,8 @@ interface MutableCookieJar : CookieJar {
@WorkerThread
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
@WorkerThread
fun removeCookies(url: HttpUrl, predicate: Predicate<Cookie>?)
suspend fun clear(): Boolean
}

View File

@@ -4,6 +4,7 @@ import android.content.Context
import androidx.annotation.WorkerThread
import androidx.collection.ArrayMap
import androidx.core.content.edit
import androidx.core.util.Predicate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Cookie
@@ -21,6 +22,7 @@ class PreferencesCookieJar(
private var isLoaded = false
@WorkerThread
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> {
loadPersistent()
val expired = HashSet<String>()
@@ -40,6 +42,7 @@ class PreferencesCookieJar(
}
@WorkerThread
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val wrapped = cookies.map { CookieWrapper(it) }
prefs.edit(commit = true) {
@@ -53,6 +56,24 @@ class PreferencesCookieJar(
}
}
@Synchronized
@WorkerThread
override fun removeCookies(url: HttpUrl, predicate: Predicate<Cookie>?) {
loadPersistent()
val toRemove = HashSet<String>()
for ((key, cookie) in cache) {
if (cookie.isExpired() || cookie.cookie.matches(url)) {
if (predicate == null || predicate.test(cookie.cookie)) {
toRemove += key
}
}
}
if (toRemove.isNotEmpty()) {
cache.removeAll(toRemove)
removePersistent(toRemove)
}
}
override suspend fun clear(): Boolean {
cache.clear()
withContext(Dispatchers.IO) {

View File

@@ -1,12 +1,9 @@
package org.koitharu.kotatsu.core.os
import android.app.ActivityManager
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ShortcutManager
import android.os.Build
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
@@ -16,6 +13,7 @@ import androidx.room.InvalidationTracker
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import coil.size.Size
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -23,15 +21,19 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.image.ThumbnailTransformation
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity
import javax.inject.Inject
import javax.inject.Singleton
@@ -44,7 +46,9 @@ class AppShortcutManager @Inject constructor(
private val settings: AppSettings,
) : InvalidationTracker.Observer(TABLE_HISTORY), SharedPreferences.OnSharedPreferenceChangeListener {
private val iconSize by lazy { getIconSize(context) }
private val iconSize by lazy {
Size(ShortcutManagerCompat.getIconMaxWidth(context), ShortcutManagerCompat.getIconMaxHeight(context))
}
private var shortcutsUpdateJob: Job? = null
init {
@@ -52,7 +56,7 @@ class AppShortcutManager @Inject constructor(
}
override fun onInvalidated(tables: Set<String>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || !settings.isDynamicShortcutsEnabled) {
if (!settings.isDynamicShortcutsEnabled) {
return
}
val prevJob = shortcutsUpdateJob
@@ -63,7 +67,7 @@ class AppShortcutManager @Inject constructor(
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && key == AppSettings.KEY_SHORTCUTS) {
if (key == AppSettings.KEY_SHORTCUTS) {
if (settings.isDynamicShortcutsEnabled) {
onInvalidated(emptySet())
} else {
@@ -72,12 +76,18 @@ class AppShortcutManager @Inject constructor(
}
}
suspend fun requestPinShortcut(manga: Manga): Boolean {
return ShortcutManagerCompat.requestPinShortcut(
context,
buildShortcutInfo(manga).build(),
null,
)
suspend fun requestPinShortcut(manga: Manga): Boolean = try {
ShortcutManagerCompat.requestPinShortcut(context, buildShortcutInfo(manga), null)
} catch (e: IllegalStateException) {
e.printStackTraceDebug()
false
}
suspend fun requestPinShortcut(source: MangaSource): Boolean = try {
ShortcutManagerCompat.requestPinShortcut(context, buildShortcutInfo(source), null)
} catch (e: IllegalStateException) {
e.printStackTraceDebug()
false
}
@VisibleForTesting
@@ -85,49 +95,39 @@ class AppShortcutManager @Inject constructor(
return shortcutsUpdateJob?.join() != null
}
fun isDynamicShortcutsAvailable(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return false
}
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
return manager.maxShortcutCountPerActivity > 0
}
fun notifyMangaOpened(mangaId: Long) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return
}
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
manager.reportShortcutUsed(mangaId.toString())
ShortcutManagerCompat.reportShortcutUsed(context, mangaId.toString())
}
fun isDynamicShortcutsAvailable(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 &&
context.getSystemService(ShortcutManager::class.java).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)
val maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context).coerceAtLeast(5)
val shortcuts = historyRepository.getList(0, maxShortcuts)
.filter { x -> x.title.isNotEmpty() }
.map { buildShortcutInfo(it).build().toShortcutInfo() }
manager.dynamicShortcuts = shortcuts
.map { buildShortcutInfo(it) }
ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts)
}.onFailure {
it.printStackTraceDebug()
}
@RequiresApi(Build.VERSION_CODES.N_MR1)
private fun clearShortcuts() {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
try {
manager.removeAllDynamicShortcuts()
ShortcutManagerCompat.removeAllDynamicShortcuts(context)
} catch (_: IllegalStateException) {
}
}
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat {
val icon = runCatchingCancellable {
coil.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.size(iconSize.width, iconSize.height)
.tag(manga.source)
.size(iconSize)
.source(manga.source)
.scale(Scale.FILL)
.transformations(ThumbnailTransformation())
.build(),
@@ -141,22 +141,34 @@ class AppShortcutManager @Inject constructor(
.setShortLabel(manga.title)
.setLongLabel(manga.title)
.setIcon(icon)
.setLongLived(true)
.setIntent(
ReaderActivity.IntentBuilder(context)
.mangaId(manga.id)
.build(),
)
.build()
}
private fun getIconSize(context: Context): Size {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
(context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager).let {
Size(it.iconMaxWidth, it.iconMaxHeight)
}
} else {
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).launcherLargeIconSize.let {
Size(it, it)
}
}
private suspend fun buildShortcutInfo(source: MangaSource): ShortcutInfoCompat {
val icon = runCatchingCancellable {
coil.execute(
ImageRequest.Builder(context)
.data(source.faviconUri())
.size(iconSize)
.scale(Scale.FIT)
.build(),
).getDrawableOrThrow().toBitmap()
}.fold(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
)
return ShortcutInfoCompat.Builder(context, source.name)
.setShortLabel(source.title)
.setLongLabel(source.title)
.setIcon(icon)
.setLongLived(true)
.setIntent(MangaListActivity.newIntent(context, source))
.build()
}
}

View File

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

View File

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

View File

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

View File

@@ -43,5 +43,7 @@ class MangaIntent private constructor(
const val KEY_MANGA = "manga"
const val KEY_ID = "id"
fun of(manga: Manga) = MangaIntent(manga, manga.id, null)
}
}

View File

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

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.lang.ref.WeakReference
import java.util.*
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
@@ -50,7 +50,7 @@ class MangaLoaderContextImpl @Inject constructor(
}
override fun encodeBase64(data: ByteArray): String {
return Base64.encodeToString(data, Base64.NO_PADDING)
return Base64.encodeToString(data, Base64.NO_WRAP)
}
override fun decodeBase64(data: String): ByteArray {

View File

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

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.parser
import androidx.annotation.AnyThread
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.Manga
@@ -36,11 +37,14 @@ interface MangaRepository {
suspend fun getTags(): Set<MangaTag>
suspend fun getRelated(seed: Manga): List<Manga>
@Singleton
class Factory @Inject constructor(
private val localMangaRepository: LocalMangaRepository,
private val loaderContext: MangaLoaderContext,
private val contentCache: ContentCache,
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
) {
private val cache = EnumMap<MangaSource, WeakReference<RemoteMangaRepository>>(MangaSource::class.java)
@@ -53,7 +57,11 @@ interface MangaRepository {
cache[source]?.get()?.let { return it }
return synchronized(cache) {
cache[source]?.get()?.let { return it }
val repository = RemoteMangaRepository(MangaParser(source, loaderContext), contentCache)
val repository = RemoteMangaRepository(
parser = MangaParser(source, loaderContext),
cache = contentCache,
mirrorSwitchInterceptor = mirrorSwitchInterceptor,
)
cache[source] = WeakReference(repository)
repository
}

View File

@@ -13,11 +13,13 @@ import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.SafeDeferred
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Favicons
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
@@ -31,6 +33,7 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
class RemoteMangaRepository(
private val parser: MangaParser,
private val cache: ContentCache,
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
) : MangaRepository, Interceptor {
override val source: MangaSource
@@ -51,7 +54,10 @@ class RemoteMangaRepository(
getConfig()[parser.configKeyDomain] = value
}
val headers: Headers?
val domains: Array<out String>
get() = parser.configKeyDomain.presetValues
val headers: Headers
get() = parser.headers
override fun intercept(chain: Interceptor.Chain): Response {
@@ -63,36 +69,73 @@ class RemoteMangaRepository(
}
override suspend fun getList(offset: Int, query: String): List<Manga> {
return parser.getList(offset, query)
return mirrorSwitchInterceptor.withMirrorSwitching {
parser.getList(offset, query)
}
}
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
return parser.getList(offset, tags, sortOrder)
return mirrorSwitchInterceptor.withMirrorSwitching {
parser.getList(offset, tags, sortOrder)
}
}
override suspend fun getDetails(manga: Manga): Manga {
cache.getDetails(source, manga.url)?.let { return it }
val details = asyncSafe {
parser.getDetails(manga)
}
cache.putDetails(source, manga.url, details)
return details.await()
}
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, withCache = true)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
cache.getPages(source, chapter.url)?.let { return it }
val pages = asyncSafe {
parser.getPages(chapter).distinctById()
mirrorSwitchInterceptor.withMirrorSwitching {
parser.getPages(chapter).distinctById()
}
}
cache.putPages(source, chapter.url, pages)
return pages.await()
}
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching {
parser.getPageUrl(page)
}
override suspend fun getTags(): Set<MangaTag> = parser.getTags()
override suspend fun getTags(): Set<MangaTag> = mirrorSwitchInterceptor.withMirrorSwitching {
parser.getTags()
}
suspend fun getFavicons(): Favicons = parser.getFavicons()
suspend fun getFavicons(): Favicons = mirrorSwitchInterceptor.withMirrorSwitching {
parser.getFavicons()
}
override suspend fun getRelated(seed: Manga): List<Manga> {
cache.getRelatedManga(source, seed.url)?.let { return it }
val related = asyncSafe {
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
}
cache.putRelatedManga(source, seed.url, related)
return related.await()
}
suspend fun getDetails(manga: Manga, withCache: Boolean): Manga {
if (!withCache) {
return parser.getDetails(manga)
}
cache.getDetails(source, manga.url)?.let { return it }
val details = asyncSafe {
mirrorSwitchInterceptor.withMirrorSwitching {
parser.getDetails(manga)
}
}
cache.putDetails(source, manga.url, details)
return details.await()
}
suspend fun peekDetails(manga: Manga): Manga? {
return cache.getDetails(source, manga.url)
}
suspend fun find(manga: Manga): Manga? {
val list = getList(0, manga.title)
return list.find { x -> x.id == manga.id }
}
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
@@ -133,4 +176,33 @@ class RemoteMangaRepository(
}
return result
}
private suspend fun <R> MirrorSwitchInterceptor.withMirrorSwitching(block: suspend () -> R): R {
if (!isEnabled) {
return block()
}
val initialMirror = domain
val result = runCatchingCancellable {
block()
}
if (result.isValidResult()) {
return result.getOrThrow()
}
return if (trySwitchMirror(this@RemoteMangaRepository)) {
val newResult = runCatchingCancellable {
block()
}
if (newResult.isValidResult()) {
return newResult.getOrThrow()
} else {
rollback(this@RemoteMangaRepository, initialMirror)
return result.getOrThrow()
}
} else {
result.getOrThrow()
}
}
private fun Result<*>.isValidResult() = exceptionOrNull() !is ParseException
&& (getOrNull() as? Collection<*>)?.isEmpty() != true
}

View File

@@ -14,6 +14,7 @@ import coil.network.HttpException
import coil.request.Options
import coil.size.Size
import coil.size.pxOrElse
import kotlinx.coroutines.ensureActive
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
@@ -21,14 +22,17 @@ import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly
import okio.Closeable
import okio.buffer
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import java.net.HttpURLConnection
import kotlin.coroutines.coroutineContext
private const val FALLBACK_SIZE = 9999 // largest icon
@@ -49,22 +53,35 @@ class FaviconFetcher(
override suspend fun fetch(): FetchResult {
getCached(options)?.let { return it }
val repo = mangaRepositoryFactory.create(mangaSource) as RemoteMangaRepository
val favicons = repo.getFavicons()
val sizePx = maxOf(
options.size.width.pxOrElse { FALLBACK_SIZE },
options.size.height.pxOrElse { FALLBACK_SIZE },
)
val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" }
val response = loadIcon(icon.url, mangaSource)
val responseBody = response.requireBody()
val source = writeToDiskCache(responseBody)?.toImageSource()?.also {
response.closeQuietly()
} ?: responseBody.toImageSource(response)
return SourceResult(
source = source,
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type),
dataSource = response.toDataSource(),
)
var favicons = repo.getFavicons()
var lastError: Exception? = null
while (favicons.isNotEmpty()) {
coroutineContext.ensureActive()
val icon = favicons.find(sizePx) ?: throwNSEE(lastError)
val response = try {
loadIcon(icon.url, mangaSource)
} catch (e: CloudFlareProtectedException) {
throw e
} catch (e: HttpException) {
lastError = e
favicons -= icon
continue
}
val responseBody = response.requireBody()
val source = writeToDiskCache(responseBody)?.toImageSource()?.also {
response.closeQuietly()
} ?: responseBody.toImageSource(response)
return SourceResult(
source = source,
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type),
dataSource = response.toDataSource(),
)
}
throwNSEE(lastError)
}
private suspend fun loadIcon(url: String, source: MangaSource): Response {
@@ -94,14 +111,14 @@ class FaviconFetcher(
)
}
private fun writeToDiskCache(body: ResponseBody): DiskCache.Snapshot? {
private suspend fun writeToDiskCache(body: ResponseBody): DiskCache.Snapshot? {
if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) {
return null
}
val editor = diskCache.value?.openEditor(diskCacheKey) ?: return null
try {
fileSystem.write(editor.data) {
body.source().readAll(this)
writeAllCancellable(body.source())
}
return editor.commitAndOpenSnapshot()
} catch (e: Throwable) {
@@ -143,6 +160,14 @@ class FaviconFetcher(
append(height.toString())
}
private fun throwNSEE(lastError: Exception?): Nothing {
if (lastError != null) {
throw lastError
} else {
throw NoSuchElementException("No favicons found")
}
}
class Factory(
context: Context,
private val okHttpClient: OkHttpClient,

View File

@@ -8,28 +8,27 @@ import android.os.Build
import android.provider.Settings
import androidx.annotation.FloatRange
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arraySetOf
import androidx.collection.ArraySet
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext
import org.koitharu.kotatsu.BuildConfig
import org.json.JSONArray
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.filterToSet
import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.find
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
import java.io.File
import java.net.Proxy
import java.util.Collections
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -40,38 +39,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val connectivityManager = context.connectivityManager
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
remove(MangaSource.LOCAL)
if (!BuildConfig.DEBUG) {
remove(MangaSource.DUMMY)
}
}
val remoteMangaSources: Set<MangaSource>
get() = Collections.unmodifiableSet(remoteSources)
var shelfSections: List<ShelfSection>
get() {
val raw = prefs.getString(KEY_SHELF_SECTIONS, null)
val values = enumValues<ShelfSection>()
if (raw.isNullOrEmpty()) {
return values.toList()
}
return raw.split('|')
.mapNotNull { values.getOrNull(it.toIntOrNull() ?: -1) }
.distinct()
}
set(value) {
val raw = value.joinToString("|") { it.ordinal.toString() }
prefs.edit { putString(KEY_SHELF_SECTIONS, raw) }
}
var listMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
val theme: Int
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull()
?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
val colorScheme: ColorScheme
get() = prefs.getEnumValue(KEY_COLOR_THEME, ColorScheme.default)
@@ -79,10 +53,41 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isAmoledTheme: Boolean
get() = prefs.getBoolean(KEY_THEME_AMOLED, false)
var mainNavItems: List<NavItem>
get() {
val raw = prefs.getString(KEY_NAV_MAIN, null)?.split(',')
return if (raw.isNullOrEmpty()) {
listOf(NavItem.HISTORY, NavItem.FAVORITES, NavItem.EXPLORE, NavItem.FEED)
} else {
raw.mapNotNull { x -> NavItem.entries.find(x) }.ifEmpty { listOf(NavItem.EXPLORE) }
}
}
set(value) {
prefs.edit {
putString(KEY_NAV_MAIN, value.joinToString(",") { it.name })
}
}
var gridSize: Int
get() = prefs.getInt(KEY_GRID_SIZE, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
var historyListMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE_HISTORY, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_HISTORY, value) }
var suggestionsListMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE_SUGGESTIONS, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_SUGGESTIONS, value) }
var favoritesListMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE_FAVORITES, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_FAVORITES, value) }
var isNsfwContentDisabled: Boolean
get() = prefs.getBoolean(KEY_DISABLE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_DISABLE_NSFW, value) }
var appLocales: LocaleListCompat
get() {
val raw = prefs.getString(KEY_APP_LOCALE, null)
@@ -97,6 +102,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val readerPageSwitch: Set<String>
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
val isReaderZoomButtonsEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_ZOOM_BUTTONS, false)
val isReaderTapsAdaptive: Boolean
get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false)
@@ -111,6 +119,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isTrackerEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true)
val isTrackerWifiOnly: Boolean
get() = prefs.getBoolean(KEY_TRACKER_WIFI_ONLY, false)
val isTrackerNotificationsEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
@@ -125,8 +136,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val notificationLight: Boolean
get() = prefs.getBoolean(KEY_NOTIFICATIONS_LIGHT, true)
val readerAnimation: Boolean
get() = prefs.getBoolean(KEY_READER_ANIMATION, false)
val readerAnimation: ReaderAnimation
get() = prefs.getEnumValue(KEY_READER_ANIMATION, ReaderAnimation.DEFAULT)
val readerBackground: ReaderBackground
get() = prefs.getEnumValue(KEY_READER_BACKGROUND, ReaderBackground.DEFAULT)
val defaultReaderMode: ReaderMode
get() = prefs.getEnumValue(KEY_READER_MODE, ReaderMode.STANDARD)
@@ -156,11 +170,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_ZOOM_MODE, ZoomMode.FIT_CENTER)
val trackSources: Set<String>
get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: arraySetOf(TRACK_FAVOURITES, TRACK_HISTORY)
get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: setOf(TRACK_FAVOURITES)
var appPassword: String?
get() = prefs.getString(KEY_APP_PASSWORD, null)
set(value) = prefs.edit { if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD) }
set(value) = prefs.edit {
if (value != null) putString(KEY_APP_PASSWORD, value) else remove(
KEY_APP_PASSWORD,
)
}
val isLoggingEnabled: Boolean
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
@@ -170,7 +188,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) }
val isMirrorSwitchingAvailable: Boolean
get() = prefs.getBoolean(KEY_MIRROR_SWITCHING, true)
get() = prefs.getBoolean(KEY_MIRROR_SWITCHING, false)
val isExitConfirmationEnabled: Boolean
get() = prefs.getBoolean(KEY_EXIT_CONFIRM, false)
@@ -186,45 +204,18 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
if (isBackgroundNetworkRestricted()) {
return false
}
val policy = NetworkPolicy.from(prefs.getString(KEY_PREFETCH_CONTENT, null), NetworkPolicy.NEVER)
val policy =
NetworkPolicy.from(prefs.getString(KEY_PREFETCH_CONTENT, null), NetworkPolicy.NEVER)
return policy.isNetworkAllowed(connectivityManager)
}
var sourcesOrder: List<String>
get() = prefs.getString(KEY_SOURCES_ORDER, null)
?.split('|')
.orEmpty()
set(value) = prefs.edit {
putString(KEY_SOURCES_ORDER, value.joinToString("|"))
}
var hiddenSources: Set<String>
get() = prefs.getStringSet(KEY_SOURCES_HIDDEN, null)?.filterToSet { name ->
remoteSources.any { it.name == name }
}.orEmpty()
set(value) = prefs.edit { putStringSet(KEY_SOURCES_HIDDEN, value) }
val isSourcesSelected: Boolean
get() = KEY_SOURCES_HIDDEN in prefs
val newSources: Set<MangaSource>
get() {
val known = sourcesOrder.toSet()
val hidden = hiddenSources
return remoteMangaSources
.filterNotTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
x.name in known || x.name in hidden
}
}
fun markKnownSources(sources: Collection<MangaSource>) {
sourcesOrder = (sourcesOrder + sources.map { it.name }).distinct()
}
var isSourcesGridMode: Boolean
get() = prefs.getBoolean(KEY_SOURCES_GRID, false)
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
val isNewSourcesTipEnabled: Boolean
get() = prefs.getBoolean(KEY_SOURCES_NEW, true)
val isPagesNumbersEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
@@ -234,14 +225,28 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
}.getOrDefault(ScreenshotsPolicy.ALLOW)
var userSpecifiedMangaDirectories: Set<File>
get() {
val set = prefs.getStringSet(KEY_LOCAL_MANGA_DIRS, emptySet()).orEmpty()
return set.mapNotNullToSet { File(it).takeIfReadable() }
}
set(value) {
val set = value.mapToSet { it.absolutePath }
prefs.edit { putStringSet(KEY_LOCAL_MANGA_DIRS, set) }
}
var mangaStorageDir: File?
get() = prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
File(it)
}?.takeIf { it.exists() }
}?.takeIf { it.exists() && it in userSpecifiedMangaDirectories }
set(value) = prefs.edit {
if (value == null) {
remove(KEY_LOCAL_STORAGE)
} else {
val userDirs = userSpecifiedMangaDirectories
if (value !in userDirs) {
userSpecifiedMangaDirectories = userDirs + value
}
putString(KEY_LOCAL_STORAGE, value.path)
}
}
@@ -256,11 +261,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS, value) }
val isSuggestionsWiFiOnly: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_WIFI_ONLY, false)
val isSuggestionsExcludeNsfw: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
val isSuggestionsNotificationAvailable: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_NOTIFICATIONS, true)
get() = prefs.getBoolean(KEY_SUGGESTIONS_NOTIFICATIONS, false)
val suggestionsTagsBlacklist: Set<String>
get() {
@@ -277,6 +285,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isReaderSliderEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
val isReaderKeepScreenOn: Boolean
get() = prefs.getBoolean(KEY_READER_SCREEN_ON, true)
val isImagesProxyEnabled: Boolean
get() = prefs.getBoolean(KEY_IMAGES_PROXY, false)
@@ -308,36 +319,40 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
var historySortOrder: ListSortOrder
get() = prefs.getEnumValue(KEY_HISTORY_ORDER, ListSortOrder.UPDATED)
set(value) = prefs.edit { putEnumValue(KEY_HISTORY_ORDER, value) }
val isRelatedMangaEnabled: Boolean
get() = prefs.getBoolean(KEY_RELATED_MANGA, true)
val isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
@get:FloatRange(from = 0.0, to = 1.0)
var readerAutoscrollSpeed: Float
get() = prefs.getFloat(KEY_READER_AUTOSCROLL_SPEED, 0f)
set(@FloatRange(from = 0.0, to = 1.0) value) = prefs.edit { putFloat(KEY_READER_AUTOSCROLL_SPEED, value) }
set(@FloatRange(from = 0.0, to = 1.0) value) = prefs.edit {
putFloat(
KEY_READER_AUTOSCROLL_SPEED,
value,
)
}
val isPagesPreloadEnabled: Boolean
get() {
if (isBackgroundNetworkRestricted()) {
return false
}
val policy = NetworkPolicy.from(prefs.getString(KEY_PAGES_PRELOAD, null), NetworkPolicy.NON_METERED)
val policy = NetworkPolicy.from(
prefs.getString(KEY_PAGES_PRELOAD, null),
NetworkPolicy.NON_METERED,
)
return policy.isNetworkAllowed(connectivityManager)
}
fun getMangaSources(includeHidden: Boolean): List<MangaSource> {
val list = remoteSources.toMutableList()
val order = sourcesOrder
list.sortBy { x ->
val e = order.indexOf(x.name)
if (e == -1) order.size + x.ordinal else e
}
if (!includeHidden) {
val hidden = hiddenSources
list.removeAll { x -> x.name in hidden }
}
return list
}
val is32BitColorsEnabled: Boolean
get() = prefs.getBoolean(KEY_32BIT_COLOR, false)
fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
@@ -361,6 +376,23 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
fun observe() = prefs.observe()
fun getAllValues(): Map<String, *> = prefs.all
fun upsertAll(m: Map<String, *>) {
prefs.edit {
m.forEach { e ->
when (val v = e.value) {
is Boolean -> putBoolean(e.key, v)
is Int -> putInt(e.key, v)
is Long -> putLong(e.key, v)
is Float -> putFloat(e.key, v)
is String -> putString(e.key, v)
is JSONArray -> putStringSet(e.key, v.toStringSet())
}
}
}
}
private fun isBackgroundNetworkRestricted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
@@ -369,6 +401,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
}
private fun JSONArray.toStringSet(): Set<String> {
val len = length()
val result = ArraySet<String>(len)
for (i in 0 until len) {
result.add(getString(i))
}
return result
}
companion object {
const val PAGE_SWITCH_TAPS = "taps"
@@ -378,11 +419,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val TRACK_FAVOURITES = "favourites"
const val KEY_LIST_MODE = "list_mode_2"
const val KEY_LIST_MODE_HISTORY = "list_mode_history"
const val KEY_LIST_MODE_FAVORITES = "list_mode_favorites"
const val KEY_LIST_MODE_SUGGESTIONS = "list_mode_suggestions"
const val KEY_THEME = "theme"
const val KEY_COLOR_THEME = "color_theme"
const val KEY_THEME_AMOLED = "amoled_theme"
const val KEY_SOURCES_ORDER = "sources_order_2"
const val KEY_SOURCES_HIDDEN = "sources_hidden"
const val KEY_TRAFFIC_WARNING = "traffic_warning"
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
@@ -394,7 +436,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_REMOTE_SOURCES = "remote_sources"
const val KEY_LOCAL_STORAGE = "local_storage"
const val KEY_READER_SWITCHERS = "reader_switchers"
const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons"
const val KEY_TRACKER_ENABLED = "tracker_enabled"
const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi"
const val KEY_TRACK_SOURCES = "track_sources"
const val KEY_TRACK_CATEGORIES = "track_categories"
const val KEY_TRACK_WARNING = "track_warning"
@@ -404,7 +448,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
const val KEY_NOTIFICATIONS_LIGHT = "notifications_light"
const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info"
const val KEY_READER_ANIMATION = "reader_animation"
const val KEY_READER_ANIMATION = "reader_animation2"
const val KEY_READER_MODE = "reader_mode"
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
const val KEY_APP_PASSWORD = "app_password"
@@ -422,6 +466,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SCREENSHOTS_POLICY = "screenshots_policy"
const val KEY_PAGES_PRELOAD = "pages_preload"
const val KEY_SUGGESTIONS = "suggestions"
const val KEY_SUGGESTIONS_WIFI_ONLY = "suggestions_wifi"
const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw"
const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags"
const val KEY_SUGGESTIONS_NOTIFICATIONS = "suggestions_notifications"
@@ -438,16 +483,19 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SYNC_SETTINGS = "sync_settings"
const val KEY_READER_BAR = "reader_bar"
const val KEY_READER_SLIDER = "reader_slider"
const val KEY_READER_BACKGROUND = "reader_background"
const val KEY_READER_SCREEN_ON = "reader_screen_on"
const val KEY_SHORTCUTS = "dynamic_shortcuts"
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
const val KEY_LOCAL_LIST_ORDER = "local_order"
const val KEY_HISTORY_ORDER = "history_order"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_SHELF_SECTIONS = "shelf_sections_2"
const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging"
const val KEY_LOGS_SHARE = "logs_share"
const val KEY_SOURCES_GRID = "sources_grid"
const val KEY_SOURCES_NEW = "sources_new"
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
const val KEY_TIPS_CLOSED = "tips_closed"
const val KEY_SSL_BYPASS = "ssl_bypass"
@@ -461,6 +509,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PROXY_LOGIN = "proxy_login"
const val KEY_PROXY_PASSWORD = "proxy_password"
const val KEY_IMAGES_PROXY = "images_proxy"
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
const val KEY_DISABLE_NSFW = "no_nsfw"
const val KEY_RELATED_MANGA = "related_manga"
const val KEY_NAV_MAIN = "nav_main"
const val KEY_32BIT_COLOR = "enhanced_colors"
// About
const val KEY_APP_UPDATE = "app_update"

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