Compare commits

...

303 Commits
v2.1.3 ... v3.1

Author SHA1 Message Date
Koitharu
2e17efe82b Update parsers 2022-04-11 18:43:52 +03:00
Koitharu
5bed854b9c Refactor entity mapping 2022-04-10 11:00:05 +03:00
Koitharu
7262b403f0 Hide reading fab if history is empty 2022-04-10 10:25:11 +03:00
Koitharu
a6fcbefc7b Update strings 2022-04-09 18:47:25 +03:00
Koitharu
7f9ea0efa0 Merge branch 'feature/multiselect' into devel 2022-04-09 18:39:25 +03:00
Koitharu
934861322e Migrate to ExtendedFloatingActionButton 2022-04-09 18:35:07 +03:00
Koitharu
e008fbab9b Merge branch 'devel' into feature/multiselect 2022-04-09 08:29:57 +03:00
Koitharu
2cd9ea19fd Update dependencies 2022-04-09 08:28:01 +03:00
Koitharu
699a249620 Merge branch 'documentation-update' of https://github.com/grrrrr/Kotatsu into devel 2022-04-09 07:35:16 +03:00
Koitharu
6c87d5b0bc Add check to avoid TransactionTooLargeException 2022-04-08 18:15:04 +03:00
Koitharu
c92bdae842 Add tags blacklist option for suggestions 2022-04-08 14:56:45 +03:00
Koitharu
6ca9608a80 Remove CurlLoggingInterceptor 2022-04-07 17:23:59 +03:00
Koitharu
8f9c0cbff1 Fix tags suggestion 2022-04-07 17:20:02 +03:00
Koitharu
cc6b114e4d Improve suggestions worker 2022-04-07 17:04:11 +03:00
grrrrr
3d5c2123d4 Update full_description.txt
- Remove HTML code so displaying on sites such as f-droid does not create a lot of wasted space
2022-04-06 19:13:43 +00:00
grrrrr
36b4e16b7c Update full_description.txt
- Remote HTML code so displaying on sites such as f-droid does not create a lot of wasted space
- add additional features taken from updated README.md
2022-04-06 19:12:15 +00:00
grrrrr
3ebd074e93 Update README.md
change feature "localized in" to "available in
2022-04-06 19:10:23 +00:00
grrrrr
e9b2b545a4 Update README.md
- Add additional features (password protection and localization) to list.
- Add details on how to contribute to translation
2022-04-06 19:05:33 +00:00
Koitharu
cca6d5fa04 Migrate to expedited jobs 2022-04-06 18:38:37 +03:00
Koitharu
36a7a3ebbc Fix DownloadService foreground notification #50 2022-04-06 17:24:10 +03:00
Koitharu
48ec9a1ea9 Merge branch 'feature/settings' into devel 2022-04-06 17:23:08 +03:00
Koitharu
76a9a0d1ab ActionMode selection in manga lists 2022-04-06 17:21:09 +03:00
Koitharu
f2175b40c0 Improve android AutoBackup support 2022-04-05 07:40:00 +03:00
Koitharu
85b992ca32 Remove SimpleSettingsActivity 2022-04-04 10:02:20 +03:00
Koitharu
41fb351fe0 Use master-detals pattern for settings 2022-04-04 09:41:57 +03:00
Koitharu
b916d4016e Fix toolbar icons color 2022-04-03 20:00:13 +03:00
Koitharu
abfd7f281d Fix toolbar icons color 2022-04-03 19:50:58 +03:00
Koitharu
515d6ab2c9 Fix widget size 2022-04-03 11:12:29 +03:00
Koitharu
8ee0dd9930 Fix local pages uri 2022-04-03 11:01:15 +03:00
Koitharu
6b9fad493c Update legacy launcher icon 2022-04-03 10:47:16 +03:00
Koitharu
a21297d209 Fix page saving 2022-04-03 10:05:05 +03:00
Koitharu
db3183c6e2 Fix strings 2022-03-31 19:31:19 +03:00
Koitharu
9eaaf96abe Update translations 2022-03-31 17:39:56 +03:00
Aliaksiej Razumaŭ
365b6a410a Translated using Weblate (Belarusian)
Currently translated at 99.2% (268 of 270 strings)

