Compare commits

...

316 Commits

Author SHA1 Message Date
d5c7d8997f Update FUNDING.yml 2022-03-20 17:45:20 +02:00
da797741f9 Fix some settings, fix m3 switches 2022-03-20 17:36:29 +02:00
626d84eea3 Fix VersionId test 2022-03-20 17:27:48 +02:00
4d2f32a082 Update ru translations 2022-03-20 17:18:06 +02:00
c7cbe18afd Cleanup resources 2022-03-20 17:15:11 +02:00
Aliaksiej Razumaŭ
d1eb76d960 Translated using Weblate (Belarusian)
Currently translated at 98.8% (265 of 268 strings)

Co-authored-by: Aliaksiej Razumaŭ <belarusaed@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2022-03-20 17:04:35 +02:00
J. Lavoie
4b49f7d7c1 Translated using Weblate (Finnish)
Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (French)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (German)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (266 of 267 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (266 of 266 strings)

Translated using Weblate (French)

Currently translated at 100.0% (266 of 266 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (266 of 266 strings)

Translated using Weblate (German)

Currently translated at 100.0% (266 of 266 strings)

Translated using Weblate (Spanish)

Currently translated at 98.4% (262 of 266 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/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2022-03-20 17:04:35 +02:00
kuragehime
fce73f6457 Translated using Weblate (Japanese)
Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (266 of 266 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-03-20 17:04:35 +02:00
Oğuz Ersen
8d958329b9 Translated using Weblate (Turkish)
Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (266 of 266 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-03-20 17:04:35 +02:00
Luiz-bro
70006b3cf4 Translated using Weblate (Portuguese)
Currently translated at 100.0% (266 of 266 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2022-03-20 17:04:35 +02:00
Allan Nordhøy
fbdac9a7c0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (265 of 265 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
Translation: Kotatsu/Strings
2022-03-20 17:04:35 +02:00
8a08d58ed7 Merge branch 'feature/m3' into devel 2022-03-20 17:04:06 +02:00
6dc8ee5cf0 Return backgroundTint to FAB 2022-03-20 11:53:54 +03:00
b646cc00a3 Fix redundant fragments in DetailsActivity 2022-03-19 16:34:48 +02:00
7d2e70da7e Minor UI fixes 2022-03-19 13:55:32 +03:00
83cc3d60c8 Use new M3 switches 2022-03-19 13:54:20 +03:00
15ee102db4 Adjust ui to m3 style 2022-03-19 08:41:50 +02:00
ff25162834 Fixes 2022-03-17 16:51:10 +02:00
4913332444 Remember search mode 2022-03-17 15:32:32 +02:00
996f8f0f2e Fixes 2022-03-17 15:14:46 +02:00
4851139ba5 Improve performance 2022-03-17 11:24:15 +02:00
f0380d7eff Fix non-parcelable extras 2022-03-17 07:28:56 +02:00
11356484b2 Enhance tablet ui (#127)
* Enhance tablet ui

* Some adjustments

* Fix MainActivity insets

* Improve details activity layout

* Fix dialogs width

* Improve thumbnails bs

* Adjust margins in manga details screen

Co-authored-by: Zakhar Timoshenko <vp1984tanki@gmail.com>
2022-03-16 18:49:13 +02:00
e6cd6617ba Fix release build 2022-03-16 18:46:53 +02:00
de176ec040 Merge branch 'feature/desktop-ui' into devel 2022-03-16 18:08:50 +02:00
8a365250d9 Improve sources settings 2022-03-16 18:07:07 +02:00
9bd47e0410 Fix MangaRepository instantiation 2022-03-15 07:26:53 +02:00
02c15f896b Move parsers out of project 2022-03-15 07:06:32 +02:00
150699f64d Improve thumbnails bs 2022-03-13 18:37:50 +02:00
05ffc145be Merge branch 'devel' into feature/desktop-ui 2022-03-13 17:57:10 +02:00
25d52c5a61 Enhance manga search suggestion 2022-03-13 17:53:43 +02:00
abc2fb0e40 Hide FAB on search suggestions 2022-03-13 17:53:43 +02:00
54dfc32455 Make SearchView clearable 2022-03-13 17:53:43 +02:00
3802bc146f Suggest tags on search 2022-03-13 17:53:43 +02:00
8b295f6a93 Fix dialogs width 2022-03-13 17:50:49 +02:00
c115bcc163 Improve details activity layout 2022-03-13 17:10:34 +02:00
88a3589f1d Fix MainActivity insets 2022-03-13 09:54:12 +02:00
52dbd70c2f Some adjustments 2022-03-11 21:50:49 +03:00
0b07e83e3c Enhance tablet ui 2022-03-11 19:30:20 +02:00
445ff89392 Enhance nsfw detection and indication 2022-03-11 19:24:10 +02:00
a8a65e953f Support authorization for MangaLib and HentaiLib #122 2022-03-10 19:52:43 +02:00
755f1e5747 Fix filter 2022-03-10 19:52:43 +02:00
d5d19c37d8 Add referer in TrackWorker 2022-03-10 10:45:34 +03:00
6f85afb841 Merge branch 'weblate-kotatsu-strings' of https://github.com/weblate/Kotatsu into weblate-weblate-kotatsu-strings 2022-03-10 08:45:56 +02:00
3aed24fb49 Fix some authorization issues 2022-03-10 08:22:34 +02:00
2947cd3038 Add NudeMoon manga parser 2022-03-10 08:22:34 +02:00
2849ac58cb Support links in description 2022-03-10 08:22:34 +02:00
a3ef1766a1 Add ComicKFun manga parser 2022-03-10 08:22:34 +02:00
Allan Nordhøy
852bcbbb24 Translated using Weblate (English)
Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
Translation: Kotatsu/Strings
2022-03-09 01:05:30 +01:00
J. Lavoie
7438b6ce05 Translated using Weblate (Finnish)
Currently translated at 100.0% (265 of 265 strings)

Translated using Weblate (French)

Currently translated at 100.0% (265 of 265 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (265 of 265 strings)

Translated using Weblate (German)

Currently translated at 100.0% (265 of 265 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/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-03-09 01:05:30 +01:00
kuragehime
fcb301260c Translated using Weblate (Japanese)
Currently translated at 100.0% (266 of 266 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-03-09 01:05:29 +01:00
Oğuz Ersen
fc4dccb4e9 Translated using Weblate (Turkish)
Currently translated at 100.0% (265 of 265 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (266 of 266 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-03-09 01:05:29 +01:00
1f1fcf281d Revert "Use circle mask for thumb in new updates notification"
This reverts commit 8ff4eb26
2022-03-08 22:13:50 +03:00
a0c5b75bba Fix webtoon scroll #20 2022-03-08 19:25:10 +02:00
ccf4e4d285 Migrate to TransitionManager from custom animations 2022-03-08 19:18:02 +02:00
15c570979b Merge branch 'feature/page-preload' into devel 2022-03-08 19:00:13 +02:00
57f3715128 Cleanup resources and code 2022-03-08 18:58:15 +02:00
148986b454 Quick search across genres in filter 2022-03-08 17:28:00 +02:00
179b08b96a Add action for empty list state 2022-03-08 16:06:51 +02:00
d7f60fa95a Fix Batoto empty results 2022-03-08 14:25:46 +02:00
564f052a2f Add Bato.To manga source #77 2022-03-08 14:04:44 +02:00
8ff4eb2602 Use circle mask for thumb in new updates notification 2022-03-08 01:54:03 +03:00
6e5197a3f5 Specify page max progress explicitly 2022-03-07 13:10:09 +02:00
2b8c713169 Merge branch 'devel' into feature/page-preload 2022-03-07 12:55:15 +02:00
J. Lavoie
6a40a388b3 Translated using Weblate (Finnish)
Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (French)

Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (German)

Currently translated at 100.0% (264 of 264 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/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-03-07 12:52:44 +02:00
mondstern
f52794e93c Translated using Weblate (Russian)
Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (264 of 264 strings)

Co-authored-by: mondstern <mondstern@snopyta.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2022-03-07 12:52:44 +02:00
kuragehime
26e32ab584 Translated using Weblate (Japanese)
Currently translated at 100.0% (264 of 264 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-03-07 12:52:44 +02:00
Allan Nordhøy
5c3baa8575 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.2% (262 of 264 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
Translation: Kotatsu/Strings
2022-03-07 12:52:44 +02:00
Oğuz Ersen
ff4fe14f89 Translated using Weblate (Turkish)
Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (Turkish)

Currently translated at 99.6% (263 of 264 strings)

Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-03-07 12:52:44 +02:00
Luiz-bro
afc9682d53 Translated using Weblate (Portuguese)
Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (Portuguese)

Currently translated at 98.4% (260 of 264 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2022-03-07 12:52:44 +02:00
9686ad6f00 Show page progress on ProgressIndicator 2022-03-07 12:42:22 +02:00
ff21d1c4ec Fix incorrect arrow direction symbol 2022-03-06 23:55:15 +03:00
e1285fe738 Option to control pages preloading 2022-03-06 19:56:11 +02:00
889eea9c89 Show page loading progress 2022-03-06 15:14:28 +02:00
4a88ecc549 Show WebView loading progress 2022-03-05 09:01:35 +02:00
6eca4028ec Dont recreate WebView on configuration changed #119 2022-03-05 08:49:10 +02:00
5158f4bd89 Replace chapters dialog with bottom sheet 2022-03-04 20:10:29 +02:00
eb7e255430 Small ui fixes 2022-03-04 19:09:58 +02:00
f6a70dc7ac Fix build 2022-03-04 18:41:03 +02:00
4d447f9f01 Merge branch 'devel' of github.com:nv95/Kotatsu into devel 2022-03-04 18:39:12 +02:00
Oğuz Ersen
6fa8406636 Translated using Weblate (Turkish)
Currently translated at 100.0% (255 of 255 strings)

Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-03-04 18:37:08 +02:00
Luiz-bro
6d409168e3 Translated using Weblate (Portuguese)
Currently translated at 100.0% (255 of 255 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.2% (253 of 255 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2022-03-04 18:37:08 +02:00
Allan Nordhøy
5c10dae028 Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.8% (252 of 255 strings)

Translated using Weblate (English)

Currently translated at 100.0% (255 of 255 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
Translation: Kotatsu/Strings
2022-03-04 18:37:08 +02:00
6a965ddb28 Merge branch 'feature/bs-filter' into devel 2022-03-04 18:35:57 +02:00
9b86052624 Merge branch 'feature/suggestions' into devel 2022-03-04 18:34:15 +02:00
3c64d6675e Handle filter loading errors 2022-03-04 08:16:36 +02:00
9588ac8cbd Preload pages 2022-03-03 21:01:16 +02:00
5c05aaeacf Open filter from list header 2022-03-03 18:39:23 +02:00
238bc89be9 Move filter into bottom sheet 2022-03-02 21:05:57 +02:00
28a4d4164e Merge branch 'weblate-kotatsu-strings' of https://github.com/weblate/Kotatsu into weblate-weblate-kotatsu-strings 2022-03-02 19:38:25 +02:00
19fe2e0eb5 Fix strings 2022-03-02 19:26:45 +02:00
862fb3c2e6 Fix badges position 2022-03-02 19:22:51 +02:00
df34e921f3 Fix scrollbars 2022-03-02 19:16:57 +02:00
44c1b5ebb4 Update feed ui 2022-03-02 19:10:39 +02:00
a9454a1455 Suggestions update action 2022-03-02 18:11:56 +02:00
kuragehime
e9e419399c Translated using Weblate (Japanese)
Currently translated at 100.0% (255 of 255 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (251 of 251 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-03-02 12:28:40 +01:00
Oğuz Ersen
09db484d5e Translated using Weblate (Turkish)
Currently translated at 100.0% (255 of 255 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (251 of 251 strings)

Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-03-02 12:28:40 +01:00
J. Lavoie
192737bab9 Translated using Weblate (Finnish)
Currently translated at 100.0% (255 of 255 strings)

Translated using Weblate (French)

Currently translated at 100.0% (255 of 255 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (255 of 255 strings)

Translated using Weblate (German)

Currently translated at 100.0% (255 of 255 strings)

Translated using Weblate (French)

Currently translated at 100.0% (251 of 251 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (251 of 251 strings)

Translated using Weblate (German)

Currently translated at 100.0% (251 of 251 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/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-03-02 12:28:40 +01:00
Allan Nordhøy
bb68f7b442 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.6% (250 of 251 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
Translation: Kotatsu/Strings
2022-03-02 12:28:40 +01:00
f46a9c5f3a Fix crash on tablets when clicking on cover 2022-03-02 14:28:28 +03:00
27658eea20 Fix tags case 2022-03-01 07:59:15 +02:00
eec21fc5c1 Adjust manga grid item 2022-02-28 20:25:01 +03:00
5d26743c8f Fix tags for suggestions 2022-02-28 19:08:16 +02:00
3afa782e91 Merge branch 'devel' into feature/suggestions 2022-02-28 18:46:33 +02:00
cfdc3a15c5 Merge branch 'feature/counters' into devel 2022-02-28 18:33:30 +02:00
a2a7c26a42 Block screenshots on password activities #114 2022-02-28 18:28:26 +02:00
7fb67be1b6 Some UI changes 2022-02-28 01:24:21 +03:00
e8a225f97a Update material components dependency 2022-02-28 01:18:59 +03:00
54a914097d Option to block screenshots in reader #114 2022-02-27 20:28:55 +02:00
245e32237e Update widgets from background 2022-02-27 19:22:59 +02:00
29df122369 Move workers initialization to background 2022-02-27 18:47:47 +02:00
Sertinel
894900e955 Translated using Weblate (Turkish)
Currently translated at 47.8% (120 of 251 strings)

Co-authored-by: Sertinel <cankalenderr@yandex.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-02-27 18:26:58 +02:00
632715e6c9 Suggestions settings 2022-02-27 18:25:18 +02:00
97c0fcf022 Merge branch 'devel' of github.com:nv95/Kotatsu into feature/suggestions 2022-02-27 16:32:32 +02:00
b3781abdeb New chapters counters in lists 2022-02-27 09:28:25 +02:00
1f7252fd12 Update feed ui 2022-02-26 18:14:15 +02:00
3c0c4ce9c0 Fix local manga size 2022-02-26 15:42:48 +02:00
ed4c470bdc Support batch manga import 2022-02-26 13:56:21 +02:00
70db9ba94a Update fast scroll 2022-02-26 13:10:34 +02:00
Hosted Weblate
3235141b2e Update translation files
Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

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
2022-02-26 12:21:41 +02:00
Luiz-bro
2f9364561d Translated using Weblate (Portuguese)
Currently translated at 100.0% (248 of 248 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2022-02-26 12:21:41 +02:00
Anonymous
8444188616 Translated using Weblate (Russian)
Currently translated at 100.0% (248 of 248 strings)

Translated using Weblate (Turkish)

Currently translated at 22.9% (57 of 248 strings)

Translated using Weblate (Persian)

Currently translated at 2.8% (7 of 248 strings)

Translated using Weblate (Finnish)

Currently translated at 97.9% (243 of 248 strings)

Translated using Weblate (Sinhala)

Currently translated at 2.8% (7 of 248 strings)

Translated using Weblate (Italian)

Currently translated at 99.1% (246 of 248 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/si/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-02-26 12:21:41 +02:00
J. Lavoie
2d38733822 Translated using Weblate (Finnish)
Currently translated at 97.9% (243 of 248 strings)

Translated using Weblate (French)

Currently translated at 100.0% (248 of 248 strings)

Translated using Weblate (Italian)

Currently translated at 99.1% (246 of 248 strings)

Translated using Weblate (German)

Currently translated at 100.0% (248 of 248 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/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-02-26 12:21:41 +02:00
Allan Nordhøy
e6b574d13f Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (248 of 248 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
Translation: Kotatsu/Strings
2022-02-26 12:21:41 +02:00
Anonymous
2e26204a4e Translated using Weblate (Spanish)
Currently translated at 100.0% (248 of 248 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (248 of 248 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2022-02-26 12:21:41 +02:00
Hosted Weblate
a932fd2cd9 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

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
2022-02-26 12:06:20 +02:00
a2d3b88c08 Fix non-translatable strings 2022-02-26 12:06:19 +02:00
62a177fcb3 Remove empty non-translated strings 2022-02-26 11:43:59 +02:00
19c751d349 Small ui updates 2022-02-26 11:39:50 +02:00
def2d5f494 Refactor and deprecations fixes 2022-02-26 10:14:40 +02:00
94e9fa35e2 Remove preferences delegates 2022-02-25 19:48:11 +02:00
14be8d4936 Merge remote-tracking branch 'origin/devel' into devel 2022-02-24 22:14:16 +03:00
38b550ecbb Add Discord link in about 2022-02-24 22:12:56 +03:00
b8ecfb5455 Add link to discord badge 2022-02-24 17:22:53 +03:00
f4c9d67178 Add discord badge to README 2022-02-24 17:20:57 +03:00
ad4c65369d Merge pull request #104 from weblate/weblate-kotatsu-strings
Translations update from Hosted Weblate
2022-02-23 18:56:02 +02:00
kuragehime
db6a53de84 Translated using Weblate (Japanese)
Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
2022-02-23 17:55:15 +01:00
fd25bd5934 Merge pull request #103 from weblate/weblate-kotatsu-strings
Translations update from Hosted Weblate
2022-02-22 20:03:00 +02:00
Anonymous
33b2ec7ab1 Translated using Weblate (Japanese)
Currently translated at 64.5% (162 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
2022-02-22 18:57:48 +01:00
Anonymous
cfb4c8d66a Translated using Weblate (Turkish (Ottoman))
Currently translated at 0.7% (2 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ota/
2022-02-22 18:57:47 +01:00
Anonymous
0797f1809a Translated using Weblate (Turkish)
Currently translated at 23.5% (59 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
2022-02-22 18:57:43 +01:00
Anonymous
e8e1ab6637 Translated using Weblate (Persian)
Currently translated at 2.7% (7 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
2022-02-22 18:57:40 +01:00
Anonymous
1cb5e8134e Translated using Weblate (Arabic)
Currently translated at 0.7% (2 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
2022-02-22 18:57:36 +01:00
Anonymous
246e3ee7d6 Translated using Weblate (Finnish)
Currently translated at 65.3% (164 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
2022-02-22 18:57:31 +01:00
Anonymous
35e782884d Translated using Weblate (Sinhala)
Currently translated at 3.1% (8 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/si/
2022-02-22 18:57:31 +01:00
Anonymous
e5e45fa40f Translated using Weblate (French)
Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-02-22 18:57:28 +01:00
Anonymous
f24aa5af06 Translated using Weblate (Portuguese)
Currently translated at 99.6% (250 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
2022-02-22 18:57:26 +01:00
Anonymous
25ebde1f0a Translated using Weblate (Italian)
Currently translated at 65.7% (165 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2022-02-22 18:57:25 +01:00
Anonymous
120f45a6c5 Translated using Weblate (German)
Currently translated at 74.5% (187 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2022-02-22 18:57:24 +01:00
Anonymous
fa8ae112ad Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.2% (234 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-02-22 18:57:23 +01:00
Anonymous
c53d7f953d Translated using Weblate (Russian)
Currently translated at 99.2% (249 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
2022-02-22 18:57:22 +01:00
Anonymous
9881f9031f Translated using Weblate (Spanish)
Currently translated at 86.0% (216 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2022-02-22 18:57:19 +01:00
Anonymous
bd11827d8b Translated using Weblate (Belarusian)
Currently translated at 66.5% (167 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2022-02-22 18:57:17 +01:00
40f2713234 Merge remote-tracking branch 'weblate/devel' into devel 2022-02-22 19:50:47 +02:00
Weblate (bot)
b8e564a8d0 Translations update from Hosted Weblate (#96)
* Translated using Weblate (Spanish)

Currently translated at 68.2% (170 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/

* Translated using Weblate (Russian)

Currently translated at 80.3% (200 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 86.7% (216 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/

* Translated using Weblate (French)

Currently translated at 75.5% (188 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/

* Translated using Weblate (Russian)

Currently translated at 99.5% (248 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/

* Translated using Weblate (German)

Currently translated at 68.9% (173 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/

* Translated using Weblate (French)

Currently translated at 82.0% (206 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/

* Translated using Weblate (Spanish)

Currently translated at 75.2% (189 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/

* Translated using Weblate (French)

Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/

* Translated using Weblate (Spanish)

Currently translated at 86.0% (216 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 93.2% (234 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/

* Added translation using Weblate (Turkish)

* Added translation using Weblate (Turkish (Ottoman))

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/

* Translated using Weblate (Turkish)

Currently translated at 23.5% (59 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/

* Translated using Weblate (English)

Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 93.2% (234 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/

* Translated using Weblate (German)

Currently translated at 74.5% (187 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/

* Translated using Weblate (French)

Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/

* Translated using Weblate (Belarusian)

Currently translated at 66.5% (167 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/

* Added translation using Weblate (Japanese)

* Translated using Weblate (Japanese)

Currently translated at 29.0% (73 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/

* Translated using Weblate (Japanese)

Currently translated at 64.5% (162 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Co-authored-by: Zakhar Timoshenko <vp1984tanki@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: mondstern <mondstern@snopyta.org>
Co-authored-by: nzgha <nzgha.hw@runbox.com>
Co-authored-by: Jakob Holkestad Molnes <Jakob.Holkestad.Molnes@gmail.com>
Co-authored-by: Sertinel <cankalenderr@yandex.com>
Co-authored-by: Aliaksiej Razumaŭ <belarusaed@gmail.com>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
2022-02-22 19:47:08 +02:00
9cbca0329a Merge branch 'master' into devel 2022-02-22 19:43:09 +02:00
kuragehime
c376662939 Translated using Weblate (Japanese)
Currently translated at 64.5% (162 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
2022-02-22 18:42:46 +01:00
kuragehime
6f79bf198d Translated using Weblate (Japanese)
Currently translated at 29.0% (73 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
2022-02-22 18:42:46 +01:00
kuragehime
542deac705 Added translation using Weblate (Japanese) 2022-02-22 18:42:46 +01:00
Aliaksiej Razumaŭ
a905806232 Translated using Weblate (Belarusian)
Currently translated at 66.5% (167 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2022-02-22 18:42:46 +01:00
J. Lavoie
7aeb691427 Translated using Weblate (French)
Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-02-22 18:42:46 +01:00
J. Lavoie
b7922d9096 Translated using Weblate (German)
Currently translated at 74.5% (187 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2022-02-22 18:42:46 +01:00
Allan Nordhøy
be2d335a5b Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.2% (234 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-02-22 18:42:46 +01:00
Allan Nordhøy
8de5c1fc3d Translated using Weblate (English)
Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
2022-02-22 18:42:46 +01:00
Sertinel
aac4d1218d Translated using Weblate (Turkish)
Currently translated at 23.5% (59 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
2022-02-22 18:42:46 +01:00
Luiz-bro
ba6474c7bb Translated using Weblate (Portuguese)
Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
2022-02-22 18:42:45 +01:00
Sertinel
236c0edaaf Added translation using Weblate (Turkish (Ottoman)) 2022-02-22 18:42:45 +01:00
Sertinel
02dc6965d1 Added translation using Weblate (Turkish) 2022-02-22 18:42:45 +01:00
Luiz-bro
735bf66593 Translated using Weblate (Portuguese)
Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
2022-02-22 18:42:45 +01:00
Jakob Holkestad Molnes
dcc180eea5 Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.2% (234 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-02-22 18:42:45 +01:00
nzgha
694dc7a807 Translated using Weblate (Spanish)
Currently translated at 86.0% (216 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2022-02-22 18:42:45 +01:00
J. Lavoie
b2b8a62a57 Translated using Weblate (French)
Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-02-22 18:42:45 +01:00
nzgha
f964dd8267 Translated using Weblate (Spanish)
Currently translated at 75.2% (189 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2022-02-22 18:42:45 +01:00
J. Lavoie
5260295079 Translated using Weblate (French)
Currently translated at 82.0% (206 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-02-22 18:42:45 +01:00
Luiz-bro
6d6f881367 Translated using Weblate (Portuguese)
Currently translated at 100.0% (251 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
2022-02-22 18:42:45 +01:00
mondstern
eae0709c09 Translated using Weblate (German)
Currently translated at 68.9% (173 of 251 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2022-02-22 18:42:45 +01:00
0c83329e59 Translated using Weblate (Russian)
Currently translated at 99.5% (248 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
2022-02-22 18:42:45 +01:00
J. Lavoie
9de5024930 Translated using Weblate (French)
Currently translated at 75.5% (188 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-02-22 18:42:45 +01:00
Luiz-bro
c813677041 Translated using Weblate (Portuguese)
Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
2022-02-22 18:42:45 +01:00
Allan Nordhøy
d7541a115e Translated using Weblate (Norwegian Bokmål)
Currently translated at 86.7% (216 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-02-22 18:42:45 +01:00
4c911e666e Translated using Weblate (Russian)
Currently translated at 80.3% (200 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
2022-02-22 18:42:45 +01:00
Luiz-bro
4e059c4ee3 Translated using Weblate (Spanish)
Currently translated at 68.2% (170 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2022-02-22 18:42:45 +01:00
15d0addb7b Fix Remanga chapters parsing 2022-02-22 19:34:00 +02:00
1713efb51f Increase version 2022-02-20 18:43:12 +02:00
9089555320 Fix internal storage sharing #98 2022-02-20 18:41:38 +02:00
2f3b1f397c Fix Remanga source and check for paid chapters #101 2022-02-20 18:40:47 +02:00
7ebb98ce06 Allow overwrite non-empty download directory #99 2022-02-20 18:13:54 +02:00
805044fcf1 Add .editorconfig file 2022-02-20 18:12:04 +02:00
51d6a073e0 Refactor local storage manager 2022-02-13 10:21:37 +02:00
02980ea1e6 Allow overwrite non-empty download directory #99 2022-02-13 08:50:41 +02:00
920ea6959c Merge branch 'devel' into feature/suggestions 2022-02-13 08:31:44 +02:00
c7aaa22eab Merge branch 'master' into devel 2022-02-13 07:07:27 +02:00
c218ae0baa Merge branch 'release/2.1.4' 2022-02-12 20:37:41 +02:00
5820b2f511 Fix saved page sharing 2022-02-12 20:31:30 +02:00
79c2bf17fd Quick search across manga sources 2022-02-12 15:16:30 +02:00
78aa4d76db Show favicons in sources list 2022-02-12 15:16:11 +02:00
e2f3ba19b8 Update dependencies minor versions 2022-02-12 15:05:58 +02:00
41045686fc Increase version 2022-02-12 14:58:03 +02:00
8b0b375dfe Fix ActivityNotFoundException 2022-02-12 14:57:27 +02:00
c7c23b9768 Fix ItemTouchHelper leak 2022-02-12 14:57:16 +02:00
33190ae3ea Fix DownloadBinder leak 2022-02-12 14:57:10 +02:00
03590f4b82 Fix widgets context leak 2022-02-12 14:57:03 +02:00
cbcf98e1d4 Fix blocking calls in coroutines 2022-02-12 14:56:30 +02:00
4098f06995 Update material components 2022-02-12 14:07:06 +02:00
98f723200b Fix ActivityNotFoundException 2022-02-12 10:31:26 +02:00
07634d01f3 Fix some settings ui 2022-02-12 10:30:16 +02:00
3bd67e2098 Move LeakCanary to settings 2022-02-11 20:13:43 +02:00
427ce5fd07 Fix ItemTouchHelper leak 2022-02-11 19:41:49 +02:00
6bf927bb2c Fix DownloadBinder leak 2022-02-11 19:32:38 +02:00
da17c3495a Fix widgets context leak 2022-02-11 19:23:41 +02:00
e739e3f9e0 Update dependencies 2022-02-11 19:18:35 +02:00
10ec72047c Update dependencies 2022-02-04 08:18:01 +02:00
4be514b754 Migrate to MaterialDividerItemDecoration 2022-02-03 08:58:12 +02:00
add72c0be3 Fix settings ui 2022-01-28 19:31:28 +02:00
5758eed77b Update chapters list, show already downloaded chapters #95 2022-01-28 19:16:33 +02:00
c7dc05be5a Merge pull request #84 from ztimms73/m3
Material 3
2022-01-28 17:44:20 +02:00
355933c742 Fix blocking calls in coroutines 2022-01-26 19:24:27 +02:00
f2bbf5855b Merge branch 'devel' into m3 2022-01-26 15:24:52 +03:00
Luiz-bro
970200aa40 Translated using Weblate (Portuguese)
Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
2022-01-26 08:36:43 +02:00
Mohammad
67306734fa Translated using Weblate (Persian)
Currently translated at 3.6% (9 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
2022-01-26 08:36:43 +02:00
Aliaksiej Razumaŭ
b14d629a45 Translated using Weblate (Belarusian)
Currently translated at 99.5% (248 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2022-01-26 08:36:43 +02:00
Mohammad
1fb9eb3e3b Added translation using Weblate (Persian) 2022-01-26 08:36:43 +02:00
b0ywearngtights
1404a83c10 Translated using Weblate (Portuguese)
Currently translated at 45.3% (113 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
2022-01-26 08:36:43 +02:00
b0ywearngtights
e18f911b1b Translated using Weblate (Spanish)
Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2022-01-26 08:36:43 +02:00
c3f0644b46 Merge branch 'comradekingu-patch-1' into devel 2022-01-26 08:31:58 +02:00
733889f238 Merge branch 'patch-1' of https://github.com/comradekingu/Kotatsu into comradekingu-patch-1 2022-01-26 08:31:20 +02:00
e280aa4963 Quick search across manga sources 2022-01-26 08:22:41 +02:00
254b0ab488 Some cleanup 2022-01-26 01:07:12 +03:00
1253ca07cc Allow Samsung devices on Android 12+ to use dynamic theme 2022-01-26 00:50:38 +03:00
e8bb4bac66 Implement dynamic theme option 2022-01-26 00:45:59 +03:00
2ac6b84f87 Show favicons in sources list 2022-01-25 19:56:37 +02:00
e3a80b5a6d Enhance download cancellation in blocking io tasks #90 2022-01-23 10:31:50 +02:00
66dc5a9597 Fix MangaTown licensed chapters 2022-01-23 10:31:50 +02:00
cb6bf91dd3 Fix missing fragment crash #91 2022-01-23 10:31:50 +02:00
fb815abad0 Fix widgets #86 2022-01-23 10:31:50 +02:00
8ef7580097 Fix MangaRead parse #87 2022-01-23 10:31:50 +02:00
5f5a98e351 Merge branch 'release/2.1.3' into devel 2022-01-23 10:31:13 +02:00
2535739c2b Enhance download cancellation in blocking io tasks #90 2022-01-23 10:13:32 +02:00
b6e13de73f Fix MangaTown licensed chapters 2022-01-23 09:46:26 +02:00
fc3efbabbd Fix missing fragment crash #91 2022-01-23 08:14:56 +02:00
2a7761dbc3 Fix widgets #86 2022-01-23 08:05:07 +02:00
852f31574f Fix MangaRead parse #87 2022-01-23 07:49:03 +02:00
ee79c23fdf Use animation to hide/show fab 2022-01-17 09:40:41 +03:00
097e040dd6 Fix AlertDialogs 2022-01-17 09:26:54 +03:00
722b6d1e59 Cleanup MangaSource fields 2022-01-16 20:14:41 +02:00
eed8ef7010 Initial Material 3 theming 2022-01-16 17:53:03 +03:00
ba30690d26 Add static icons for manga sources in drawer 2022-01-15 18:40:14 +02:00
4da6a4d450 Update dependencies 2022-01-15 18:29:51 +02:00
197393fbd1 Fix webtoon scroll 2022-01-15 17:21:03 +02:00
J. Lavoie
51ef6e3c78 Translated using Weblate (French)
Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-01-15 08:37:11 +02:00
J. Lavoie
663277fe6f Translated using Weblate (Italian)
Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2022-01-15 08:37:11 +02:00
J. Lavoie
332a38d674 Translated using Weblate (German)
Currently translated at 100.0% (249 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2022-01-15 08:37:11 +02:00
e9410a2f54 [MangaOwl] Fix missing pages 2022-01-15 08:20:17 +02:00
b5fa2bd660 Fix MangaDex pages extraction 2022-01-14 08:50:04 +02:00
e56c61d834 Update manga parsers 2022-01-10 18:36:52 +02:00
Allan Nordhøy
7cb51f552a Suggested changes made
Co-authored-by: Koitharu <nvasya95@gmail.com>
2022-01-10 13:32:29 +01:00
677f71dd84 Increase version 2022-01-10 08:53:54 +02:00
Allan Nordhøy
3f90f88600 Translated using Weblate (Norwegian Bokmål)
Currently translated at 87.9% (219 of 249 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-01-10 08:03:16 +02:00
Allan Nordhøy
c7348f7438 Merge branch 'devel' into patch-1 2022-01-07 23:54:56 +01:00
Allan Nordhøy
22ac13c140 Suggested changes made 2022-01-07 20:28:22 +00:00
229a7c70d9 Implement new manga sources settings list screen #78 2022-01-07 19:04:59 +02:00
a2dbec98f9 Fix Ninemanga filter 2022-01-06 17:46:46 +02:00
3a02f8090e Fix pages numbers 2022-01-06 17:31:49 +02:00
17519db44e Refactor sources settings list 2022-01-06 17:26:16 +02:00
99186bf269 Show pages numbers in reader 2022-01-06 09:58:20 +02:00
9a65e40be1 Update page flip animation 2022-01-05 15:46:22 +02:00
f0add59f99 Fix smooth cover loading in details 2022-01-05 15:04:14 +02:00
f18c182a6a Refactor filters 2022-01-05 14:49:18 +02:00
68e9588f24 Fix AniBel endpoint 2022-01-05 11:49:56 +02:00
eea427216d Fullscreen cover view activity 2022-01-05 11:42:03 +02:00
8e9b89f6f0 Fix GroupLe sources covers 2022-01-05 10:51:36 +02:00
4f3281be99 MangaDex source 2022-01-05 10:43:32 +02:00
badlop
eb56a82702 Translated using Weblate (Spanish)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2022-01-04 12:37:53 +02:00
nzgha
089ccc9d15 Translated using Weblate (Spanish)
Currently translated at 99.1% (244 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2022-01-04 12:37:53 +02:00
J. Lavoie
12c1365513 Translated using Weblate (Finnish)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
2022-01-04 12:37:53 +02:00
J. Lavoie
7ecf9316e3 Translated using Weblate (French)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-01-04 12:37:53 +02:00
J. Lavoie
12e98ec36a Translated using Weblate (Italian)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2022-01-04 12:37:53 +02:00
J. Lavoie
22977fc7bc Translated using Weblate (German)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2022-01-04 12:37:53 +02:00
Aliaksiej Razumaŭ
b387a49a4e Translated using Weblate (Belarusian)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2022-01-04 12:37:53 +02:00
Allan Nordhøy
dbbb0d0f64 Translated using Weblate (Norwegian Bokmål)
Currently translated at 88.2% (217 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-01-04 12:37:53 +02:00
Allan Nordhøy
0bbf2b752f Translated using Weblate (English)
Currently translated at 100.0% (246 of 246 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
2022-01-04 12:37:53 +02:00
Aliaksiej Razumaŭ
14c1eacffa Translated using Weblate (Belarusian)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2022-01-04 12:37:53 +02:00
J. Lavoie
c2a0525bb8 Translated using Weblate (French)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2022-01-04 12:37:53 +02:00
J. Lavoie
4f502e580c Translated using Weblate (Italian)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2022-01-04 12:37:53 +02:00
J. Lavoie
7cb303966a Translated using Weblate (German)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2022-01-04 12:37:53 +02:00
Allan Nordhøy
3f0431f88b Translated using Weblate (Norwegian Bokmål)
Currently translated at 87.3% (214 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
2022-01-04 12:37:53 +02:00
Allan Nordhøy
aad5601df1 Translated using Weblate (English)
Currently translated at 100.0% (245 of 245 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
2022-01-04 12:37:53 +02:00
8f2cf8141a Mark HenChan manga as nsfw 2021-12-10 18:10:12 +02:00
eefd1129f7 Update dependencies, setup FragmentStrictMode 2021-12-04 12:09:13 +02:00
5ed0f8b5a6 Check if category name is not empty 2021-12-03 21:11:50 +02:00
9b4aa4fd64 Migrate AniBel parser to graphql 2021-12-03 20:30:20 +02:00
bbb226791b Merge branch 'hotifx/2.0.1' into devel 2021-11-22 08:48:11 +02:00
30ac4435d4 Update version 2021-11-22 08:41:35 +02:00
1b9dfe1901 Temporary change anibel domain 2021-11-21 17:41:32 +02:00
808a6efd8f [Source] [MangaOwl] Fix not loading chapter list 2021-11-21 17:13:39 +02:00
66ed19ed5a [Source] [MangaOwl] Fix not loading chapter list 2021-11-21 17:12:04 +02:00
527a3cbd09 Option to exclude NSFW content from history 2021-11-20 16:49:30 +02:00
f22963b315 Use DownloadManager for pages saving 2021-11-18 20:06:44 +02:00
2ce5cb524f Increase version 2021-11-18 18:43:30 +02:00
4cbc6392fb Update AdapterDelegates library 2021-11-17 19:23:08 +02:00
049f9fa625 Fix cover image in lists 2021-11-17 19:08:13 +02:00
c853fae820 Fix browser activity insets 2021-11-12 20:46:25 +02:00
dd1d84a4fe Remanga authorization support #73 2021-11-12 20:36:01 +02:00
1569aa5dd5 Cleanup data classes 2021-11-12 20:13:49 +02:00
51cd88eded Update dependencies 2021-11-12 20:13:48 +02:00
Aliaksiej Razumaŭ
bf386deef0 Translated using Weblate (Belarusian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-11-12 20:13:26 +02:00
J. Lavoie
5c80cdee81 Translated using Weblate (Spanish)
Currently translated at 98.7% (241 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2021-11-12 20:13:26 +02:00
J. Lavoie
b29fbb37cd Translated using Weblate (Finnish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
2021-11-12 20:13:26 +02:00
J. Lavoie
589831beef Translated using Weblate (French)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-11-12 20:13:26 +02:00
J. Lavoie
0f5d153543 Translated using Weblate (Italian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-11-12 20:13:26 +02:00
J. Lavoie
ab1c99d132 Translated using Weblate (German)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-11-12 20:13:26 +02:00
22eebe89e7 Merge branch 'devel' into feature/suggestions 2021-09-26 17:37:59 +03:00
550dfa9c9e Update app/src/main/res/values/strings.xml
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
2021-07-12 20:29:30 +03:00
e6b6a6bb37 Update app/src/main/res/values/strings.xml
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
2021-07-12 20:29:14 +03:00
Allan Nordhøy
92f9438992 App strings reworked 2021-07-12 12:59:34 +00:00
f0e56c4b6a Suggestions 2021-05-12 20:10:47 +03:00
569 changed files with 11116 additions and 10136 deletions

17
.editorconfig Normal file
View File

@@ -0,0 +1,17 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
insert_final_newline = false
max_line_length = 120
tab_width = 4
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
ij_continuation_indent_size = 4
[{*.gradle.kts,*.kt,*.kts,*.main.kts}]
ij_kotlin_allow_trailing_comma = true
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
custom: ["https://money.yandex.ru/to/410012543938752"]
custom: ["https://yoomoney.ru/to/410012543938752"]

1
.idea/gradle.xml generated
View File

@@ -14,7 +14,6 @@
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>

View File

@@ -36,5 +36,10 @@
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://maven.pkg.github.com/nv95/kotatsu-parsers" />
</remote-repository>
</component>
</project>

View File

@@ -2,7 +2,7 @@
Kotatsu is a free and open source manga reader for Android.
![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669)
![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5)
### Download

View File

@@ -8,13 +8,14 @@ plugins {
android {
compileSdkVersion 31
buildToolsVersion '30.0.3'
namespace 'org.koitharu.kotatsu'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 31
versionCode 370
versionName '2.0-b2'
versionCode 399
versionName '3.0-alpha1'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -24,10 +25,6 @@ android {
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
debug {
applicationIdSuffix = '.debug'
@@ -45,76 +42,79 @@ android {
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
lintOptions {
disable 'MissingTranslation'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [
'-Xjvm-default=enable',
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-Xopt-in=kotlinx.coroutines.FlowPreview',
'-Xopt-in=kotlin.contracts.ExperimentalContracts',
]
}
lint {
abortOnError false
disable 'MissingTranslation'
}
testOptions {
unitTests.includeAndroidResources = true
unitTests.returnDefaultValues = false
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-Xopt-in=kotlinx.coroutines.FlowPreview',
'-Xopt-in=org.koin.core.component.KoinApiExtension'
]
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'com.github.nv95:kotatsu-parsers:fe243c8acf'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.activity:activity-ktx:1.3.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-service:2.3.1'
implementation 'androidx.lifecycle:lifecycle-process:2.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-service:2.4.1'
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.7.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.material:material:1.6.0-alpha03'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.1'
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
implementation 'androidx.room:room-runtime:2.3.0'
implementation 'androidx.room:room-ktx:2.3.0'
kapt 'androidx.room:room-compiler:2.3.0'
implementation 'androidx.room:room-runtime:2.4.2'
implementation 'androidx.room:room-ktx:2.4.2'
kapt 'androidx.room:room-compiler:2.4.2'
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okio:okio:2.10.0'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.okio:okio:3.0.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'io.insert-koin:koin-android:3.1.2'
implementation 'io.insert-koin:koin-android:3.1.5'
implementation 'io.coil-kt:coil-base:1.4.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.solkin:disk-lru-cache:1.3'
implementation 'com.github.solkin:disk-lru-cache:1.4'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.google.truth:truth:1.1.3'
testImplementation 'org.json:json:20210307'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
testImplementation 'io.insert-koin:koin-test-junit4:3.1.2'
testImplementation 'org.json:json:20211205'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
testImplementation 'io.insert-koin:koin-test-junit4:3.1.5'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
androidTestImplementation 'androidx.room:room-testing:2.3.0'
androidTestImplementation 'com.google.truth:truth:1.1.3'
androidTestImplementation 'androidx.room:room-testing:2.4.2'
}

View File

@@ -1,3 +1,4 @@
-optimizationpasses 8
-dontobfuscate
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void checkExpressionValueIsNotNull(...);
@@ -5,9 +6,8 @@
public static void checkReturnedValueIsNotNull(...);
public static void checkFieldIsNotNull(...);
public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...);
}
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-keepclassmembers public class * extends org.koitharu.kotatsu.core.parser.MangaRepository {
public <init>(...);
}
-dontwarn okhttp3.internal.platform.ConscryptPlatform

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@id/action_leaks"
android:title="@string/leak_canary_display_activity_label"
app:showAsAction="never" />
</menu>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="use_light_status">true</bool>
<bool name="leak_canary_add_launcher_icon">false</bool>
</resources>

View File

@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.koitharu.kotatsu">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -20,7 +19,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:theme="@style/Theme.Kotatsu"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="UnusedAttribute">
<activity
@@ -32,7 +31,7 @@
</intent-filter>
<meta-data
android:name="android.app.default_searchable"
android:value=".ui.search.SearchActivity" />
android:value="org.koitharu.kotatsu.ui.search.SearchActivity" />
</activity>
<activity
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity"
@@ -51,6 +50,8 @@
<activity
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
android:label="@string/search" />
<activity android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
android:label="@string/search_manga" />
<activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:label="@string/settings" />
@@ -65,9 +66,11 @@
</activity>
<activity
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
@@ -94,11 +97,12 @@
android:noHistory="true"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".settings.protect.ProtectSetupActivity"
android:name="org.koitharu.kotatsu.settings.protect.ProtectSetupActivity"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
android:label="@string/downloads" />
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity"/>
<service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"

View File

@@ -3,14 +3,13 @@ package org.koitharu.kotatsu
import android.app.Application
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule
import org.koitharu.kotatsu.core.network.networkModule
import org.koitharu.kotatsu.core.parser.parserModule
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.AppCrashHandler
import org.koitharu.kotatsu.core.ui.uiModule
@@ -22,10 +21,12 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.localModule
import org.koitharu.kotatsu.main.mainModule
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.readerModule
import org.koitharu.kotatsu.remotelist.remoteListModule
import org.koitharu.kotatsu.search.searchModule
import org.koitharu.kotatsu.settings.settingsModule
import org.koitharu.kotatsu.suggestions.suggestionsModule
import org.koitharu.kotatsu.tracker.trackerModule
import org.koitharu.kotatsu.widget.WidgetUpdater
import org.koitharu.kotatsu.widget.appWidgetModule
@@ -54,7 +55,6 @@ class KotatsuApp : Application() {
databaseModule,
githubModule,
uiModule,
parserModule,
mainModule,
searchModule,
localModule,
@@ -65,7 +65,8 @@ class KotatsuApp : Application() {
trackerModule,
settingsModule,
readerModule,
appWidgetModule
appWidgetModule,
suggestionsModule,
)
}
}
@@ -86,5 +87,13 @@ class KotatsuApp : Application() {
.penaltyLog()
.build()
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
.detectWrongFragmentContainer()
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.detectFragmentTagUsage()
.build()
}
}

View File

@@ -5,8 +5,11 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.mapToSet
class MangaDataRepository(private val db: MangaDatabase) {
@@ -34,7 +37,7 @@ class MangaDataRepository(private val db: MangaDatabase) {
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga
intent.mangaId != MangaIntent.ID_NONE -> db.mangaDao.find(intent.mangaId)?.toManga()
intent.mangaId != 0L -> db.mangaDao.find(intent.mangaId)?.toManga()
else -> null // TODO resolve uri
}
@@ -45,4 +48,10 @@ class MangaDataRepository(private val db: MangaDatabase) {
db.mangaDao.upsert(MangaEntity.from(manga), tags)
}
}
suspend fun findTags(source: MangaSource): Set<MangaTag> {
return db.tagsDao.findTags(source.name).mapToSet {
it.toMangaTag()
}
}
}

View File

@@ -3,31 +3,32 @@ package org.koitharu.kotatsu.base.domain
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.parsers.model.Manga
data class MangaIntent(
class MangaIntent private constructor(
val manga: Manga?,
val mangaId: Long,
val uri: Uri?
val uri: Uri?,
) {
constructor(intent: Intent?) : this(
manga = intent?.getParcelableExtra<ParcelableManga>(KEY_MANGA)?.manga,
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
uri = intent?.data
)
constructor(args: Bundle?) : this(
manga = args?.getParcelable<ParcelableManga>(KEY_MANGA)?.manga,
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
uri = null
)
companion object {
fun from(intent: Intent?) = MangaIntent(
manga = intent?.getParcelableExtra(KEY_MANGA),
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
uri = intent?.data
)
fun from(args: Bundle?) = MangaIntent(
manga = args?.getParcelable(KEY_MANGA),
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
uri = null
)
const val ID_NONE = 0L
const val KEY_MANGA = "manga"
const val KEY_ID = "id"
}
}
}

View File

@@ -1,64 +0,0 @@
package org.koitharu.kotatsu.base.domain
import okhttp3.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.utils.ext.await
open class MangaLoaderContext(
private val okHttp: OkHttpClient,
val cookieJar: CookieJar
) : KoinComponent {
suspend fun httpGet(url: String, headers: Headers? = null): Response {
val request = Request.Builder()
.get()
.url(url)
if (headers != null) {
request.headers(headers)
}
return okHttp.newCall(request.build()).await()
}
suspend fun httpPost(
url: String,
form: Map<String, String>
): Response {
val body = FormBody.Builder()
form.forEach { (k, v) ->
body.addEncoded(k, v)
}
val request = Request.Builder()
.post(body.build())
.url(url)
return okHttp.newCall(request.build()).await()
}
suspend fun httpPost(
url: String,
payload: String
): Response {
val body = FormBody.Builder()
payload.split('&').forEach {
val pos = it.indexOf('=')
if (pos != -1) {
val k = it.substring(0, pos)
val v = it.substring(pos + 1)
body.addEncoded(k, v)
}
}
val request = Request.Builder()
.post(body.build())
.url(url)
return okHttp.newCall(request.build()).await()
}
open fun getSettings(source: MangaSource) = SourceSettings(get(), source)
private companion object {
private const val SCHEME_HTTP = "http"
}
}

View File

@@ -1,24 +0,0 @@
package org.koitharu.kotatsu.base.domain
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
object MangaProviderFactory {
fun getSources(settings: AppSettings, includeHidden: Boolean): List<MangaSource> {
val list = MangaSource.values().toList() - MangaSource.LOCAL
val order = settings.sourcesOrder
val hidden = settings.hiddenSources
val sorted = list.sortedBy { x ->
val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
}
return if (includeHidden) {
sorted
} else {
sorted.filterNot { x ->
x.name in hidden
}
}
}
}

View File

@@ -3,16 +3,17 @@ package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.medianOrNull
import java.io.InputStream
import java.util.zip.ZipFile
@@ -23,29 +24,30 @@ object MangaUtils : KoinComponent {
* Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide
*/
@WorkerThread
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
try {
val page = pages.medianOrNull() ?: return null
val url = page.source.repository.getPageUrl(page)
val url = MangaRepository(page.source).getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
}
} else {
val client = get<OkHttpClient>()
val request = Request.Builder()
.url(url)
.get()
.header(CommonHeaders.REFERER, page.referer)
.cacheControl(CacheUtils.CONTROL_DISABLED)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.build()
client.newCall(request).await().use {
getBitmapSize(it.body?.byteStream())
get<OkHttpClient>().newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
}
return size.width * 2 < size.height
@@ -61,10 +63,10 @@ object MangaUtils : KoinComponent {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)
BitmapFactory.decodeStream(input, null, options)?.recycle()
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}
}

View File

@@ -5,9 +5,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
@@ -17,10 +17,9 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
get() = checkNotNull(viewBinding)
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext())
val binding = onInflateView(inflater, null)
val binding = onInflateView(layoutInflater, null)
viewBinding = binding
return AlertDialog.Builder(requireContext(), theme)
return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(binding.root)
.also(::onBuildDialog)
.create()
@@ -38,9 +37,9 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
super.onDestroyView()
}
open fun onBuildDialog(builder: AlertDialog.Builder) = Unit
open fun onBuildDialog(builder: MaterialAlertDialogBuilder) = Unit
protected fun bindingOrNull(): B? = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}
}

View File

@@ -12,34 +12,39 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.graphics.Insets
import androidx.core.view.*
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindowInsetsListener {
abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(),
WindowInsetsDelegate.WindowInsetsListener {
protected lateinit var binding: B
private set
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
ExceptionResolver(this, supportFragmentManager)
}
@Suppress("LeakingThis")
protected val exceptionResolver = ExceptionResolver(this)
private var lastInsets: Insets = Insets.NONE
@Suppress("LeakingThis")
protected val insetsDelegate = WindowInsetsDelegate(this)
override fun onCreate(savedInstanceState: Bundle?) {
if (get<AppSettings>().isAmoledTheme) {
setTheme(R.style.AppTheme_AMOLED)
val settings = get<AppSettings>()
when {
settings.isAmoledTheme -> setTheme(R.style.ThemeOverlay_Kotatsu_AMOLED)
settings.isDynamicTheme -> setTheme(R.style.Theme_Kotatsu_Monet)
}
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
insetsDelegate.handleImeInsets = true
}
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
@@ -59,28 +64,7 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
super.setContentView(binding.root)
val toolbar = (binding.root.findViewById<View>(R.id.toolbar) as? Toolbar)
toolbar?.let(this::setSupportActionBar)
ViewCompat.setOnApplyWindowInsetsListener(binding.root, this)
val toolbarParams = (binding.root.findViewById<View>(R.id.toolbar_card) ?: toolbar)
?.layoutParams as? AppBarLayout.LayoutParams
if (toolbarParams != null) {
if (get<AppSettings>().isToolbarHideWhenScrolling) {
toolbarParams.scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP
} else {
toolbarParams.scrollFlags = SCROLL_FLAG_NO_SCROLL
}
}
}
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
val baseInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val newInsets = Insets.max(baseInsets, imeInsets)
if (newInsets != lastInsets) {
onWindowInsetsChanged(newInsets)
lastInsets = newInsets
}
return insets
insetsDelegate.onViewCreated(binding.root)
}
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
@@ -96,8 +80,6 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
return super.onKeyDown(keyCode, event)
}
protected abstract fun onWindowInsetsChanged(insets: Insets)
private fun setupToolbar() {
(findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
}
@@ -129,4 +111,4 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
super.onBackPressed()
}
}
}
}

View File

@@ -5,19 +5,26 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.appcompat.app.AppCompatDialog
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R
import com.google.android.material.R as materialR
abstract class BaseBottomSheet<B : ViewBinding> :
BottomSheetDialogFragment() {
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
private var viewBinding: B? = null
protected val binding: B
get() = checkNotNull(viewBinding)
protected val behavior: BottomSheetBehavior<*>?
get() = (dialog as? BottomSheetDialog)?.behavior
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -35,9 +42,22 @@ abstract class BaseBottomSheet<B : ViewBinding> :
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (resources.getBoolean(R.bool.is_tablet)) {
AppCompatDialog(context, theme)
AppCompatDialog(context, R.style.Theme_Kotatsu_Dialog)
} else super.onCreateDialog(savedInstanceState)
}
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
val b = behavior ?: return
if (isExpanded) {
b.state = BottomSheetBehavior.STATE_EXPANDED
}
b.isFitToContents = !isExpanded
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
rootView?.updateLayoutParams {
height = if (isExpanded) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
}
b.isDraggable = !isLocked
}
}

View File

@@ -1,30 +1,27 @@
package org.koitharu.kotatsu.base.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsListener {
abstract class BaseFragment<B : ViewBinding> : Fragment(),
WindowInsetsDelegate.WindowInsetsListener {
private var viewBinding: B? = null
protected val binding: B
get() = checkNotNull(viewBinding)
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
ExceptionResolver(viewLifecycleOwner, childFragmentManager)
}
@Suppress("LeakingThis")
protected val exceptionResolver = ExceptionResolver(this)
private var lastInsets: Insets = Insets.NONE
@Suppress("LeakingThis")
protected val insetsDelegate = WindowInsetsDelegate(this)
override fun onCreateView(
inflater: LayoutInflater,
@@ -38,36 +35,16 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsLi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lastInsets = Insets.NONE
ViewCompat.setOnApplyWindowInsetsListener(view, this)
insetsDelegate.onViewCreated(view)
}
override fun onDestroyView() {
viewBinding = null
insetsDelegate.onDestroyView()
super.onDestroyView()
}
open fun getTitle(): CharSequence? = null
override fun onAttach(context: Context) {
super.onAttach(context)
getTitle()?.let {
activity?.title = it
}
}
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
val newInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
if (newInsets != lastInsets) {
onWindowInsetsChanged(newInsets)
lastInsets = newInsets
}
return insets
}
protected fun bindingOrNull() = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
protected abstract fun onWindowInsetsChanged(insets: Insets)
}

View File

@@ -7,6 +7,16 @@ import android.view.View
import android.view.WindowManager
import androidx.viewbinding.ViewBinding
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
View.OnSystemUiVisibilityChangeListener {
@@ -25,6 +35,7 @@ abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
showSystemUI()
}
@Deprecated("Deprecated in Java")
final override fun onSystemUiVisibilityChange(visibility: Int) {
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
}
@@ -39,19 +50,4 @@ abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
}
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
@Suppress("DEPRECATION")
private companion object {
const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
}
}

View File

@@ -2,38 +2,53 @@ package org.koitharu.kotatsu.base.ui
import android.os.Bundle
import android.view.View
import androidx.annotation.CallSuper
import androidx.annotation.StringRes
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), OnApplyWindowInsetsListener {
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : PreferenceFragmentCompat(),
WindowInsetsDelegate.WindowInsetsListener,
RecyclerViewOwner {
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
@Suppress("LeakingThis")
protected val insetsDelegate = WindowInsetsDelegate(this)
override val recyclerView: RecyclerView
get() = listView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listView.clipToPadding = false
ViewCompat.setOnApplyWindowInsetsListener(view, this)
insetsDelegate.onViewCreated(view)
}
override fun onDestroyView() {
insetsDelegate.onDestroyView()
super.onDestroyView()
}
override fun onResume() {
super.onResume()
activity?.setTitle(titleId)
if (titleId != 0) {
activity?.setTitle(titleId)
}
}
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
@CallSuper
override fun onWindowInsetsChanged(insets: Insets) {
listView.updatePadding(
left = systemBars.left,
right = systemBars.right,
bottom = systemBars.bottom
left = insets.left,
right = insets.right,
bottom = insets.bottom
)
return insets
}
}
}

View File

@@ -33,7 +33,7 @@ abstract class BaseViewModel : ViewModel() {
}
}
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
protected fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
if (BuildConfig.DEBUG) {
throwable.printStackTrace()
}

View File

@@ -6,6 +6,7 @@ import android.view.LayoutInflater
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
@@ -17,7 +18,7 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
private val binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
private val delegate = AlertDialog.Builder(context)
private val delegate = MaterialAlertDialogBuilder(context)
.setView(binding.root)
fun setTitle(@StringRes titleResId: Int): Builder {

View File

@@ -8,12 +8,11 @@ import android.widget.BaseAdapter
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.utils.ext.inflate
import org.koitharu.kotatsu.utils.ext.longHashCode
import java.io.File
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
@@ -21,19 +20,22 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
fun show() = delegate.show()
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
class Builder(context: Context, storageManager: LocalStorageManager, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(context)
private val delegate = AlertDialog.Builder(context)
private val adapter = VolumesAdapter(storageManager)
private val delegate = MaterialAlertDialogBuilder(context)
init {
if (adapter.isEmpty) {
delegate.setMessage(R.string.cannot_find_available_storage)
} else {
val checked = adapter.volumes.indexOfFirst {
val defaultValue = runBlocking {
storageManager.getDefaultWriteableDir()
}
adapter.selectedItemPosition = adapter.volumes.indexOfFirst {
it.first.canonicalPath == defaultValue?.canonicalPath
}
delegate.setSingleChoiceItems(adapter, checked) { d, i ->
delegate.setAdapter(adapter) { d, i ->
listener.onStorageSelected(adapter.getItem(i).first)
d.dismiss()
}
@@ -58,14 +60,18 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
fun create() = StorageSelectDialog(delegate.create())
}
private class VolumesAdapter(context: Context) : BaseAdapter() {
private class VolumesAdapter(storageManager: LocalStorageManager) : BaseAdapter() {
val volumes = getAvailableVolumes(context)
var selectedItemPosition: Int = -1
val volumes = getAvailableVolumes(storageManager)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: parent.inflate(R.layout.item_storage)
val binding = (view.tag as? ItemStorageBinding) ?: ItemStorageBinding.bind(view).also {
view.tag = it
}
val item = volumes[position]
val binding = ItemStorageBinding.bind(view)
binding.imageViewIndicator.isChecked = selectedItemPosition == position
binding.textViewTitle.text = item.second
binding.textViewSubtitle.text = item.first.path
return view
@@ -73,23 +79,23 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
override fun getItem(position: Int): Pair<File, String> = volumes[position]
override fun getItemId(position: Int) = volumes[position].first.absolutePath.longHashCode()
override fun getItemId(position: Int) = position.toLong()
override fun getCount() = volumes.size
override fun hasStableIds() = true
private fun getAvailableVolumes(storageManager: LocalStorageManager): List<Pair<File, String>> {
return runBlocking {
storageManager.getWriteableDirs().map {
it to storageManager.getStorageDisplayName(it)
}
}
}
}
fun interface OnStorageSelectListener {
fun onStorageSelected(file: File)
}
private companion object {
fun getAvailableVolumes(context: Context): List<Pair<File, String>> {
return LocalMangaRepository.getAvailableStorageDirs(context).map {
it to it.getStorageName(context)
}
}
}
}

View File

@@ -10,7 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.databinding.DialogInputBinding
class TextInputDialog private constructor(
private val delegate: AlertDialog
private val delegate: AlertDialog,
) : DialogInterface by delegate {
fun show() = delegate.show()
@@ -19,7 +19,7 @@ class TextInputDialog private constructor(
private val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
private val delegate = AlertDialog.Builder(context)
private val delegate = MaterialAlertDialogBuilder(context)
.setView(binding.root)
fun setTitle(@StringRes titleResId: Int): Builder {
@@ -33,7 +33,7 @@ class TextInputDialog private constructor(
}
fun setHint(@StringRes hintResId: Int): Builder {
binding.inputLayout.hint = binding.root.context.getString(hintResId)
binding.inputEdit.hint = binding.root.context.getString(hintResId)
return this
}
@@ -64,7 +64,7 @@ class TextInputDialog private constructor(
listener: (DialogInterface, String) -> Unit
): Builder {
delegate.setPositiveButton(textId) { dialog, _ ->
listener(dialog, binding.inputEdit.text.toString().orEmpty())
listener(dialog, binding.inputEdit.text?.toString().orEmpty())
}
return this
}

View File

@@ -1,26 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import java.util.*
@Deprecated("")
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
getId(oldList[oldItemPosition]) == getId(newList[newItemPosition])
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
Objects.equals(oldList[oldItemPosition], newList[newItemPosition])
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
})
operator fun invoke(adapter: RecyclerView.Adapter<*>) {
diff.dispatchUpdatesTo(adapter)
}
}

View File

@@ -1,28 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import org.koin.core.component.KoinComponent
@Deprecated("")
abstract class BaseViewHolder<T, E, B : ViewBinding> protected constructor(val binding: B) :
RecyclerView.ViewHolder(binding.root), KoinComponent {
var boundData: T? = null
private set
val context get() = itemView.context!!
fun bind(data: T, extra: E) {
boundData = data
onBind(data, extra)
}
fun requireData(): T {
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
}
open fun onRecycled() = Unit
abstract fun onBind(data: T, extra: E)
}

View File

@@ -0,0 +1,37 @@
package org.koitharu.kotatsu.base.ui.list
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
class FitHeightGridLayoutManager : GridLayoutManager {
constructor(context: Context?, spanCount: Int) : super(context, spanCount)
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int,
) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(
context: Context?,
spanCount: Int,
orientation: Int,
reverseLayout: Boolean,
) : super(context, spanCount, orientation, reverseLayout)
override fun layoutDecoratedWithMargins(child: View, left: Int, top: Int, right: Int, bottom: Int) {
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
val parentBottom = height - paddingBottom
val offset = parentBottom - bottom
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
} else {
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
}
}
}

View File

@@ -0,0 +1,37 @@
package org.koitharu.kotatsu.base.ui.list
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutParams
class FitHeightLinearLayoutManager : LinearLayoutManager {
constructor(context: Context) : super(context)
constructor(
context: Context,
@RecyclerView.Orientation orientation: Int,
reverseLayout: Boolean,
) : super(context, orientation, reverseLayout)
constructor(
context: Context,
attrs: AttributeSet?,
@AttrRes defStyleAttr: Int,
@StyleRes defStyleRes: Int,
) : super(context, attrs, defStyleAttr, defStyleRes)
override fun layoutDecoratedWithMargins(child: View, left: Int, top: Int, right: Int, bottom: Int) {
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
val parentBottom = height - paddingBottom
val offset = parentBottom - bottom
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
} else {
super.layoutDecoratedWithMargins(child, left, top, right, bottom)
}
}
}

View File

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

View File

@@ -1,58 +0,0 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.utils.ext.getThemeDrawable
import kotlin.math.roundToInt
class ItemTypeDividerDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val divider = context.getThemeDrawable(android.R.attr.listDivider)
private val bounds = Rect()
override fun getItemOffsets(
outRect: Rect, view: View,
parent: RecyclerView, state: RecyclerView.State
) {
outRect.set(0, divider?.intrinsicHeight ?: 0, 0, 0)
}
override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) {
if (parent.layoutManager == null || divider == null) {
return
}
val adapter = parent.adapter ?: return
canvas.save()
val left: Int
val right: Int
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
canvas.clipRect(
left, parent.paddingTop, right,
parent.height - parent.paddingBottom
)
} else {
left = 0
right = parent.width
}
var lastItemType = -1
for (child in parent.children) {
val itemType = adapter.getItemViewType(parent.getChildAdapterPosition(child))
if (lastItemType != -1 && itemType != lastItemType) {
parent.getDecoratedBoundsWithMargins(child, bounds)
val top: Int = bounds.top + child.translationY.roundToInt()
val bottom: Int = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(canvas)
}
lastItemType = itemType
}
canvas.restore()
}
}

View File

@@ -1,96 +0,0 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.inflate
import kotlin.math.max
/**
* https://github.com/paetztm/recycler_view_headers
*/
class SectionItemDecoration(
private val isSticky: Boolean,
private val callback: Callback
) : RecyclerView.ItemDecoration() {
private var headerView: TextView? = null
private var headerOffset: Int = 0
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (headerOffset == 0) {
headerOffset = parent.resources.getDimensionPixelSize(R.dimen.header_height)
}
val pos = parent.getChildAdapterPosition(view)
outRect.set(0, if (callback.isSection(pos)) headerOffset else 0, 0, 0)
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val textView = headerView ?: parent.inflate<TextView>(R.layout.item_filter_header).also {
headerView = it
}
fixLayoutSize(textView, parent)
for (child in parent.children) {
val pos = parent.getChildAdapterPosition(child)
if (callback.isSection(pos)) {
textView.text = callback.getSectionTitle(pos) ?: continue
c.save()
if (isSticky) {
c.translate(
0f,
max(0f, (child.top - textView.height).toFloat())
)
} else {
c.translate(
0f,
(child.top - textView.height).toFloat()
)
}
textView.draw(c)
c.restore()
}
}
}
/**
* Measures the header view to make sure its size is greater than 0 and will be drawn
* https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations
*/
private fun fixLayoutSize(view: View, parent: ViewGroup) {
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec =
View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidth = ViewGroup.getChildMeasureSpec(
widthSpec,
parent.paddingLeft + parent.paddingRight,
view.layoutParams.width
)
val childHeight = ViewGroup.getChildMeasureSpec(
heightSpec,
parent.paddingTop + parent.paddingBottom,
view.layoutParams.height
)
view.measure(childWidth, childHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
interface Callback {
fun isSection(position: Int): Boolean
fun getSectionTitle(position: Int): CharSequence?
}
}

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.base.ui.util
import androidx.recyclerview.widget.RecyclerView
interface RecyclerViewOwner {
val recyclerView: RecyclerView
}

View File

@@ -0,0 +1,69 @@
package org.koitharu.kotatsu.base.ui.util
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class WindowInsetsDelegate(
private val listener: WindowInsetsListener,
) : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
var handleImeInsets: Boolean = false
var interceptingWindowInsetsListener: OnApplyWindowInsetsListener? = null
private var lastInsets: Insets? = null
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat?): WindowInsetsCompat? {
if (insets == null) {
return null
}
val handledInsets = interceptingWindowInsetsListener?.onApplyWindowInsets(v, insets) ?: insets
val newInsets = if (handleImeInsets) {
Insets.max(
handledInsets.getInsets(WindowInsetsCompat.Type.systemBars()),
handledInsets.getInsets(WindowInsetsCompat.Type.ime()),
)
} else {
handledInsets.getInsets(WindowInsetsCompat.Type.systemBars())
}
if (newInsets != lastInsets) {
listener.onWindowInsetsChanged(newInsets)
lastInsets = newInsets
}
return handledInsets
}
override fun onLayoutChange(
view: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int,
) {
view.removeOnLayoutChangeListener(this)
if (lastInsets == null) { // Listener may not be called
onApplyWindowInsets(view, ViewCompat.getRootWindowInsets(view))
}
}
fun onViewCreated(view: View) {
ViewCompat.setOnApplyWindowInsetsListener(view, this)
view.addOnLayoutChangeListener(this)
}
fun onDestroyView() {
lastInsets = null
}
interface WindowInsetsListener {
fun onWindowInsetsChanged(insets: Insets)
}
}

View File

@@ -1,41 +0,0 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isGone
import com.google.android.material.R
import com.google.android.material.appbar.MaterialToolbar
import java.lang.reflect.Field
class AnimatedToolbar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.toolbarStyle,
) : MaterialToolbar(context, attrs, defStyleAttr) {
private var navButtonView: View? = null
get() {
if (field == null) {
runCatching {
field = navButtonViewField?.get(this) as? View
}
}
return field
}
override fun setNavigationIcon(icon: Drawable?) {
super.setNavigationIcon(icon)
navButtonView?.isGone = (icon == null)
}
private companion object {
val navButtonViewField: Field? = runCatching {
Toolbar::class.java.getDeclaredField("mNavButtonView")
.also { it.isAccessible = true }
}.getOrNull()
}
}

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.AttrRes
import androidx.annotation.IdRes
import androidx.core.view.children
import com.google.android.material.button.MaterialButton
class CheckableButtonGroup @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr), View.OnClickListener {
var onCheckedChangeListener: OnCheckedChangeListener? = null
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
if (child is MaterialButton) {
child.setOnClickListener(this)
}
super.addView(child, index, params)
}
override fun onClick(v: View) {
setCheckedId(v.id)
}
fun setCheckedId(@IdRes viewRes: Int) {
children.forEach {
(it as? MaterialButton)?.isChecked = it.id == viewRes
}
onCheckedChangeListener?.onCheckedChanged(this, viewRes)
}
fun interface OnCheckedChangeListener {
fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int)
}
}

View File

@@ -1,12 +1,19 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.util.AttributeSet
import android.widget.Checkable
import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.os.ParcelCompat
class CheckableImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : AppCompatImageView(context, attrs, defStyleAttr), Checkable {
private var isCheckedInternal = false
@@ -14,20 +21,6 @@ class CheckableImageView @JvmOverloads constructor(
var onCheckedChangeListener: OnCheckedChangeListener? = null
init {
setOnClickListener {
toggle()
}
}
fun setOnCheckedChangeListener(listener: (Boolean) -> Unit) {
onCheckedChangeListener = object : OnCheckedChangeListener {
override fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean) {
listener(isChecked)
}
}
}
override fun isChecked() = isCheckedInternal
override fun toggle() {
@@ -49,18 +42,54 @@ class CheckableImageView @JvmOverloads constructor(
override fun onCreateDrawableState(extraSpace: Int): IntArray {
val state = super.onCreateDrawableState(extraSpace + 1)
if (isCheckedInternal) {
mergeDrawableStates(state, CHECKED_STATE_SET)
mergeDrawableStates(state, intArrayOf(android.R.attr.state_checked))
}
return state
}
override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState() ?: return null
return SavedState(superState, isChecked)
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is SavedState) {
super.onRestoreInstanceState(state.superState)
isChecked = state.isChecked
} else {
super.onRestoreInstanceState(state)
}
}
fun interface OnCheckedChangeListener {
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)
}
private companion object {
private class SavedState : BaseSavedState {
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
val isChecked: Boolean
constructor(superState: Parcelable, checked: Boolean) : super(superState) {
isChecked = checked
}
constructor(source: Parcel) : super(source) {
isChecked = ParcelCompat.readBoolean(source)
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
ParcelCompat.writeBoolean(out, isChecked)
}
companion object {
@JvmField
val CREATOR: Creator<SavedState> = object : Creator<SavedState> {
override fun createFromParcel(`in`: Parcel) = SavedState(`in`)
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
}
}
}
}

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.util.AttributeSet
import android.view.View.OnClickListener
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.view.children
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
@@ -77,11 +76,11 @@ class ChipsView @JvmOverloads constructor(
val chip = Chip(context)
val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip)
chip.setChipDrawable(drawable)
chip.setTextColor(ContextCompat.getColor(context, R.color.color_primary))
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener)
chip.isCheckable = false
addView(chip)
return chip
}
@@ -96,11 +95,32 @@ class ChipsView @JvmOverloads constructor(
}
}
data class ChipModel(
class ChipModel(
@DrawableRes val icon: Int,
val title: CharSequence,
val data: Any? = null
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ChipModel
if (icon != other.icon) return false
if (title != other.title) return false
if (data != other.data) return false
return true
}
override fun hashCode(): Int {
var result = icon
result = 31 * result + title.hashCode()
result = 31 * result + data.hashCode()
return result
}
}
fun interface OnChipClickListener {

View File

@@ -2,70 +2,42 @@ package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatImageView
import android.widget.LinearLayout.HORIZONTAL
import android.widget.LinearLayout.VERTICAL
import androidx.annotation.AttrRes
import androidx.core.content.withStyledAttributes
import com.google.android.material.imageview.ShapeableImageView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.resolveAdjustedSize
import kotlin.math.roundToInt
private const val ASPECT_RATIO_HEIGHT = 18f
private const val ASPECT_RATIO_WIDTH = 13f
class CoverImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,
) : AppCompatImageView(context, attrs, defStyleAttr) {
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : ShapeableImageView(context, attrs, defStyleAttr) {
private var orientation: Int = HORIZONTAL
init {
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
orientation = getInt(R.styleable.CoverImageView_android_orientation, HORIZONTAL)
orientation = getInt(R.styleable.CoverImageView_android_orientation, orientation)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val w: Int
val h: Int
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredWidth: Int
val desiredHeight: Int
if (orientation == VERTICAL) {
val desiredHeight = (drawable?.intrinsicHeight?.coerceAtLeast(0) ?: 0) +
paddingTop + paddingBottom
h = resolveAdjustedSize(
desiredHeight.coerceAtLeast(suggestedMinimumHeight),
maxHeight,
heightMeasureSpec
)
val desiredWidth =
(h * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).toInt() + paddingLeft + paddingRight
w = resolveAdjustedSize(
desiredWidth.coerceAtLeast(suggestedMinimumWidth),
maxWidth,
widthMeasureSpec
)
desiredHeight = measuredHeight
desiredWidth = (desiredHeight * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt()
} else {
val desiredWidth = (drawable?.intrinsicWidth?.coerceAtLeast(0) ?: 0) +
paddingLeft + paddingRight
w = resolveAdjustedSize(
desiredWidth.coerceAtLeast(suggestedMinimumWidth),
maxWidth,
widthMeasureSpec
)
val desiredHeight =
(w * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).toInt() + paddingTop + paddingBottom
h = resolveAdjustedSize(
desiredHeight.coerceAtLeast(suggestedMinimumHeight),
maxHeight,
heightMeasureSpec
)
desiredWidth = measuredWidth
desiredHeight = (desiredWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt()
}
val widthSize = resolveSizeAndState(w, widthMeasureSpec, 0)
val heightSize = resolveSizeAndState(h, heightMeasureSpec, 0)
setMeasuredDimension(widthSize, heightSize)
}
companion object {
const val VERTICAL = LinearLayout.VERTICAL
const val HORIZONTAL = LinearLayout.HORIZONTAL
private const val ASPECT_RATIO_HEIGHT = 18f
private const val ASPECT_RATIO_WIDTH = 13f
setMeasuredDimension(desiredWidth, desiredHeight)
}
}

View File

@@ -26,6 +26,10 @@ import androidx.annotation.StringRes
import androidx.core.view.postDelayed
import org.koitharu.kotatsu.R
private const val ENTER_DURATION = 300L
private const val EXIT_DURATION = 200L
private const val SHORT_DURATION = 1_500L
private const val LONG_DURATION = 2_750L
/**
* A custom snackbar implementation allowing more control over placement and entry/exit animations.
*
@@ -87,11 +91,4 @@ class FadingSnackbar @JvmOverloads constructor(
dismissListener()
}
}
companion object {
private const val ENTER_DURATION = 300L
private const val EXIT_DURATION = 200L
private const val SHORT_DURATION = 1_500L
private const val LONG_DURATION = 2_750L
}
}

View File

@@ -0,0 +1,120 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatCheckedTextView
import androidx.core.content.res.use
import androidx.core.content.withStyledAttributes
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import org.koitharu.kotatsu.R
class ListItemTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = R.attr.listItemTextViewStyle,
) : AppCompatCheckedTextView(context, attrs, defStyleAttr) {
private var checkedDrawableStart: Drawable? = null
private var checkedDrawableEnd: Drawable? = null
private var isInitialized = false
private var isCheckDrawablesVisible: Boolean = false
private var defaultPaddingStart: Int = 0
private var defaultPaddingEnd: Int = 0
init {
context.withStyledAttributes(attrs, R.styleable.ListItemTextView, defStyleAttr) {
background = RippleDrawable(
getColorStateList(R.styleable.ListItemTextView_rippleColor) ?: getRippleColorFallback(context),
createShapeDrawable(this),
ShapeDrawable(RectShape()),
)
checkedDrawableStart = getDrawable(R.styleable.ListItemTextView_checkedDrawableStart)
checkedDrawableEnd = getDrawable(R.styleable.ListItemTextView_checkedDrawableEnd)
}
checkedDrawableStart?.setTintList(textColors)
checkedDrawableEnd?.setTintList(textColors)
defaultPaddingStart = paddingStart
defaultPaddingEnd = paddingEnd
isInitialized = true
adjustCheckDrawables()
}
override fun refreshDrawableState() {
super.refreshDrawableState()
adjustCheckDrawables()
}
override fun setTextColor(colors: ColorStateList?) {
checkedDrawableStart?.setTintList(colors)
checkedDrawableEnd?.setTintList(colors)
super.setTextColor(colors)
}
override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
defaultPaddingStart = start
defaultPaddingEnd = end
super.setPaddingRelative(start, top, end, bottom)
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
defaultPaddingStart = if (isRtl) right else left
defaultPaddingEnd = if (isRtl) left else right
super.setPadding(left, top, right, bottom)
}
private fun adjustCheckDrawables() {
if (isInitialized && isCheckDrawablesVisible != isChecked) {
setCompoundDrawablesRelativeWithIntrinsicBounds(
if (isChecked) checkedDrawableStart else null,
null,
if (isChecked) checkedDrawableEnd else null,
null,
)
super.setPaddingRelative(
if (isChecked && checkedDrawableStart != null) {
defaultPaddingStart + compoundDrawablePadding
} else defaultPaddingStart,
paddingTop,
if (isChecked && checkedDrawableEnd != null) {
defaultPaddingEnd + compoundDrawablePadding
} else defaultPaddingEnd,
paddingBottom,
)
isCheckDrawablesVisible = isChecked
}
}
private fun createShapeDrawable(ta: TypedArray): InsetDrawable {
val shapeAppearance = ShapeAppearanceModel.builder(
context,
ta.getResourceId(R.styleable.ListItemTextView_shapeAppearance, 0),
ta.getResourceId(R.styleable.ListItemTextView_shapeAppearanceOverlay, 0),
).build()
val shapeDrawable = MaterialShapeDrawable(shapeAppearance)
shapeDrawable.fillColor = ta.getColorStateList(R.styleable.ListItemTextView_backgroundTint)
return InsetDrawable(
shapeDrawable,
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetLeft, 0),
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetTop, 0),
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetRight, 0),
ta.getDimensionPixelOffset(R.styleable.ListItemTextView_android_insetBottom, 0),
)
}
private fun getRippleColorFallback(context: Context): ColorStateList {
return context.obtainStyledAttributes(intArrayOf(android.R.attr.colorControlHighlight)).use {
it.getColorStateList(0)
} ?: ColorStateList.valueOf(Color.TRANSPARENT)
}
}

View File

@@ -14,6 +14,7 @@ import androidx.core.view.updatePadding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled")
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
@@ -23,12 +24,16 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_cross)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
with(binding.webView.settings) {
javaScriptEnabled = true
}
binding.webView.webViewClient = BrowserClient(this)
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
if (savedInstanceState != null) {
return
}
val url = intent?.dataString
if (url.isNullOrEmpty()) {
finishAfterTransition()
@@ -41,6 +46,16 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
binding.webView.saveState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
binding.webView.restoreState(savedInstanceState)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_browser, menu)
return super.onCreateOptionsMenu(menu)
@@ -82,6 +97,11 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
binding.webView.onResume()
}
override fun onDestroy() {
super.onDestroy()
binding.webView.destroy()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
binding.progressBar.isVisible = isLoading
}
@@ -92,8 +112,16 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.appbar.updatePadding(top = insets.top)
binding.webView.updatePadding(bottom = insets.bottom)
binding.appbar.updatePadding(
top = insets.top,
left = insets.left,
right = insets.right,
)
binding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
)
}
companion object {

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.browser
import android.webkit.WebChromeClient
import android.webkit.WebView
import androidx.core.view.isVisible
import com.google.android.material.progressindicator.BaseProgressIndicator
import org.koitharu.kotatsu.utils.ext.setIndeterminateCompat
private const val PROGRESS_MAX = 100
class ProgressChromeClient(
private val progressIndicator: BaseProgressIndicator<*>,
) : WebChromeClient() {
init {
progressIndicator.max = PROGRESS_MAX
}
override fun onProgressChanged(view: WebView?, newProgress: Int) {
super.onProgressChanged(view, newProgress)
if (!progressIndicator.isVisible) {
return
}
if (newProgress in 1 until PROGRESS_MAX) {
progressIndicator.setIndeterminateCompat(false)
progressIndicator.setProgressCompat(newProgress.coerceAtMost(PROGRESS_MAX), true)
} else {
progressIndicator.setIndeterminateCompat(true)
}
}
}

View File

@@ -6,6 +6,8 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.network.WebViewClientCompat
private const val CF_CLEARANCE = "cf_clearance"
class CloudFlareClient(
private val cookieJar: AndroidCookieJar,
private val callback: CloudFlareCallback,
@@ -40,9 +42,4 @@ class CloudFlareClient(
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
.find { it.name == name }?.value
}
private companion object {
const val CF_CLEARANCE = "cf_clearance"
}
}

View File

@@ -8,9 +8,9 @@ import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isInvisible
import androidx.fragment.app.setFragmentResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
@@ -52,7 +52,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
super.onDestroyView()
}
override fun onBuildDialog(builder: AlertDialog.Builder) {
override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
builder.setNegativeButton(android.R.string.cancel, null)
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.backup
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.koitharu.kotatsu.R
@@ -33,8 +34,7 @@ class BackupArchive(file: File) : MutableZipFile(file) {
private const val DIR_BACKUPS = "backups"
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun createNew(context: Context): BackupArchive = withContext(Dispatchers.IO) {
suspend fun createNew(context: Context): BackupArchive = runInterruptible(Dispatchers.IO) {
val dir = context.run {
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
}

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.backup
import org.json.JSONArray
data class BackupEntry(
class BackupEntry(
val name: String,
val data: JSONArray
) {

View File

@@ -10,6 +10,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
private const val PAGE_SIZE = 10
class BackupRepository(private val db: MangaDatabase) {
suspend fun dumpHistory(): BackupEntry {
@@ -65,7 +67,7 @@ class BackupRepository(private val db: MangaDatabase) {
return entry
}
suspend fun createIndex(): BackupEntry {
fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
val json = JSONObject()
json.put("app_id", BuildConfig.APPLICATION_ID)
@@ -129,9 +131,4 @@ class BackupRepository(private val db: MangaDatabase) {
jo.put("created_at", createdAt)
return jo
}
private companion object {
const val PAGE_SIZE = 10
}
}

View File

@@ -5,23 +5,23 @@ import org.json.JSONObject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.utils.ext.getBooleanOrDefault
import org.koitharu.kotatsu.utils.ext.getStringOrNull
import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.map
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.json.mapJSON
class RestoreRepository(private val db: MangaDatabase) {
suspend fun upsertHistory(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data) {
for (item in entry.data.JSONIterator()) {
val mangaJson = item.getJSONObject("manga")
val manga = parseManga(mangaJson)
val tags = mangaJson.getJSONArray("tags").map {
val tags = mangaJson.getJSONArray("tags").mapJSON {
parseTag(it)
}
val history = parseHistory(item)
@@ -38,7 +38,7 @@ class RestoreRepository(private val db: MangaDatabase) {
suspend fun upsertCategories(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data) {
for (item in entry.data.JSONIterator()) {
val category = parseCategory(item)
result += runCatching {
db.favouriteCategoriesDao.upsert(category)
@@ -49,10 +49,10 @@ class RestoreRepository(private val db: MangaDatabase) {
suspend fun upsertFavourites(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data) {
for (item in entry.data.JSONIterator()) {
val mangaJson = item.getJSONObject("manga")
val manga = parseManga(mangaJson)
val tags = mangaJson.getJSONArray("tags").map {
val tags = mangaJson.getJSONArray("tags").mapJSON {
parseTag(it)
}
val favourite = parseFavourite(item)

View File

@@ -4,7 +4,7 @@ import android.content.res.Resources
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.parsers.model.SortOrder
class DatabasePrePopulateCallback(private val resources: Resources) : RoomDatabase.Callback() {

View File

@@ -10,6 +10,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouritesDao
import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.suggestions.data.SuggestionDao
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
@Database(
entities = [
@@ -35,4 +37,6 @@ abstract class MangaDatabase : RoomDatabase() {
abstract val tracksDao: TracksDao
abstract val trackLogsDao: TrackLogsDao
abstract val suggestionDao: SuggestionDao
}

View File

@@ -6,8 +6,47 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
abstract class TagsDao {
@Query("SELECT * FROM tags")
abstract suspend fun getAllTags(): List<TagEntity>
@Query("SELECT * FROM tags WHERE source = :source")
abstract suspend fun findTags(source: String): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findPopularTags(source: String, limit: Int): List<TagEntity>
@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
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findTags(source: String, query: String, limit: Int): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE title LIKE :query
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findTags(query: String, limit: Int): List<TagEntity>
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(tag: TagEntity): Long

View File

@@ -13,6 +13,9 @@ abstract class TracksDao {
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun find(mangaId: Long): TrackEntity?
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun findNewChapters(mangaId: Long): Int?
@Query("DELETE FROM tracks")
abstract suspend fun clear()

View File

@@ -3,13 +3,13 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaState
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
@Entity(tableName = "manga")
data class MangaEntity(
class MangaEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val id: Long,
@ColumnInfo(name = "title") val title: String,

View File

@@ -14,7 +14,7 @@ import androidx.room.PrimaryKey
onDelete = ForeignKey.CASCADE
)]
)
data class MangaPrefsEntity(
class MangaPrefsEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "mode") val mode: Int

View File

@@ -20,7 +20,7 @@ import androidx.room.ForeignKey
)
]
)
data class MangaTagsEntity(
class MangaTagsEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "tag_id", index = true) val tagId: Long
)

View File

@@ -5,7 +5,7 @@ import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.utils.ext.mapToSet
data class MangaWithTags(
class MangaWithTags(
@Embedded val manga: MangaEntity,
@Relation(
parentColumn = "manga_id",

View File

@@ -3,12 +3,13 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.parsers.util.toTitleCase
@Entity(tableName = "tags")
data class TagEntity(
class TagEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "tag_id") val id: Long,
@ColumnInfo(name = "title") val title: String,
@@ -18,7 +19,7 @@ data class TagEntity(
fun toMangaTag() = MangaTag(
key = this.key,
title = this.title,
title = this.title.toTitleCase(),
source = MangaSource.valueOf(this.source)
)

View File

@@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
)
]
)
data class TrackEntity(
class TrackEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "chapters_total") val totalChapters: Int,

View File

@@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
)
]
)
data class TrackLogEntity(
class TrackLogEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") val id: Long = 0L,
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.util.*
data class TrackLogWithManga(
class TrackLogWithManga(
@Embedded val trackLog: TrackLogEntity,
@Relation(
parentColumn = "manga_id",

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.parsers.model.SortOrder
class Migration8To9 : Migration(8, 9) {

View File

@@ -1,13 +0,0 @@
package org.koitharu.kotatsu.core.exceptions
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
class AuthRequiredException(
val url: String
) : RuntimeException("Authorization required"), ResolvableException {
@StringRes
override val resolveTextId: Int = R.string.sign_in
}

View File

@@ -3,12 +3,7 @@ package org.koitharu.kotatsu.core.exceptions
import androidx.annotation.StringRes
import okio.IOException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
class CloudFlareProtectedException(
val url: String
) : IOException("Protected by CloudFlare"), ResolvableException {
@StringRes
override val resolveTextId: Int = R.string.captcha_solve
}
) : IOException("Protected by CloudFlare")

View File

@@ -1,4 +0,0 @@
package org.koitharu.kotatsu.core.exceptions
class ParseException(message: String? = null, cause: Throwable? = null) :
RuntimeException(message, cause)

View File

@@ -1,40 +1,85 @@
package org.koitharu.kotatsu.core.exceptions.resolve
import android.util.ArrayMap
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.AuthRequiredException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import org.koitharu.kotatsu.utils.TaggedActivityResult
import org.koitharu.kotatsu.utils.isSuccess
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ExceptionResolver(
private val lifecycleOwner: LifecycleOwner,
private val fm: FragmentManager
) {
class ExceptionResolver private constructor(
private val activity: FragmentActivity?,
private val fragment: Fragment?,
) : ActivityResultCallback<TaggedActivityResult> {
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
private lateinit var sourceAuthContract: ActivityResultLauncher<MangaSource>
suspend fun resolve(e: ResolvableException): Boolean = when (e) {
constructor(activity: FragmentActivity) : this(activity = activity, fragment = null) {
sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this)
}
constructor(fragment: Fragment) : this(activity = null, fragment = fragment) {
sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this)
}
override fun onActivityResult(result: TaggedActivityResult?) {
result ?: return
continuations.remove(result.tag)?.resume(result.isSuccess)
}
suspend fun resolve(e: Throwable): Boolean = when (e) {
is CloudFlareProtectedException -> resolveCF(e.url)
is AuthRequiredException -> false //TODO
is AuthRequiredException -> resolveAuthException(e.source)
else -> false
}
private suspend fun resolveCF(url: String) = suspendCancellableCoroutine<Boolean> { cont ->
private suspend fun resolveCF(url: String): Boolean {
val dialog = CloudFlareDialog.newInstance(url)
fm.clearFragmentResult(CloudFlareDialog.TAG)
continuations[CloudFlareDialog.TAG] = cont
fm.setFragmentResultListener(CloudFlareDialog.TAG, lifecycleOwner) { key, result ->
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
}
dialog.show(fm, CloudFlareDialog.TAG)
cont.invokeOnCancellation {
continuations.remove(CloudFlareDialog.TAG, cont)
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
dialog.dismiss()
val fm = getFragmentManager()
return suspendCancellableCoroutine { cont ->
fm.clearFragmentResult(CloudFlareDialog.TAG)
continuations[CloudFlareDialog.TAG] = cont
fm.setFragmentResultListener(CloudFlareDialog.TAG, checkNotNull(fragment ?: activity)) { key, result ->
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
}
dialog.show(fm, CloudFlareDialog.TAG)
cont.invokeOnCancellation {
continuations.remove(CloudFlareDialog.TAG, cont)
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
dialog.dismiss()
}
}
}
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
continuations[SourceAuthActivity.TAG] = cont
sourceAuthContract.launch(source)
}
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
companion object {
@StringRes
fun getResolveStringId(e: Throwable) = when (e) {
is CloudFlareProtectedException -> R.string.captcha_solve
is AuthRequiredException -> R.string.sign_in
else -> 0
}
fun canResolve(e: Throwable) = getResolveStringId(e) != 0
}
}

View File

@@ -1,6 +0,0 @@
package org.koitharu.kotatsu.core.exceptions.resolve
interface ResolvableException {
val resolveTextId: Int
}

View File

@@ -2,8 +2,8 @@ package org.koitharu.kotatsu.core.github
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.parseJson
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.parseJson
class GithubRepository(private val okHttp: OkHttpClient) {

View File

@@ -2,12 +2,12 @@ package org.koitharu.kotatsu.core.github
import java.util.*
data class VersionId(
class VersionId(
val major: Int,
val minor: Int,
val build: Int,
val variantType: String,
val variantNumber: Int
val variantNumber: Int,
) : Comparable<VersionId> {
override fun compareTo(other: VersionId): Int {
@@ -30,6 +30,30 @@ data 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
}
companion object {
private fun variantWeight(variantType: String) =

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.*
@Parcelize

View File

@@ -1,29 +0,0 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Manga(
val id: Long,
val title: String,
val altTitle: String? = null,
val url: String, // relative url for internal use
val publicUrl: String,
val rating: Float = NO_RATING, //normalized value [0..1] or -1
val isNsfw: Boolean = false,
val coverUrl: String,
val largeCoverUrl: String? = null,
val description: String? = null, //HTML
val tags: Set<MangaTag> = emptySet(),
val state: MangaState? = null,
val author: String? = null,
val chapters: List<MangaChapter>? = null,
val source: MangaSource
) : Parcelable {
companion object {
const val NO_RATING = -1f
}
}

View File

@@ -1,16 +0,0 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class MangaChapter(
val id: Long,
val name: String,
val number: Int,
val url: String,
val scanlator: String? = null,
val uploadDate: Long,
val branch: String? = null,
val source: MangaSource
) : Parcelable

View File

@@ -1,10 +0,0 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class MangaFilter(
val sortOrder: SortOrder?,
val tags: Set<MangaTag>,
) : Parcelable

View File

@@ -10,5 +10,5 @@ data class MangaHistory(
val updatedAt: Date,
val chapterId: Long,
val page: Int,
val scroll: Int
val scroll: Int,
) : Parcelable

View File

@@ -1,13 +0,0 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class MangaPage(
val id: Long,
val url: String,
val referer: String,
val preview: String? = null,
val source: MangaSource
) : Parcelable

View File

@@ -1,51 +0,0 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koin.core.context.GlobalContext
import org.koin.core.error.NoBeanDefFoundException
import org.koin.core.qualifier.named
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.site.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
@Suppress("SpellCheckingInspection")
@Parcelize
enum class MangaSource(
val title: String,
val locale: String?,
val cls: Class<out MangaRepository>
) : Parcelable {
LOCAL("Local", null, LocalMangaRepository::class.java),
READMANGA_RU("ReadManga", "ru", ReadmangaRepository::class.java),
MINTMANGA("MintManga", "ru", MintMangaRepository::class.java),
SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java),
MANGACHAN("Манга-тян", "ru", MangaChanRepository::class.java),
DESUME("Desu.me", "ru", DesuMeRepository::class.java),
HENCHAN("Хентай-тян", "ru", HenChanRepository::class.java),
YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java),
MANGATOWN("MangaTown", "en", MangaTownRepository::class.java),
MANGALIB("MangaLib", "ru", MangaLibRepository::class.java),
// NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java),
MANGAREAD("MangaRead", "en", MangareadRepository::class.java),
REMANGA("Remanga", "ru", RemangaRepository::class.java),
HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java),
ANIBEL("Anibel", "be", AnibelRepository::class.java),
NINEMANGA_EN("NineManga English", "en", NineMangaRepository.English::class.java),
NINEMANGA_ES("NineManga Español", "es", NineMangaRepository.Spanish::class.java),
NINEMANGA_RU("NineManga Русский", "ru", NineMangaRepository.Russian::class.java),
NINEMANGA_DE("NineManga Deutsch", "de", NineMangaRepository.Deutsch::class.java),
NINEMANGA_IT("NineManga Italiano", "it", NineMangaRepository.Italiano::class.java),
NINEMANGA_BR("NineManga Brasil", "pt", NineMangaRepository.Brazil::class.java),
NINEMANGA_FR("NineManga Français", "fr", NineMangaRepository.Francais::class.java),
EXHENTAI("ExHentai", null, ExHentaiRepository::class.java),
MANGAOWL("MangaOwl", "en", MangaOwlRepository::class.java)
;
@get:Throws(NoBeanDefFoundException::class)
@Deprecated("", ReplaceWith("MangaRepository(this)",
"org.koitharu.kotatsu.core.parser.MangaRepository"))
val repository: MangaRepository
get() = GlobalContext.get().get(named(this))
}

View File

@@ -1,5 +0,0 @@
package org.koitharu.kotatsu.core.model
enum class MangaState {
ONGOING, FINISHED
}

View File

@@ -1,11 +0,0 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class MangaTag(
val title: String,
val key: String,
val source: MangaSource
) : Parcelable

View File

@@ -2,13 +2,13 @@ package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
@Parcelize
data class MangaTracking(
val manga: Manga,
val knownChaptersCount: Int,
val lastChapterId: Long,
val lastNotifiedChapterId: Long,
val lastCheck: Date?
) : Parcelable
)

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.core.model
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
enum class SortOrder(@StringRes val titleRes: Int) {
UPDATED(R.string.updated),
POPULARITY(R.string.popular),
RATING(R.string.by_rating),
NEWEST(R.string.newest),
ALPHABETICAL(R.string.by_name)
}

View File

@@ -2,12 +2,12 @@ package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
@Parcelize
data class TrackingLogItem(
val id: Long,
val manga: Manga,
val chapters: List<String>,
val createdAt: Date
) : Parcelable
)

View File

@@ -0,0 +1,91 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import androidx.core.os.ParcelCompat
import org.koitharu.kotatsu.parsers.model.*
fun Manga.writeToParcel(out: Parcel, flags: Int) {
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)
out.writeParcelable(chapters?.let(::ParcelableMangaChapters), flags)
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(readParcelable<ParcelableMangaTags>(ParcelableMangaTags::class.java.classLoader)).tags,
state = readSerializable() as MangaState?,
author = readString(),
chapters = readParcelable<ParcelableMangaChapters>(ParcelableMangaChapters::class.java.classLoader)?.chapters,
source = readSerializable() as MangaSource,
)
fun MangaPage.writeToParcel(out: Parcel) {
out.writeLong(id)
out.writeString(url)
out.writeString(referer)
out.writeString(preview)
out.writeSerializable(source)
}
fun Parcel.readMangaPage() = MangaPage(
id = readLong(),
url = requireNotNull(readString()),
referer = requireNotNull(readString()),
preview = readString(),
source = readSerializable() as MangaSource,
)
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 = readSerializable() as MangaSource,
)
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 = readSerializable() as MangaSource,
)

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.Manga
class ParcelableManga(
val manga: Manga,
): Parcelable {
constructor(parcel: Parcel) : this(parcel.readManga())
override fun writeToParcel(parcel: Parcel, flags: Int) {
manga.writeToParcel(parcel, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ParcelableManga> {
override fun createFromParcel(parcel: Parcel): ParcelableManga {
return ParcelableManga(parcel)
}
override fun newArray(size: Int): Array<ParcelableManga?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.utils.ext.createList
class ParcelableMangaChapters(
val chapters: List<MangaChapter>,
) : Parcelable {
constructor(parcel: Parcel) : this(
createList(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,36 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.utils.ext.createList
class ParcelableMangaPages(
val pages: List<MangaPage>,
) : Parcelable {
constructor(parcel: Parcel) : this(
createList(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

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

View File

@@ -7,6 +7,9 @@ import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
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 {
@@ -19,10 +22,4 @@ class CloudFlareInterceptor : Interceptor {
}
return response
}
private companion object {
private const val HEADER_SERVER = "Server"
private const val SERVER_CLOUDFLARE = "cloudflare"
}
}

View File

@@ -1,9 +1,15 @@
package org.koitharu.kotatsu.core.network
import okhttp3.CacheControl
object CommonHeaders {
const val REFERER = "Referer"
const val USER_AGENT = "User-Agent"
const val ACCEPT = "Accept"
const val CONTENT_DISPOSITION = "Content-Disposition"
}
const val COOKIE = "Cookie"
val CACHE_CONTROL_DISABLED: CacheControl
get() = CacheControl.Builder().noStore().build()
}

View File

@@ -8,6 +8,8 @@ import okio.Buffer
import java.io.IOException
import java.nio.charset.StandardCharsets
private const val TAG = "CURL"
class CurlLoggingInterceptor(
private val extraCurlOptions: String? = null,
) : Interceptor {
@@ -51,9 +53,4 @@ class CurlLoggingInterceptor(
Log.d(TAG, "╰--- (copy and paste the above line to a terminal)")
return chain.proceed(request)
}
private companion object {
const val TAG = "CURL"
}
}

View File

@@ -2,25 +2,25 @@ package org.koitharu.kotatsu.core.network
import okhttp3.CookieJar
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.utils.DownloadManagerHelper
import java.util.concurrent.TimeUnit
val networkModule
get() = module {
single { AndroidCookieJar() } bind CookieJar::class
single(named(CacheUtils.QUALIFIER_HTTP)) { CacheUtils.createHttpCache(androidContext()) }
single {
OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(get())
cache(get(named(CacheUtils.QUALIFIER_HTTP)))
cache(get<LocalStorageManager>().createHttpCache())
addInterceptor(UserAgentInterceptor())
addInterceptor(CloudFlareInterceptor())
if (BuildConfig.DEBUG) {
@@ -28,4 +28,6 @@ val networkModule
}
}.build()
}
}
factory { DownloadManagerHelper(get(), get()) }
single<MangaLoaderContext> { MangaLoaderContextImpl(get(), get(), get()) }
}

View File

@@ -15,8 +15,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.requireBitmap

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.core.parser
import android.net.Uri
import coil.map.Mapper
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.parsers.model.MangaSource
class FaviconMapper() : Mapper<Uri, HttpUrl> {
override fun map(data: Uri): HttpUrl {
val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
val repo = MangaRepository(mangaSource) as RemoteMangaRepository
return repo.getFaviconUrl().toHttpUrl()
}
override fun handles(data: Uri) = data.scheme == "favicon"
}

View File

@@ -0,0 +1,53 @@
package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint
import android.content.Context
import android.util.Base64
import android.webkit.WebView
import androidx.core.os.LocaleListCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.toList
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class MangaLoaderContextImpl(
override val httpClient: OkHttpClient,
override val cookieJar: AndroidCookieJar,
private val androidContext: Context,
) : MangaLoaderContext() {
@SuppressLint("SetJavaScriptEnabled")
override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main) {
val webView = WebView(androidContext)
webView.settings.javaScriptEnabled = true
suspendCoroutine { cont ->
webView.evaluateJavascript(script) { result ->
cont.resume(result?.takeUnless { it == "null" })
}
}
}
override fun getConfig(source: MangaSource): MangaSourceConfig {
return SourceSettings(androidContext, source)
}
override fun encodeBase64(data: ByteArray): String {
return Base64.encodeToString(data, Base64.NO_PADDING)
}
override fun decodeBase64(data: String): ByteArray {
return Base64.decode(data, Base64.DEFAULT)
}
override fun getPreferredLocales(): List<Locale> {
return LocaleListCompat.getAdjustedDefault().toList()
}
}

View File

@@ -2,14 +2,16 @@ package org.koitharu.kotatsu.core.parser
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.qualifier.named
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.*
interface MangaRepository {
val source: MangaSource
val sortOrders: Set<SortOrder>
suspend fun getList2(
suspend fun getList(
offset: Int,
query: String? = null,
tags: Set<MangaTag>? = null,
@@ -27,7 +29,11 @@ interface MangaRepository {
companion object : KoinComponent {
operator fun invoke(source: MangaSource): MangaRepository {
return get(named(source))
return if (source == MangaSource.LOCAL) {
get<LocalMangaRepository>()
} else {
RemoteMangaRepository(source, get())
}
}
}
}

View File

@@ -1,8 +0,0 @@
package org.koitharu.kotatsu.core.parser
interface MangaRepositoryAuthProvider {
val authUrl: String
fun isAuthorized(): Boolean
}

View File

@@ -1,37 +0,0 @@
package org.koitharu.kotatsu.core.parser
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.site.*
val parserModule
get() = module {
single { MangaLoaderContext(get(), get()) }
factory<MangaRepository>(named(MangaSource.READMANGA_RU)) { ReadmangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.MINTMANGA)) { MintMangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.SELFMANGA)) { SelfMangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGACHAN)) { MangaChanRepository(get()) }
factory<MangaRepository>(named(MangaSource.DESUME)) { DesuMeRepository(get()) }
factory<MangaRepository>(named(MangaSource.HENCHAN)) { HenChanRepository(get()) }
factory<MangaRepository>(named(MangaSource.YAOICHAN)) { YaoiChanRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGATOWN)) { MangaTownRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGALIB)) { MangaLibRepository(get()) }
// factory<MangaRepository>(named(MangaSource.NUDEMOON)) { NudeMoonRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGAREAD)) { MangareadRepository(get()) }
factory<MangaRepository>(named(MangaSource.REMANGA)) { RemangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.HENTAILIB)) { HentaiLibRepository(get()) }
factory<MangaRepository>(named(MangaSource.ANIBEL)) { AnibelRepository(get()) }
factory<MangaRepository>(named(MangaSource.NINEMANGA_EN)) { NineMangaRepository.English(get()) }
factory<MangaRepository>(named(MangaSource.NINEMANGA_BR)) { NineMangaRepository.Brazil(get()) }
factory<MangaRepository>(named(MangaSource.NINEMANGA_DE)) { NineMangaRepository.Deutsch(get()) }
factory<MangaRepository>(named(MangaSource.NINEMANGA_ES)) { NineMangaRepository.Spanish(get()) }
factory<MangaRepository>(named(MangaSource.NINEMANGA_RU)) { NineMangaRepository.Russian(get()) }
factory<MangaRepository>(named(MangaSource.NINEMANGA_IT)) { NineMangaRepository.Italiano(get()) }
factory<MangaRepository>(named(MangaSource.NINEMANGA_FR)) { NineMangaRepository.Francais(get()) }
factory<MangaRepository>(named(MangaSource.EXHENTAI)) { ExHentaiRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGAOWL)) { MangaOwlRepository(get()) }
}

View File

@@ -1,86 +1,51 @@
package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.newParser
abstract class RemoteMangaRepository(
protected val loaderContext: MangaLoaderContext
class RemoteMangaRepository(
override val source: MangaSource,
loaderContext: MangaLoaderContext,
) : MangaRepository {
protected abstract val source: MangaSource
private val parser: MangaParser = source.newParser(loaderContext)
protected abstract val defaultDomain: String
override val sortOrders: Set<SortOrder>
get() = parser.sortOrders
private val conf by lazy {
loaderContext.getSettings(source)
}
val title: String
get() = source.title
override val sortOrders: Set<SortOrder> get() = emptySet()
override suspend fun getPageUrl(page: MangaPage): String = page.url.withDomain()
override suspend fun getTags(): Set<MangaTag> = emptySet()
open fun onCreatePreferences(map: MutableMap<String, Any>) {
map[SourceSettings.KEY_DOMAIN] = defaultDomain
}
protected fun getDomain() = conf.getDomain(defaultDomain)
protected fun String.withDomain(subdomain: String? = null) = when {
this.startsWith("//") -> buildString {
append("http")
if (conf.isUseSsl(true)) {
append('s')
}
append(":")
append(this@withDomain)
var defaultSortOrder: SortOrder?
get() = getConfig().defaultSortOrder ?: sortOrders.firstOrNull()
set(value) {
getConfig().defaultSortOrder = value
}
this.startsWith("/") -> buildString {
append("http")
if (conf.isUseSsl(true)) {
append('s')
}
append("://")
if (subdomain != null) {
append(subdomain)
append('.')
}
append(conf.getDomain(defaultDomain))
append(this@withDomain)
}
else -> this
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder?
): List<Manga> = parser.getList(offset, query, tags, sortOrder)
override suspend fun getDetails(manga: Manga): Manga = parser.getDetails(manga)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = parser.getPages(chapter)
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
override suspend fun getTags(): Set<MangaTag> = parser.getTags()
fun getFaviconUrl(): String = parser.getFaviconUrl()
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
fun getConfigKeys(): List<ConfigKey<*>> = ArrayList<ConfigKey<*>>().also {
parser.onCreateConfig(it)
}
protected fun generateUid(url: String): Long {
var h = 1125899906842597L
source.name.forEach { c ->
h = 31 * h + c.toLong()
}
url.forEach { c ->
h = 31 * h + c.toLong()
}
return h
}
protected fun generateUid(id: Long): Long {
var h = 1125899906842597L
source.name.forEach { c ->
h = 31 * h + c.toLong()
}
h = 31 * h + id
return h
}
protected fun parseFailed(message: String? = null): Nothing {
throw ParseException(message)
}
}
private fun getConfig() = parser.config as SourceSettings
}

View File

@@ -1,179 +0,0 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.*
import java.util.*
class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
override val source = MangaSource.ANIBEL
override val defaultDomain = "anibel.net"
override val sortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.NEWEST
)
override suspend fun getList2(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder?
): List<Manga> {
if (!query.isNullOrEmpty()) {
return if (offset == 0) search(query) else emptyList()
}
val page = (offset / 12f).toIntUp().inc()
val link = when {
tags.isNullOrEmpty() -> "/manga?page=$page".withDomain()
else -> tags.joinToString(
prefix = "/manga?",
postfix = "&page=$page",
separator = "&",
) { tag -> "genre[]=${tag.key}" }.withDomain()
}
val doc = loaderContext.httpGet(link).parseHtml()
val root = doc.body().select("div.manga-block") ?: parseFailed("Cannot find root")
val items = root.select("div.anime-card")
return items.mapNotNull { card ->
val href = card.selectFirst("a")?.attr("href") ?: return@mapNotNull null
val status = card.select("tr")[2].text()
val fullTitle = card.selectFirst("h1.anime-card-title")?.text()
?.substringBeforeLast('[') ?: return@mapNotNull null
val titleParts = fullTitle.splitTwoParts('/')
Manga(
id = generateUid(href),
title = titleParts?.first?.trim() ?: fullTitle,
coverUrl = card.selectFirst("img")?.attr("data-src")
?.withDomain().orEmpty(),
altTitle = titleParts?.second?.trim(),
author = null,
rating = Manga.NO_RATING,
url = href,
publicUrl = href.withDomain(),
tags = card.select("p.tupe.tag").select("a").mapNotNullToSet tags@{ x ->
MangaTag(
title = x.text(),
key = x.attr("href").ifEmpty {
return@mapNotNull null
}.substringAfterLast("="),
source = source
)
},
state = when (status) {
"выпускаецца" -> MangaState.ONGOING
"завершанае" -> MangaState.FINISHED
else -> null
},
source = source
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = loaderContext.httpGet(manga.publicUrl).parseHtml()
val root = doc.body().select("div.container") ?: parseFailed("Cannot find root")
return manga.copy(
description = root.select("div.manga-block.grid-12")[2].select("p").text(),
chapters = root.select("ul.series").flatMap { table ->
table.select("li")
}.map { it.selectFirst("a") }.mapIndexedNotNull { i, a ->
val href = a?.select("a")?.first()?.attr("href")
?.toRelativeUrl(getDomain()) ?: return@mapIndexedNotNull null
MangaChapter(
id = generateUid(href),
name = "Глава " + a.selectFirst("a")?.text().orEmpty(),
number = i + 1,
url = href,
uploadDate = 0L,
source = source
)
}
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.withDomain()
val doc = loaderContext.httpGet(fullUrl).parseHtml()
val scripts = doc.select("script")
for (script in scripts) {
val data = script.html()
val pos = data.indexOf("dataSource")
if (pos == -1) {
continue
}
val json = data.substring(pos).substringAfter('[').substringBefore(']')
val domain = getDomain()
return json.split(",").mapNotNull {
it.trim()
.removeSurrounding('"', '\'')
.toRelativeUrl(domain)
.takeUnless(String::isBlank)
}.map { url ->
MangaPage(
id = generateUid(url),
url = url,
referer = fullUrl,
source = source
)
}
}
parseFailed("Pages list not found at ${chapter.url.withDomain()}")
}
override suspend fun getTags(): Set<MangaTag> {
val doc = loaderContext.httpGet("https://${getDomain()}/manga").parseHtml()
val root = doc.body().select("div#tabs-genres").select("ul#list.ul-three-colums")
return root.select("p.menu-tags.tupe").mapToSet { p ->
val a = p.selectFirst("a") ?: parseFailed("a is null")
MangaTag(
title = a.text().toCamelCase(),
key = a.attr("data-name"),
source = source
)
}
}
private suspend fun search(query: String): List<Manga> {
val domain = getDomain()
val doc = loaderContext.httpGet("https://$domain/search?q=$query").parseHtml()
val root = doc.body().select("div.manga-block").select("article.tab-2") ?: parseFailed("Cannot find root")
val items = root.select("div.anime-card")
return items.mapNotNull { card ->
val href = card.select("a").attr("href")
val status = card.select("tr")[2].text()
val fullTitle = card.selectFirst("h1.anime-card-title")?.text()
?.substringBeforeLast('[') ?: return@mapNotNull null
val titleParts = fullTitle.splitTwoParts('/')
Manga(
id = generateUid(href),
title = titleParts?.first?.trim() ?: fullTitle,
coverUrl = card.selectFirst("img")?.attr("src")
?.withDomain().orEmpty(),
altTitle = titleParts?.second?.trim(),
author = null,
rating = Manga.NO_RATING,
url = href,
publicUrl = href.withDomain(),
tags = card.select("p.tupe.tag").select("a").mapNotNullToSet tags@{ x ->
MangaTag(
title = x.text(),
key = x.attr("href").ifEmpty {
return@mapNotNull null
}.substringAfterLast("="),
source = source
)
},
state = when (status) {
"выпускаецца" -> MangaState.ONGOING
"завершанае" -> MangaState.FINISHED
else -> null
},
source = source
)
}
}
}

View File

@@ -1,159 +0,0 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat
import java.util.*
abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(
loaderContext
) {
override val sortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.NEWEST,
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL
)
override suspend fun getList2(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder?
): List<Manga> {
val domain = getDomain()
val url = when {
!query.isNullOrEmpty() -> {
if (offset != 0) {
return emptyList()
}
"https://$domain/?do=search&subaction=search&story=${query.urlEncoded()}"
}
!tags.isNullOrEmpty() -> tags.joinToString(
prefix = "https://$domain/tags/",
postfix = "&n=${getSortKey2(sortOrder)}?offset=$offset",
separator = "+",
) { tag -> tag.key }
else -> "https://$domain/${getSortKey(sortOrder)}?offset=$offset"
}
val doc = loaderContext.httpGet(url).parseHtml()
val root = doc.body().selectFirst("div.main_fon")?.getElementById("content")
?: throw ParseException("Cannot find root")
return root.select("div.content_row").mapNotNull { row ->
val a = row.selectFirst("div.manga_row1")?.selectFirst("h2")?.selectFirst("a")
?: return@mapNotNull null
val href = a.relUrl("href")
Manga(
id = generateUid(href),
url = href,
publicUrl = href.inContextOf(a),
altTitle = a.attr("title"),
title = a.text().substringAfterLast('(').substringBeforeLast(')'),
author = row.getElementsByAttributeValueStarting(
"href",
"/mangaka"
).firstOrNull()?.text(),
coverUrl = row.selectFirst("div.manga_images")?.selectFirst("img")
?.absUrl("src").orEmpty(),
tags = runCatching {
row.selectFirst("div.genre")?.select("a")?.mapToSet {
MangaTag(
title = it.text(),
key = it.attr("href").substringAfterLast('/').urlEncoded(),
source = source
)
}
}.getOrNull().orEmpty(),
source = source
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val doc = loaderContext.httpGet(manga.url.withDomain()).parseHtml()
val root =
doc.body().getElementById("dle-content") ?: throw ParseException("Cannot find root")
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("<div"),
largeCoverUrl = root.getElementById("cover")?.absUrl("src"),
chapters = root.select("table.table_cha tr:gt(1)").reversed().mapIndexedNotNull { i, tr ->
val href = tr?.selectFirst("a")?.relUrl("href") ?: return@mapIndexedNotNull null
MangaChapter(
id = generateUid(href),
name = tr.selectFirst("a")?.text().orEmpty(),
number = i + 1,
url = href,
uploadDate = dateFormat.tryParse(tr.selectFirst("div.date")?.text()),
source = source
)
}
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.withDomain()
val doc = loaderContext.httpGet(fullUrl).parseHtml()
val scripts = doc.select("script")
for (script in scripts) {
val data = script.html()
val pos = data.indexOf("\"fullimg")
if (pos == -1) {
continue
}
val json = data.substring(pos).substringAfter('[').substringBefore(';')
.substringBeforeLast(']')
val domain = getDomain()
return json.split(",").mapNotNull {
it.trim()
.removeSurrounding('"', '\'')
.toRelativeUrl(domain)
.takeUnless(String::isBlank)
}.map { url ->
MangaPage(
id = generateUid(url),
url = url,
referer = fullUrl,
source = source
)
}
}
throw ParseException("Pages list not found at ${chapter.url}")
}
override suspend fun getTags(): Set<MangaTag> {
val domain = getDomain()
val doc = loaderContext.httpGet("https://$domain/catalog").parseHtml()
val root = doc.body().selectFirst("div.main_fon")?.getElementById("side")
?.select("ul")?.last() ?: throw ParseException("Cannot find root")
return root.select("li.sidetag").mapToSet { li ->
val a = li.children().last() ?: throw ParseException("a is null")
MangaTag(
title = a.text().toCamelCase(),
key = a.attr("href").substringAfterLast('/'),
source = source
)
}
}
private fun getSortKey(sortOrder: SortOrder?) =
when (sortOrder ?: sortOrders.minByOrNull { it.ordinal }) {
SortOrder.ALPHABETICAL -> "catalog"
SortOrder.POPULARITY -> "mostfavorites"
SortOrder.NEWEST -> "manga/new"
else -> "mostfavorites"
}
private fun getSortKey2(sortOrder: SortOrder?) =
when (sortOrder ?: sortOrders.minByOrNull { it.ordinal }) {
SortOrder.ALPHABETICAL -> "abcasc"
SortOrder.POPULARITY -> "favdesc"
SortOrder.NEWEST -> "datedesc"
else -> "favdesc"
}
}

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