Co-authored-by: Aliaksiej Razumaŭ <belarusaed@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2022-03-31 17:35:43 +03:00
Allan Nordhøy
a6a601c365 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (270 of 270 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-31 17:35:43 +03:00
J. Lavoie
6ae52df8f8 Translated using Weblate (Finnish)
Currently translated at 100.0% (270 of 270 strings)

Translated using Weblate (French)

Currently translated at 100.0% (270 of 270 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (270 of 270 strings)

Translated using Weblate (German)

Currently translated at 100.0% (270 of 270 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (269 of 270 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/
Translation: Kotatsu/Strings
2022-03-31 17:35:43 +03:00
Anupam Malhotra
993c139715 Translated using Weblate (Spanish)
Currently translated at 99.6% (269 of 270 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (266 of 267 strings)

Co-authored-by: Anupam Malhotra <anpm.malhotra@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2022-03-31 17:35:43 +03:00
J. Lavoie
78ca36af11 Translated using Weblate (French)
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)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-03-31 17:35:43 +03:00
Luiz-bro
078d0c9cf9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (270 of 270 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (267 of 267 strings)

Added translation using Weblate (Portuguese (Brazil))

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-03-31 17:35:43 +03:00
kuragehime
40602272da Translated using Weblate (Japanese)
Currently translated at 100.0% (270 of 270 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)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (269 of 269 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-31 17:35:43 +03:00
Koitharu
de46cfe7ee Fix manga downloading 2022-03-31 16:18:53 +03:00
Koitharu
8b5a985842 Remove AsyncLayoutInflater; fixes 2022-03-29 20:43:06 +03:00
Koitharu
b57e4c520b Add monochrome icon 2022-03-29 18:45:24 +03:00
Koitharu
ec6b8224ae Update favourite bottom sheet 2022-03-29 08:23:15 +03:00
Koitharu
c48cf83343 Fix default branch selection 2022-03-29 08:13:18 +03:00
Koitharu
0c1ec2b0fc Fixes for api<23 2022-03-28 18:58:32 +03:00
Koitharu
5d2c046d53 Update screenshots 2022-03-27 15:03:49 +03:00
Koitharu
b0f221e5a7 Fixes 2022-03-26 09:07:15 +02:00
Koitharu
85b8bc5d07 Optimize drawables 2022-03-24 06:58:53 +02:00
Koitharu
ae0aa370b2 Update dependencies and fix some warnings 2022-03-24 06:58:53 +02:00
Koitharu
d3e9dc2ea4 Search in chapters #133 2022-03-24 06:58:53 +02:00
Koitharu
d5c7d8997f Update FUNDING.yml 2022-03-20 17:45:20 +02:00
Koitharu
da797741f9 Fix some settings, fix m3 switches 2022-03-20 17:36:29 +02:00
Koitharu
626d84eea3 Fix VersionId test 2022-03-20 17:27:48 +02:00
Koitharu
4d2f32a082 Update ru translations 2022-03-20 17:18:06 +02:00
Koitharu
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
Koitharu
8a08d58ed7 Merge branch 'feature/m3' into devel 2022-03-20 17:04:06 +02:00
Zakhar Timoshenko
6dc8ee5cf0 Return backgroundTint to FAB 2022-03-20 11:53:54 +03:00
Koitharu
b646cc00a3 Fix redundant fragments in DetailsActivity 2022-03-19 16:34:48 +02:00
Zakhar Timoshenko
7d2e70da7e Minor UI fixes 2022-03-19 13:55:32 +03:00
Zakhar Timoshenko
83cc3d60c8 Use new M3 switches 2022-03-19 13:54:20 +03:00
Koitharu
15ee102db4 Adjust ui to m3 style 2022-03-19 08:41:50 +02:00
Koitharu
ff25162834 Fixes 2022-03-17 16:51:10 +02:00
Koitharu
4913332444 Remember search mode 2022-03-17 15:32:32 +02:00
Koitharu
996f8f0f2e Fixes 2022-03-17 15:14:46 +02:00
Koitharu
4851139ba5 Improve performance 2022-03-17 11:24:15 +02:00
Koitharu
f0380d7eff Fix non-parcelable extras 2022-03-17 07:28:56 +02:00
Koitharu
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
Koitharu
e6cd6617ba Fix release build 2022-03-16 18:46:53 +02:00
Koitharu
de176ec040 Merge branch 'feature/desktop-ui' into devel 2022-03-16 18:08:50 +02:00
Koitharu
8a365250d9 Improve sources settings 2022-03-16 18:07:07 +02:00
Koitharu
9bd47e0410 Fix MangaRepository instantiation 2022-03-15 07:26:53 +02:00
Koitharu
02c15f896b Move parsers out of project 2022-03-15 07:06:32 +02:00
Koitharu
150699f64d Improve thumbnails bs 2022-03-13 18:37:50 +02:00
Koitharu
05ffc145be Merge branch 'devel' into feature/desktop-ui 2022-03-13 17:57:10 +02:00
Koitharu
25d52c5a61 Enhance manga search suggestion 2022-03-13 17:53:43 +02:00
Koitharu
abc2fb0e40 Hide FAB on search suggestions 2022-03-13 17:53:43 +02:00
Koitharu
54dfc32455 Make SearchView clearable 2022-03-13 17:53:43 +02:00
Koitharu
3802bc146f Suggest tags on search 2022-03-13 17:53:43 +02:00
Koitharu
8b295f6a93 Fix dialogs width 2022-03-13 17:50:49 +02:00
Koitharu
c115bcc163 Improve details activity layout 2022-03-13 17:10:34 +02:00
Koitharu
88a3589f1d Fix MainActivity insets 2022-03-13 09:54:12 +02:00
Zakhar Timoshenko
52dbd70c2f Some adjustments 2022-03-11 21:50:49 +03:00
Koitharu
0b07e83e3c Enhance tablet ui 2022-03-11 19:30:20 +02:00
Koitharu
445ff89392 Enhance nsfw detection and indication 2022-03-11 19:24:10 +02:00
Koitharu
a8a65e953f Support authorization for MangaLib and HentaiLib #122 2022-03-10 19:52:43 +02:00
Koitharu
755f1e5747 Fix filter 2022-03-10 19:52:43 +02:00
Zakhar Timoshenko
d5d19c37d8 Add referer in TrackWorker 2022-03-10 10:45:34 +03:00
Koitharu
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
Koitharu
3aed24fb49 Fix some authorization issues 2022-03-10 08:22:34 +02:00
Koitharu
2947cd3038 Add NudeMoon manga parser 2022-03-10 08:22:34 +02:00
Koitharu
2849ac58cb Support links in description 2022-03-10 08:22:34 +02:00
Koitharu
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
Zakhar Timoshenko
1f1fcf281d Revert "Use circle mask for thumb in new updates notification"
This reverts commit 8ff4eb26
2022-03-08 22:13:50 +03:00
Koitharu
a0c5b75bba Fix webtoon scroll #20 2022-03-08 19:25:10 +02:00
Koitharu
ccf4e4d285 Migrate to TransitionManager from custom animations 2022-03-08 19:18:02 +02:00
Koitharu
15c570979b Merge branch 'feature/page-preload' into devel 2022-03-08 19:00:13 +02:00
Koitharu
57f3715128 Cleanup resources and code 2022-03-08 18:58:15 +02:00
Koitharu
148986b454 Quick search across genres in filter 2022-03-08 17:28:00 +02:00
Koitharu
179b08b96a Add action for empty list state 2022-03-08 16:06:51 +02:00
Koitharu
d7f60fa95a Fix Batoto empty results 2022-03-08 14:25:46 +02:00
Koitharu
564f052a2f Add Bato.To manga source #77 2022-03-08 14:04:44 +02:00
Zakhar Timoshenko
8ff4eb2602 Use circle mask for thumb in new updates notification 2022-03-08 01:54:03 +03:00
Koitharu
6e5197a3f5 Specify page max progress explicitly 2022-03-07 13:10:09 +02:00
Koitharu
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
Koitharu
9686ad6f00 Show page progress on ProgressIndicator 2022-03-07 12:42:22 +02:00
Zakhar Timoshenko
ff21d1c4ec Fix incorrect arrow direction symbol 2022-03-06 23:55:15 +03:00
Koitharu
e1285fe738 Option to control pages preloading 2022-03-06 19:56:11 +02:00
Koitharu
889eea9c89 Show page loading progress 2022-03-06 15:14:28 +02:00
Koitharu
4a88ecc549 Show WebView loading progress 2022-03-05 09:01:35 +02:00
Koitharu
6eca4028ec Dont recreate WebView on configuration changed #119 2022-03-05 08:49:10 +02:00
Koitharu
5158f4bd89 Replace chapters dialog with bottom sheet 2022-03-04 20:10:29 +02:00
Koitharu
eb7e255430 Small ui fixes 2022-03-04 19:09:58 +02:00
Koitharu
f6a70dc7ac Fix build 2022-03-04 18:41:03 +02:00
Koitharu
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
Koitharu
6a965ddb28 Merge branch 'feature/bs-filter' into devel 2022-03-04 18:35:57 +02:00
Koitharu
9b86052624 Merge branch 'feature/suggestions' into devel 2022-03-04 18:34:15 +02:00
Koitharu
3c64d6675e Handle filter loading errors 2022-03-04 08:16:36 +02:00
Koitharu
9588ac8cbd Preload pages 2022-03-03 21:01:16 +02:00
Koitharu
5c05aaeacf Open filter from list header 2022-03-03 18:39:23 +02:00
Koitharu
238bc89be9 Move filter into bottom sheet 2022-03-02 21:05:57 +02:00
Koitharu
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
Koitharu
19fe2e0eb5 Fix strings 2022-03-02 19:26:45 +02:00
Koitharu
862fb3c2e6 Fix badges position 2022-03-02 19:22:51 +02:00
Koitharu
df34e921f3 Fix scrollbars 2022-03-02 19:16:57 +02:00
Koitharu
44c1b5ebb4 Update feed ui 2022-03-02 19:10:39 +02:00
Koitharu
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
Zakhar Timoshenko
f46a9c5f3a Fix crash on tablets when clicking on cover 2022-03-02 14:28:28 +03:00
Koitharu
27658eea20 Fix tags case 2022-03-01 07:59:15 +02:00
Zakhar Timoshenko
eec21fc5c1 Adjust manga grid item 2022-02-28 20:25:01 +03:00
Koitharu
5d26743c8f Fix tags for suggestions 2022-02-28 19:08:16 +02:00
Koitharu
3afa782e91 Merge branch 'devel' into feature/suggestions 2022-02-28 18:46:33 +02:00
Koitharu
cfdc3a15c5 Merge branch 'feature/counters' into devel 2022-02-28 18:33:30 +02:00
Koitharu
a2a7c26a42 Block screenshots on password activities #114 2022-02-28 18:28:26 +02:00
Zakhar Timoshenko
7fb67be1b6 Some UI changes 2022-02-28 01:24:21 +03:00
Zakhar Timoshenko
e8a225f97a Update material components dependency 2022-02-28 01:18:59 +03:00
Koitharu
54a914097d Option to block screenshots in reader #114 2022-02-27 20:28:55 +02:00
Koitharu
245e32237e Update widgets from background 2022-02-27 19:22:59 +02:00
Koitharu
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
Koitharu
632715e6c9 Suggestions settings 2022-02-27 18:25:18 +02:00
Koitharu
97c0fcf022 Merge branch 'devel' of github.com:nv95/Kotatsu into feature/suggestions 2022-02-27 16:32:32 +02:00
Koitharu
b3781abdeb New chapters counters in lists 2022-02-27 09:28:25 +02:00
Koitharu
1f7252fd12 Update feed ui 2022-02-26 18:14:15 +02:00
Koitharu
3c0c4ce9c0 Fix local manga size 2022-02-26 15:42:48 +02:00
Koitharu
ed4c470bdc Support batch manga import 2022-02-26 13:56:21 +02:00
Koitharu
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
Koitharu
a2d3b88c08 Fix non-translatable strings 2022-02-26 12:06:19 +02:00
Koitharu
62a177fcb3 Remove empty non-translated strings 2022-02-26 11:43:59 +02:00
Koitharu
19c751d349 Small ui updates 2022-02-26 11:39:50 +02:00
Koitharu
def2d5f494 Refactor and deprecations fixes 2022-02-26 10:14:40 +02:00
Koitharu
94e9fa35e2 Remove preferences delegates 2022-02-25 19:48:11 +02:00
Zakhar Timoshenko
14be8d4936 Merge remote-tracking branch 'origin/devel' into devel 2022-02-24 22:14:16 +03:00
Zakhar Timoshenko
38b550ecbb Add Discord link in about 2022-02-24 22:12:56 +03:00
Zakhar Timoshenko
b8ecfb5455 Add link to discord badge 2022-02-24 17:22:53 +03:00
Zakhar Timoshenko
f4c9d67178 Add discord badge to README 2022-02-24 17:20:57 +03:00
Koitharu
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
Koitharu
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
Koitharu
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
Koitharu
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
Zakhar Timoshenko
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
Zakhar Timoshenko
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
Koitharu
15d0addb7b Fix Remanga chapters parsing 2022-02-22 19:34:00 +02:00
Koitharu
1713efb51f Increase version 2022-02-20 18:43:12 +02:00
Koitharu
9089555320 Fix internal storage sharing #98 2022-02-20 18:41:38 +02:00
Koitharu
2f3b1f397c Fix Remanga source and check for paid chapters #101 2022-02-20 18:40:47 +02:00
Koitharu
7ebb98ce06 Allow overwrite non-empty download directory #99 2022-02-20 18:13:54 +02:00
Koitharu
805044fcf1 Add .editorconfig file 2022-02-20 18:12:04 +02:00
Koitharu
51d6a073e0 Refactor local storage manager 2022-02-13 10:21:37 +02:00
Koitharu
02980ea1e6 Allow overwrite non-empty download directory #99 2022-02-13 08:50:41 +02:00
Koitharu
920ea6959c Merge branch 'devel' into feature/suggestions 2022-02-13 08:31:44 +02:00
Koitharu
c7aaa22eab Merge branch 'master' into devel 2022-02-13 07:07:27 +02:00
Koitharu
c218ae0baa Merge branch 'release/2.1.4' 2022-02-12 20:37:41 +02:00
Koitharu
5820b2f511 Fix saved page sharing 2022-02-12 20:31:30 +02:00
Koitharu
79c2bf17fd Quick search across manga sources 2022-02-12 15:16:30 +02:00
Koitharu
78aa4d76db Show favicons in sources list 2022-02-12 15:16:11 +02:00
Koitharu
e2f3ba19b8 Update dependencies minor versions 2022-02-12 15:05:58 +02:00
Koitharu
41045686fc Increase version 2022-02-12 14:58:03 +02:00
Koitharu
8b0b375dfe Fix ActivityNotFoundException 2022-02-12 14:57:27 +02:00
Koitharu
c7c23b9768 Fix ItemTouchHelper leak 2022-02-12 14:57:16 +02:00
Koitharu
33190ae3ea Fix DownloadBinder leak 2022-02-12 14:57:10 +02:00
Koitharu
03590f4b82 Fix widgets context leak 2022-02-12 14:57:03 +02:00
Koitharu
cbcf98e1d4 Fix blocking calls in coroutines 2022-02-12 14:56:30 +02:00
Koitharu
4098f06995 Update material components 2022-02-12 14:07:06 +02:00
Koitharu
98f723200b Fix ActivityNotFoundException 2022-02-12 10:31:26 +02:00
Koitharu
07634d01f3 Fix some settings ui 2022-02-12 10:30:16 +02:00
Koitharu
3bd67e2098 Move LeakCanary to settings 2022-02-11 20:13:43 +02:00
Koitharu
427ce5fd07 Fix ItemTouchHelper leak 2022-02-11 19:41:49 +02:00
Koitharu
6bf927bb2c Fix DownloadBinder leak 2022-02-11 19:32:38 +02:00
Koitharu
da17c3495a Fix widgets context leak 2022-02-11 19:23:41 +02:00
Koitharu
e739e3f9e0 Update dependencies 2022-02-11 19:18:35 +02:00
Koitharu
10ec72047c Update dependencies 2022-02-04 08:18:01 +02:00
Koitharu
4be514b754 Migrate to MaterialDividerItemDecoration 2022-02-03 08:58:12 +02:00
Koitharu
add72c0be3 Fix settings ui 2022-01-28 19:31:28 +02:00
Koitharu
5758eed77b Update chapters list, show already downloaded chapters #95 2022-01-28 19:16:33 +02:00
Koitharu
c7dc05be5a Merge pull request #84 from ztimms73/m3
Material 3
2022-01-28 17:44:20 +02:00
Koitharu
355933c742 Fix blocking calls in coroutines 2022-01-26 19:24:27 +02:00
Zakhar Timoshenko
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
Koitharu
c3f0644b46 Merge branch 'comradekingu-patch-1' into devel 2022-01-26 08:31:58 +02:00
Koitharu
733889f238 Merge branch 'patch-1' of https://github.com/comradekingu/Kotatsu into comradekingu-patch-1 2022-01-26 08:31:20 +02:00
Koitharu
e280aa4963 Quick search across manga sources 2022-01-26 08:22:41 +02:00
Zakhar Timoshenko
254b0ab488 Some cleanup 2022-01-26 01:07:12 +03:00
Zakhar Timoshenko
1253ca07cc Allow Samsung devices on Android 12+ to use dynamic theme 2022-01-26 00:50:38 +03:00
Zakhar Timoshenko
e8bb4bac66 Implement dynamic theme option 2022-01-26 00:45:59 +03:00
Koitharu
2ac6b84f87 Show favicons in sources list 2022-01-25 19:56:37 +02:00
Koitharu
5f5a98e351 Merge branch 'release/2.1.3' into devel 2022-01-23 10:31:13 +02:00
Koitharu
2535739c2b Enhance download cancellation in blocking io tasks #90 2022-01-23 10:13:32 +02:00
Koitharu
b6e13de73f Fix MangaTown licensed chapters 2022-01-23 09:46:26 +02:00
Koitharu
fc3efbabbd Fix missing fragment crash #91 2022-01-23 08:14:56 +02:00
Koitharu
2a7761dbc3 Fix widgets #86 2022-01-23 08:05:07 +02:00
Koitharu
852f31574f Fix MangaRead parse #87 2022-01-23 07:49:03 +02:00
Zakhar Timoshenko
ee79c23fdf Use animation to hide/show fab 2022-01-17 09:40:41 +03:00
Zakhar Timoshenko
097e040dd6 Fix AlertDialogs 2022-01-17 09:26:54 +03:00
Koitharu
722b6d1e59 Cleanup MangaSource fields 2022-01-16 20:14:41 +02:00
Zakhar Timoshenko
eed8ef7010 Initial Material 3 theming 2022-01-16 17:53:03 +03:00
Koitharu
ba30690d26 Add static icons for manga sources in drawer 2022-01-15 18:40:14 +02:00
Koitharu
4da6a4d450 Update dependencies 2022-01-15 18:29:51 +02:00
Allan Nordhøy
7cb51f552a Suggested changes made
Co-authored-by: Koitharu <nvasya95@gmail.com>
2022-01-10 13:32:29 +01: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
Koitharu
22eebe89e7 Merge branch 'devel' into feature/suggestions 2021-09-26 17:37:59 +03:00
Koitharu
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
Koitharu
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
Koitharu
f0e56c4b6a Suggestions 2021-05-12 20:10:47 +03:00
677 changed files with 13318 additions and 11923 deletions

19
.editorconfig Normal file
View File

@@ -0,0 +1,19 @@
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
# noinspection EditorConfigKeyCorrectness
disabled_rules=no-wildcard-imports,no-unused-imports
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
ij_continuation_indent_size = 4
[{*.kt,*.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"]

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>

7
.idea/ktlint.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KtlintProjectConfiguration">
<androidMode>true</androidMode>
<treatAsErrors>false</treatAsErrors>
</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
@@ -25,15 +25,25 @@ Download APK from Github Releases:
* Tablet-optimized material design UI
* Standard and Webtoon-optimized reader
* Notifications about new chapters with updates feed
* Available in multiple languages
* Password protect access to the app
### Screenshots
| ![Screenshot_20200226-210337](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/1.png) | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/2.png) | ![Screenshot_20200226-210232](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/3.png) |
|---|---|---|
| ![Screenshot_20200226-210405](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/4.png) | ![Screenshot_20200226-210151](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/5.png) | ![Screenshot_20200226-210223](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/6.png) |
| ![Screenshot_20200226-210337](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/1.png) | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/2.png) | ![Screenshot_20200226-210232](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/3.png) |
|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| ![Screenshot_20200226-210405](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/4.png) | ![Screenshot_20200226-210151](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/5.png) | ![Screenshot_20200226-210223](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/6.png) |
| ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/1.png) | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/2.png) |
|---|---|
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
### Localization
<a href="https://hosted.weblate.org/engage/kotatsu/">
<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status" />
</a>
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages, please head over to the Weblate <a href="https://hosted.weblate.org/engage/kotatsu/">project page</a>
### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

View File

@@ -6,15 +6,16 @@ plugins {
}
android {
compileSdkVersion 31
buildToolsVersion '30.0.3'
compileSdkVersion 32
buildToolsVersion '32.0.0'
namespace 'org.koitharu.kotatsu'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 31
versionCode 379
versionName '2.1.3'
targetSdkVersion 32
versionCode 401
versionName '3.1'
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,75 +42,78 @@ 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 += [
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-opt-in=kotlinx.coroutines.FlowPreview',
'-opt-in=kotlin.contracts.ExperimentalContracts',
]
}
lint {
abortOnError false
disable 'MissingTranslation', 'PrivateResource'
}
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=kotlin.contracts.ExperimentalContracts',
]
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
implementation('com.github.nv95:kotatsu-parsers:8e23a7fcd4') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-service:2.4.0'
implementation 'androidx.lifecycle:lifecycle-process:2.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
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.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'com.google.android.material:material:1.6.0-beta01'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.0'
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
implementation 'androidx.room:room-runtime:2.4.0'
implementation 'androidx.room:room-ktx:2.4.0'
kapt 'androidx.room:room-compiler:2.4.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.1'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.1'
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.4'
implementation 'io.insert-koin:koin-android:3.1.6'
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:20211205'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
testImplementation 'io.insert-koin:koin-test-junit4:3.1.4'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
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.4.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(...);

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

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool>
</resources>

View File

@@ -1,28 +1,30 @@
<?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" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name="org.koitharu.kotatsu.KotatsuApp"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
android:dataExtractionRules="@xml/backup_rules"
android:fullBackupContent="@xml/backup_content"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/Theme.Kotatsu"
tools:ignore="UnusedAttribute">
<activity
android:name="org.koitharu.kotatsu.main.ui.MainActivity"
android:exported="true">
@@ -32,7 +34,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,23 +53,18 @@
<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" />
<activity
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
android:exported="true"
android:label="@string/settings">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</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,12 +91,13 @@
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:launchMode="singleTop"
android:label="@string/downloads" />
<activity android:name=".image.ui.ImageActivity"/>
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity"/>
<service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"

View File

@@ -7,11 +7,9 @@ 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
@@ -23,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
@@ -55,7 +55,6 @@ class KotatsuApp : Application() {
databaseModule,
githubModule,
uiModule,
parserModule,
mainModule,
searchModule,
localModule,
@@ -67,6 +66,7 @@ class KotatsuApp : Application() {
settingsModule,
readerModule,
appWidgetModule,
suggestionsModule,
)
}
}
@@ -93,6 +93,7 @@ class KotatsuApp : Application() {
.detectWrongFragmentContainer()
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.detectFragmentTagUsage()
.build()
}
}

View File

@@ -2,19 +2,19 @@ package org.koitharu.kotatsu.base.domain
import androidx.room.withTransaction
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.db.entity.*
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
class MangaDataRepository(private val db: MangaDatabase) {
suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
val tags = manga.tags.toEntities()
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(MangaEntity.from(manga), tags)
db.mangaDao.upsert(manga.toEntity(), tags)
db.preferencesDao.upsert(
MangaPrefsEntity(
mangaId = manga.id,
@@ -34,15 +34,19 @@ 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 -> findMangaById(intent.mangaId)
else -> null // TODO resolve uri
}
suspend fun storeManga(manga: Manga) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
val tags = manga.tags.toEntities()
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(MangaEntity.from(manga), tags)
db.mangaDao.upsert(manga.toEntity(), tags)
}
}
suspend fun findTags(source: MangaSource): Set<MangaTag> {
return db.tagsDao.findTags(source.name).toMangaTags()
}
}

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
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,84 +0,0 @@
package org.koitharu.kotatsu.base.domain
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.core.exceptions.GraphQLException
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.parseJson
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()
}
suspend fun graphQLQuery(endpoint: String, query: String): JSONObject {
val body = JSONObject()
body.put("operationName", null)
body.put("variables", JSONObject())
body.put("query", "{${query}}")
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = body.toString().toRequestBody(mediaType)
val request = Request.Builder()
.post(requestBody)
.url(endpoint)
val json = okHttp.newCall(request.build()).await().parseJson()
json.optJSONArray("errors")?.let {
if (it.length() != 0) {
throw GraphQLException(it)
}
}
return json
}
open fun getSettings(source: MangaSource) = SourceSettings(get(), source)
}

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 sorted = list.sortedBy { x ->
val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
}
return if (includeHidden) {
sorted
} else {
val hidden = settings.hiddenSources
sorted.filterNot { x ->
x.name in hidden
}
}
}
}

View File

@@ -3,17 +3,18 @@ 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.utils.ext.medianOrNull
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.parsers.util.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,7 +37,7 @@ 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

View File

@@ -7,39 +7,48 @@ import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
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.ActionModeDelegate
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)
val actionModeDelegate = ActionModeDelegate()
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 +68,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 +84,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)
}
@@ -108,8 +94,10 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
return isNight && get<AppSettings>().isAmoledTheme
}
@CallSuper
override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode)
actionModeDelegate.onSupportActionModeStarted(mode)
val insets = ViewCompat.getRootWindowInsets(binding.root)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
val view = findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)
@@ -118,6 +106,12 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
}
}
@CallSuper
override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode)
actionModeDelegate.onSupportActionModeFinished(mode)
}
override fun onBackPressed() {
if ( // https://issuetracker.google.com/issues/139738913
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
@@ -129,4 +123,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.R as materialR
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
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,32 @@
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.ActionModeDelegate
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)
protected val actionModeDelegate: ActionModeDelegate
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
override fun onCreateView(
inflater: LayoutInflater,
@@ -38,36 +40,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,61 @@ 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.fragment.app.Fragment
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
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), OnApplyWindowInsetsListener {
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) {
setTitle(getString(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
bottom = insets.bottom
)
return insets
}
@Suppress("UsePropertyAccessSyntax")
protected fun setTitle(title: CharSequence) {
(parentFragment as? SettingsHeadersFragment)?.setTitle(title)
?: activity?.setTitle(title)
}
}

View File

@@ -3,11 +3,11 @@ package org.koitharu.kotatsu.base.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.*
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.utils.SingleLiveEvent
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
abstract class BaseViewModel : ViewModel() {

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

@@ -7,12 +7,12 @@ import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.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) :
@@ -20,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()
}
@@ -57,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
@@ -72,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

@@ -6,6 +6,7 @@ import android.text.InputFilter
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.databinding.DialogInputBinding
class TextInputDialog private constructor(
@@ -18,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 {

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

@@ -1,18 +1,39 @@
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 org.koitharu.kotatsu.utils.ext.getThemeDrawable
import kotlin.math.roundToInt
import com.google.android.material.R as materialR
@SuppressLint("PrivateResource")
abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val bounds = Rect()
private val divider = context.getThemeDrawable(android.R.attr.listDivider)
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,
@@ -20,27 +41,29 @@ abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.It
parent: RecyclerView,
state: RecyclerView.State,
) {
outRect.set(0, divider?.intrinsicHeight ?: 0, 0, 0)
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 || divider == null) {
if (parent.layoutManager == null || thickness == 0) {
return
}
canvas.save()
val left: Int
val right: Int
val left: Float
val right: Float
if (parent.clipToPadding) {
left = parent.paddingLeft
right = parent.width - parent.paddingRight
left = parent.paddingLeft.toFloat()
right = (parent.width - parent.paddingRight).toFloat()
canvas.clipRect(
left, parent.paddingTop, right,
parent.height - parent.paddingBottom
left,
parent.paddingTop.toFloat(),
right,
(parent.height - parent.paddingBottom).toFloat()
)
} else {
left = 0
right = parent.width
left = 0f
right = parent.width.toFloat()
}
var previous: RecyclerView.ViewHolder? = null
@@ -48,10 +71,9 @@ abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.It
val holder = parent.getChildViewHolder(child)
if (previous != null && shouldDrawDivider(previous, holder)) {
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)
val top: Float = bounds.top + child.translationY
val bottom: Float = top + thickness
canvas.drawRect(left, top, right, bottom, paint)
}
previous = holder
}

View File

@@ -0,0 +1,111 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.view.View
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID
abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
private val bounds = Rect()
private val boundsF = RectF()
private val selection = HashSet<Long>()
protected var hasBackground: Boolean = true
protected var hasForeground: Boolean = false
protected var isIncludeDecorAndMargins: Boolean = true
val checkedItemsCount: Int
get() = selection.size
val checkedItemsIds: Set<Long>
get() = selection
fun toggleItemChecked(id: Long) {
if (!selection.remove(id)) {
selection.add(id)
}
}
fun setItemIsChecked(id: Long, isChecked: Boolean) {
if (isChecked) {
selection.add(id)
} else {
selection.remove(id)
}
}
fun checkAll(ids: Collection<Long>) {
selection.addAll(ids)
}
fun clearSelection() {
selection.clear()
}
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (hasBackground) {
doDraw(canvas, parent, state, false)
} else {
super.onDraw(canvas, parent, state)
}
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (hasForeground) {
doDraw(canvas, parent, state, true)
} else {
super.onDrawOver(canvas, parent, state)
}
}
private fun doDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State, isOver: Boolean) {
val checkpoint = canvas.save()
if (parent.clipToPadding) {
canvas.clipRect(
parent.paddingLeft, parent.paddingTop, parent.width - parent.paddingRight,
parent.height - parent.paddingBottom
)
}
for (child in parent.children) {
val itemId = getItemId(parent, child)
if (itemId != NO_ID && itemId in selection) {
if (isIncludeDecorAndMargins) {
parent.getDecoratedBoundsWithMargins(child, bounds)
} else {
bounds.set(child.left, child.top, child.right, child.bottom)
}
boundsF.set(bounds)
boundsF.offset(child.translationX, child.translationY)
if (isOver) {
onDrawForeground(canvas, parent, child, boundsF, state)
} else {
onDrawBackground(canvas, parent, child, boundsF, state)
}
}
}
canvas.restoreToCount(checkpoint)
}
protected open fun getItemId(parent: RecyclerView, child: View) = parent.getChildItemId(child)
protected open fun onDrawBackground(
canvas: Canvas,
parent: RecyclerView,
child: View,
bounds: RectF,
state: RecyclerView.State,
) = Unit
protected open fun onDrawForeground(
canvas: Canvas,
parent: RecyclerView,
child: View,
bounds: RectF,
state: RecyclerView.State,
) = Unit
}

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

@@ -0,0 +1,50 @@
package org.koitharu.kotatsu.base.ui.util
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
class ActionModeDelegate {
private var activeActionMode: ActionMode? = null
private var listeners: MutableList<ActionModeListener>? = null
val isActionModeStarted: Boolean
get() = activeActionMode != null
fun onSupportActionModeStarted(mode: ActionMode) {
activeActionMode = mode
listeners?.forEach { it.onActionModeStarted(mode) }
}
fun onSupportActionModeFinished(mode: ActionMode) {
activeActionMode = null
listeners?.forEach { it.onActionModeFinished(mode) }
}
fun addListener(listener: ActionModeListener) {
if (listeners == null) {
listeners = ArrayList()
}
checkNotNull(listeners).add(listener)
}
fun removeListener(listener: ActionModeListener) {
listeners?.remove(listener)
}
fun addListener(listener: ActionModeListener, owner: LifecycleOwner) {
addListener(listener)
owner.lifecycle.addObserver(ListenerLifecycleObserver(listener))
}
private inner class ListenerLifecycleObserver(
private val listener: ActionModeListener,
) : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
removeListener(listener)
}
}
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.base.ui.util
import androidx.appcompat.view.ActionMode
interface ActionModeListener {
fun onActionModeStarted(mode: ActionMode)
fun onActionModeFinished(mode: ActionMode)
}

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,48 @@
package org.koitharu.kotatsu.base.ui.util
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior
import androidx.core.view.ViewCompat
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
class ShrinkOnScrollBehavior : Behavior<ExtendedFloatingActionButton> {
@Suppress("unused") constructor() : super()
@Suppress("unused") constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: ExtendedFloatingActionButton,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL
}
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: ExtendedFloatingActionButton,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
if (dyConsumed > 0) {
if (child.isExtended) {
child.shrink()
}
} else if (dyConsumed < 0) {
if (!child.isExtended) {
child.extend()
}
}
}
}

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
}

View File

@@ -2,16 +2,22 @@ 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 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
@@ -34,13 +40,4 @@ class CoverImageView @JvmOverloads constructor(
}
setMeasuredDimension(desiredWidth, desiredHeight)
}
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
}
}

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,126 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.annotation.SuppressLint
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.ripple.RippleUtils
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import org.koitharu.kotatsu.R
@SuppressLint("RestrictedApi")
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) {
val itemRippleColor = getColorStateList(R.styleable.ListItemTextView_rippleColor)
?: getRippleColorFallback(context)
val shape = createShapeDrawable(this)
background = RippleDrawable(
RippleUtils.sanitizeRippleDrawableColor(itemRippleColor),
shape,
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
}

View File

@@ -0,0 +1,30 @@
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
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.isIndeterminate = false
progressIndicator.setProgressCompat(newProgress.coerceAtMost(PROGRESS_MAX), true)
} else {
progressIndicator.setIndeterminate(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

@@ -1,14 +1,15 @@
package org.koitharu.kotatsu.core.backup
import android.content.Context
import java.io.File
import java.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.MutableZipFile
import org.koitharu.kotatsu.utils.ext.format
import java.io.File
import java.util.*
class BackupArchive(file: File) : MutableZipFile(file) {
@@ -33,14 +34,13 @@ 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)
}
dir.mkdirs()
val filename = buildString {
append(context.getString(R.string.app_name).toLowerCase(Locale.ROOT))
append(context.getString(R.string.app_name).lowercase(Locale.ROOT))
append('_')
append(Date().format("ddMMyyyy"))
append(".bak")

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

@@ -1,28 +1,9 @@
package org.koitharu.kotatsu.core.db
import androidx.room.Room
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.migrations.*
val databaseModule
get() = module {
single {
Room.databaseBuilder(
androidContext(),
MangaDatabase::class.java,
"kotatsu-db"
).addMigrations(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7(),
Migration7To8(),
Migration8To9(),
).addCallback(
DatabasePrePopulateCallback(androidContext().resources)
).build()
}
single { MangaDatabase.create(androidContext()) }
}

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

@@ -1,22 +1,28 @@
package org.koitharu.kotatsu.core.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import org.koitharu.kotatsu.core.db.dao.*
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.db.migrations.*
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
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 = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class
], version = 9
],
version = 9
)
abstract class MangaDatabase : RoomDatabase() {
@@ -35,4 +41,26 @@ abstract class MangaDatabase : RoomDatabase() {
abstract val tracksDao: TracksDao
abstract val trackLogsDao: TrackLogsDao
abstract val suggestionDao: SuggestionDao
companion object {
fun create(context: Context): MangaDatabase = Room.databaseBuilder(
context,
MangaDatabase::class.java,
"kotatsu-db"
).addMigrations(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7(),
Migration7To8(),
Migration8To9(),
).addCallback(
DatabasePrePopulateCallback(context.resources)
).build()
}
}

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 tags.title
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 tags.title
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 tags.title
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 tags.title
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

@@ -0,0 +1,76 @@
package org.koitharu.kotatsu.core.db.entity
import java.util.*
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
// Entity to model
fun TagEntity.toMangaTag() = MangaTag(
key = this.key,
title = this.title.toTitleCase(),
source = MangaSource.valueOf(this.source),
)
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
id = this.id,
title = this.title,
altTitle = this.altTitle,
state = this.state?.let { MangaState.valueOf(it) },
rating = this.rating,
isNsfw = this.isNsfw,
url = this.url,
publicUrl = this.publicUrl,
coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl,
author = this.author,
source = MangaSource.valueOf(this.source),
tags = tags
)
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
fun TrackLogWithManga.toTrackingLogItem() = TrackingLogItem(
id = trackLog.id,
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
manga = manga.toManga(tags.toMangaTags()),
createdAt = Date(trackLog.createdAt)
)
// Model to entity
fun Manga.toEntity() = MangaEntity(
id = id,
url = url,
publicUrl = publicUrl,
source = source.name,
largeCoverUrl = largeCoverUrl,
coverUrl = coverUrl,
altTitle = altTitle,
rating = rating,
isNsfw = isNsfw,
state = state?.name,
title = title,
author = author,
)
fun MangaTag.toEntity() = TagEntity(
title = title,
key = key,
source = source.name,
id = "${key}_${source.name}".longHashCode()
)
fun Collection<MangaTag>.toEntities() = map(MangaTag::toEntity)
// Other
@Suppress("FunctionName")
fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {
SortOrder.valueOf(name)
}.getOrDefault(fallback)

View File

@@ -3,10 +3,6 @@ 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
@Entity(tableName = "manga")
class MangaEntity(
@@ -16,46 +12,11 @@ class MangaEntity(
@ColumnInfo(name = "alt_title") val altTitle: String?,
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "public_url") val publicUrl: String,
@ColumnInfo(name = "rating") val rating: Float, //normalized value [0..1] or -1
@ColumnInfo(name = "rating") val rating: Float, // normalized value [0..1] or -1
@ColumnInfo(name = "nsfw") val isNsfw: Boolean,
@ColumnInfo(name = "cover_url") val coverUrl: String,
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
@ColumnInfo(name = "state") val state: String?,
@ColumnInfo(name = "author") val author: String?,
@ColumnInfo(name = "source") val source: String
) {
fun toManga(tags: Set<MangaTag> = emptySet()) = Manga(
id = this.id,
title = this.title,
altTitle = this.altTitle,
state = this.state?.let { MangaState.valueOf(it) },
rating = this.rating,
isNsfw = this.isNsfw,
url = this.url,
publicUrl = this.publicUrl,
coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl,
author = this.author,
source = MangaSource.valueOf(this.source),
tags = tags
)
companion object {
fun from(manga: Manga) = MangaEntity(
id = manga.id,
url = manga.url,
publicUrl = manga.publicUrl,
source = manga.source.name,
largeCoverUrl = manga.largeCoverUrl,
coverUrl = manga.coverUrl,
altTitle = manga.altTitle,
rating = manga.rating,
isNsfw = manga.isNsfw,
state = manga.state?.name,
title = manga.title,
author = manga.author
)
}
}
)

View File

@@ -6,13 +6,15 @@ import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "preferences", foreignKeys = [
tableName = "preferences",
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)]
)
]
)
class MangaPrefsEntity(
@PrimaryKey(autoGenerate = false)

View File

@@ -5,7 +5,8 @@ import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"], foreignKeys = [
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"],
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.utils.ext.mapToSet
class MangaWithTags(
@Embedded val manga: MangaEntity,
@@ -12,10 +11,5 @@ class MangaWithTags(
entityColumn = "tag_id",
associateBy = Junction(MangaTagsEntity::class)
)
val tags: List<TagEntity>
) {
fun toManga() = manga.toManga(tags.mapToSet {
it.toMangaTag()
})
}
val tags: List<TagEntity>,
)

View File

@@ -3,9 +3,6 @@ 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
@Entity(tableName = "tags")
class TagEntity(
@@ -14,21 +11,4 @@ class TagEntity(
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "key") val key: String,
@ColumnInfo(name = "source") val source: String
) {
fun toMangaTag() = MangaTag(
key = this.key,
title = this.title,
source = MangaSource.valueOf(this.source)
)
companion object {
fun fromMangaTag(tag: MangaTag) = TagEntity(
title = tag.title,
key = tag.key,
source = tag.source.name,
id = "${tag.key}_${tag.source.name}".longHashCode()
)
}
}
)

View File

@@ -6,7 +6,8 @@ import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "tracks", foreignKeys = [
tableName = "tracks",
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],

View File

@@ -6,7 +6,8 @@ import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "track_logs", foreignKeys = [
tableName = "track_logs",
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
@@ -20,5 +21,5 @@ class TrackLogEntity(
@ColumnInfo(name = "id") val id: Long = 0L,
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "chapters") val chapters: String,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
)

View File

@@ -3,9 +3,6 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.util.*
class TrackLogWithManga(
@Embedded val trackLog: TrackLogEntity,
@@ -19,13 +16,5 @@ class TrackLogWithManga(
entityColumn = "tag_id",
associateBy = Junction(MangaTagsEntity::class)
)
val tags: List<TagEntity>
) {
fun toTrackingLogItem() = TrackingLogItem(
id = trackLog.id,
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
manga = manga.toManga(tags.mapToSet { x -> x.toMangaTag() }),
createdAt = Date(trackLog.createdAt)
)
}
val tags: List<TagEntity>,
)

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,14 +0,0 @@
package org.koitharu.kotatsu.core.exceptions
import org.json.JSONArray
import org.koitharu.kotatsu.utils.ext.map
class GraphQLException(private val errors: JSONArray) : RuntimeException() {
val messages = errors.map {
it.getString("message")
}
override val message: String
get() = messages.joinToString("\n")
}

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,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 +1,28 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet
@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 {
fun Manga.withoutChapters() = if (chapters.isNullOrEmpty()) {
this
} else {
Manga(
id = id,
title = title,
altTitle = altTitle,
url = url,
publicUrl = publicUrl,
rating = rating,
isNsfw = isNsfw,
coverUrl = coverUrl,
tags = tags,
state = state,
author = author,
largeCoverUrl = largeCoverUrl,
description = description,
chapters = null,
source = source,
)
}
companion object {
const val NO_RATING = -1f
}
}
fun Collection<Manga>.ids() = mapToSet { it.id }

View File

@@ -1,21 +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?,
val uploadDate: Long,
val branch: String?,
val source: MangaSource,
) : Parcelable, Comparable<MangaChapter> {
override fun compareTo(other: MangaChapter): Int {
return number.compareTo(other.number)
}
}

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

@@ -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?,
val source: MangaSource,
) : Parcelable

View File

@@ -1,52 +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),
MANGADEX("MangaDex", null, MangaDexRepository::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,95 @@
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, withChapters: Boolean) {
out.writeLong(id)
out.writeString(title)
out.writeString(altTitle)
out.writeString(url)
out.writeString(publicUrl)
out.writeFloat(rating)
ParcelCompat.writeBoolean(out, isNsfw)
out.writeString(coverUrl)
out.writeString(largeCoverUrl)
out.writeString(description)
out.writeParcelable(ParcelableMangaTags(tags), flags)
out.writeSerializable(state)
out.writeString(author)
if (withChapters) {
out.writeParcelable(chapters?.let(::ParcelableMangaChapters), flags)
} else {
out.writeString(null)
}
out.writeSerializable(source)
}
fun Parcel.readManga() = Manga(
id = readLong(),
title = requireNotNull(readString()),
altTitle = readString(),
url = requireNotNull(readString()),
publicUrl = requireNotNull(readString()),
rating = readFloat(),
isNsfw = ParcelCompat.readBoolean(this),
coverUrl = requireNotNull(readString()),
largeCoverUrl = readString(),
description = readString(),
tags = requireNotNull(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,48 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.Manga
// Limits to avoid TransactionTooLargeException
private const val MAX_SAFE_SIZE = 1024 * 512 // Assume that 512 kb is safe parcel size
private const val MAX_SAFE_CHAPTERS_COUNT = 40 // this is 100% safe
class ParcelableManga(
val manga: Manga,
) : Parcelable {
constructor(parcel: Parcel) : this(parcel.readManga())
override fun writeToParcel(parcel: Parcel, flags: Int) {
val chapters = manga.chapters
if (chapters == null || chapters.size <= MAX_SAFE_CHAPTERS_COUNT) {
// fast path
manga.writeToParcel(parcel, flags, withChapters = true)
return
}
val tempParcel = Parcel.obtain()
manga.writeToParcel(tempParcel, flags, withChapters = true)
val size = tempParcel.dataSize()
if (size < MAX_SAFE_SIZE) {
parcel.appendFrom(tempParcel, 0, size)
} else {
manga.writeToParcel(parcel, flags, withChapters = false)
}
tempParcel.recycle()
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ParcelableManga> {
override fun createFromParcel(parcel: Parcel): ParcelableManga {
return ParcelableManga(parcel)
}
override fun newArray(size: Int): Array<ParcelableManga?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -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,5 +1,7 @@
package org.koitharu.kotatsu.core.network
import okhttp3.CacheControl
object CommonHeaders {
const val REFERER = "Referer"
@@ -7,4 +9,7 @@ object CommonHeaders {
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

@@ -1,59 +0,0 @@
package org.koitharu.kotatsu.core.network
import android.util.Log
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okio.Buffer
import java.io.IOException
import java.nio.charset.StandardCharsets
class CurlLoggingInterceptor(
private val extraCurlOptions: String? = null,
) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
var compressed = false
val curlCmd = StringBuilder("curl")
if (extraCurlOptions != null) {
curlCmd.append(" ").append(extraCurlOptions)
}
curlCmd.append(" -X ").append(request.method)
val headers = request.headers
var i = 0
val count = headers.size
while (i < count) {
val name = headers.name(i)
val value = headers.value(i)
if ("Accept-Encoding".equals(name, ignoreCase = true) && "gzip".equals(value,
ignoreCase = true)
) {
compressed = true
}
curlCmd.append(" -H " + "\"").append(name).append(": ").append(value).append("\"")
i++
}
val requestBody = request.body
if (requestBody != null) {
val buffer = Buffer()
requestBody.writeTo(buffer)
val contentType = requestBody.contentType()
val charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
curlCmd.append(" --data $'")
.append(buffer.readString(charset).replace("\n", "\\n"))
.append("'")
}
curlCmd.append(if (compressed) " --compressed " else " ").append(request.url)
Log.d(TAG, "╭--- cURL (" + request.url + ")")
Log.d(TAG, curlCmd.toString())
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

@@ -1,33 +1,27 @@
package org.koitharu.kotatsu.core.network
import java.util.concurrent.TimeUnit
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.utils.DownloadManagerHelper
import java.util.concurrent.TimeUnit
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.MangaLoaderContext
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) {
addNetworkInterceptor(CurlLoggingInterceptor())
}
}.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
@@ -24,7 +24,7 @@ class ShortcutsRepository(
private val context: Context,
private val coil: ImageLoader,
private val historyRepository: HistoryRepository,
private val mangaRepository: MangaDataRepository
private val mangaRepository: MangaDataRepository,
) {
private val iconSize by lazy {
@@ -69,7 +69,7 @@ class ShortcutsRepository(
.setLongLabel(manga.title)
.setIcon(icon)
.setIntent(
ReaderActivity.newIntent(context, manga.id, null)
ReaderActivity.newIntent(context, manga.id)
.setAction(ReaderActivity.ACTION_MANGA_READ)
)
}

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,38 +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()) }
factory<MangaRepository>(named(MangaSource.MANGADEX)) { MangaDexRepository(get()) }
}

View File

@@ -1,88 +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).removePrefix("www."))
} else {
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,259 +0,0 @@
package org.koitharu.kotatsu.core.parser.site
import androidx.collection.ArraySet
import org.json.JSONArray
import org.json.JSONObject
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.map
import org.koitharu.kotatsu.utils.ext.mapIndexed
import org.koitharu.kotatsu.utils.ext.stringIterator
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 filters = tags?.takeUnless { it.isEmpty() }?.joinToString(
separator = ",",
prefix = "genres: [",
postfix = "]"
) { "\"it.key\"" }.orEmpty()
val array = apiCall(
"""
getMediaList(offset: $offset, limit: 20, mediaType: manga, filters: {$filters}) {
docs {
mediaId
title {
be
alt
}
rating
poster
genres
slug
mediaType
status
}
}
""".trimIndent()
).getJSONObject("getMediaList").getJSONArray("docs")
return array.map { jo ->
val mediaId = jo.getString("mediaId")
val title = jo.getJSONObject("title")
val href = "${jo.getString("mediaType")}/${jo.getString("slug")}"
Manga(
id = generateUid(mediaId),
title = title.getString("be"),
coverUrl = jo.getString("poster").removePrefix("/cdn")
.withDomain("cdn") + "?width=200&height=280",
altTitle = title.getString("alt").takeUnless(String::isEmpty),
author = null,
rating = jo.getDouble("rating").toFloat() / 10f,
url = href,
publicUrl = "https://${getDomain()}/${href}",
tags = jo.getJSONArray("genres").mapToTags(),
state = when (jo.getString("status")) {
"ongoing" -> MangaState.ONGOING
"finished" -> MangaState.FINISHED
else -> null
},
source = source,
)
}
}
override suspend fun getDetails(manga: Manga): Manga {
val (type, slug) = manga.url.split('/')
val details = apiCall(
"""
media(mediaType: $type, slug: "$slug") {
mediaId
title {
be
alt
}
description {
be
}
status
poster
rating
genres
}
""".trimIndent()
).getJSONObject("media")
val title = details.getJSONObject("title")
val poster = details.getString("poster").removePrefix("/cdn")
.withDomain("cdn")
val chapters = apiCall(
"""
chapters(mediaId: "${details.getString("mediaId")}") {
id
chapter
released
}
""".trimIndent()
).getJSONArray("chapters")
return manga.copy(
title = title.getString("be"),
altTitle = title.getString("alt"),
coverUrl = "$poster?width=200&height=280",
largeCoverUrl = poster,
description = details.getJSONObject("description").getString("be"),
rating = details.getDouble("rating").toFloat() / 10f,
tags = details.getJSONArray("genres").mapToTags(),
state = when (details.getString("status")) {
"ongoing" -> MangaState.ONGOING
"finished" -> MangaState.FINISHED
else -> null
},
chapters = chapters.map { jo ->
val number = jo.getInt("chapter")
MangaChapter(
id = generateUid(jo.getString("id")),
name = "Глава $number",
number = number,
url = "${manga.url}/read/$number",
scanlator = null,
uploadDate = jo.getLong("released"),
branch = null,
source = source,
)
}
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val (_, slug, _, number) = chapter.url.split('/')
val chapterJson = apiCall(
"""
chapter(slug: "$slug", chapter: $number) {
id
images {
large
thumbnail
}
}
""".trimIndent()
).getJSONObject("chapter")
val pages = chapterJson.getJSONArray("images")
val chapterUrl = "https://${getDomain()}/${chapter.url}"
return pages.mapIndexed { i, jo ->
MangaPage(
id = generateUid("${chapter.url}/$i"),
url = jo.getString("large"),
referer = chapterUrl,
preview = jo.getString("thumbnail"),
source = source,
)
}
}
override suspend fun getTags(): Set<MangaTag> {
val json = apiCall(
"""
getFilters(mediaType: manga) {
genres
}
""".trimIndent()
)
val array = json.getJSONObject("getFilters").getJSONArray("genres")
return array.mapToTags()
}
private suspend fun search(query: String): List<Manga> {
val json = apiCall(
"""
search(query: "$query", limit: 40) {
id
title {
be
en
}
poster
url
type
}
""".trimIndent()
)
val array = json.getJSONArray("search")
return array.map { jo ->
val mediaId = jo.getString("id")
val title = jo.getJSONObject("title")
val href = "${jo.getString("type").lowercase()}/${jo.getString("url")}"
Manga(
id = generateUid(mediaId),
title = title.getString("be"),
coverUrl = jo.getString("poster").removePrefix("/cdn")
.withDomain("cdn") + "?width=200&height=280",
altTitle = title.getString("en").takeUnless(String::isEmpty),
author = null,
rating = Manga.NO_RATING,
url = href,
publicUrl = "https://${getDomain()}/${href}",
tags = emptySet(),
state = null,
source = source,
)
}
}
private suspend fun apiCall(request: String): JSONObject {
return loaderContext.graphQLQuery("https://api.${getDomain()}/graphql", request)
.getJSONObject("data")
}
private fun JSONArray.mapToTags(): Set<MangaTag> {
fun toTitle(slug: String): String {
val builder = StringBuilder(slug)
var capitalize = true
for ((i, c) in builder.withIndex()) {
when {
c == '-' -> {
builder.setCharAt(i, ' ')
capitalize = true
}
capitalize -> {
builder.setCharAt(i, c.uppercaseChar())
capitalize = false
}
}
}
return builder.toString()
}
val result = ArraySet<MangaTag>(length())
stringIterator().forEach {
result.add(
MangaTag(
title = toTitle(it),
key = it,
source = source,
)
)
}
return result
}
}

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