Compare commits

...

100 Commits
v5.1.4 ... v5.2

Author SHA1 Message Date
Koitharu
cd23b044df Merge branch 'devel' 2023-06-13 19:08:45 +03:00
Koitharu
4922881343 Update parsers 2023-06-13 18:41:32 +03:00
Koitharu
ff0d04bea6 Add current screen info to crash reports 2023-06-13 18:32:11 +03:00
Koitharu
97de629c3b Fix BottomSheet duplication 2023-06-13 10:45:29 +03:00
Koitharu
7b482e5bcf Fix shortcuts icons size 2023-06-12 16:16:33 +03:00
Koitharu
fd575b8131 Merge branch 'devel' 2023-06-12 12:57:29 +03:00
Koitharu
c77e023bef Fix color filter preview 2023-06-12 12:54:24 +03:00
Koitharu
a3cf52859b Reader refactoring 2023-06-12 12:47:05 +03:00
Koitharu
5e55bce529 Use indirect image url in bookmarks 2023-06-12 10:54:12 +03:00
Koitharu
b1ba70bf77 Fix settings issues 2023-06-10 15:48:30 +03:00
Koitharu
b930272221 Fix settings background 2023-06-09 16:34:29 +03:00
Koitharu
75305c0b94 Fix fast scroll issues 2023-06-09 16:18:58 +03:00
Koitharu
24b16e2ce2 Fix debug/release sources packages 2023-06-08 21:15:32 +03:00
qrynill
0ccbba6787 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 81.8% (356 of 435 strings)

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

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

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

Translated using Weblate (Russian)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (435 of 435 strings)

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

Added translation using Weblate (Vietnamese)

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (434 of 434 strings)

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

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

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

Translated using Weblate (French)

Currently translated at 100.0% (430 of 430 strings)

Co-authored-by: frablock <franon1.frablock29@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-06-05 12:56:26 +03:00
FateXBlood
73217b8e11 Translated using Weblate (Nepali)
Currently translated at 34.1% (144 of 422 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (419 of 422 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (422 of 422 strings)

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

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

Translated using Weblate (Russian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (French)

Currently translated at 100.0% (421 of 421 strings)

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

Translated using Weblate (Turkish)

Currently translated at 99.7% (416 of 417 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (417 of 417 strings)

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

Translated using Weblate (Odia)

Currently translated at 5.5% (23 of 416 strings)

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

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

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

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Japanese)

Currently translated at 91.8% (382 of 416 strings)

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

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

Translated using Weblate (Belarusian)

Currently translated at 100.0% (416 of 416 strings)

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

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

Translated using Weblate (French)

Currently translated at 100.0% (430 of 430 strings)

Co-authored-by: frablock <franon1.frablock29@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-06-05 11:53:42 +02:00
FateXBlood
61a98f54b9 Translated using Weblate (Nepali)
Currently translated at 34.1% (144 of 422 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (419 of 422 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (422 of 422 strings)

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

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

Translated using Weblate (Russian)

Currently translated at 100.0% (431 of 431 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (431 of 431 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (French)

Currently translated at 100.0% (421 of 421 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-05 11:53:40 +02:00
Bai
ff2cf9d18a Translated using Weblate (Turkish)
Currently translated at 100.0% (431 of 431 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (416 of 417 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (423 of 423 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (421 of 421 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (417 of 417 strings)

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

Translated using Weblate (Odia)

Currently translated at 5.5% (23 of 416 strings)

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

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

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

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Japanese)

Currently translated at 91.8% (382 of 416 strings)

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

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

Translated using Weblate (Belarusian)

Currently translated at 100.0% (416 of 416 strings)

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

3
.weblate Normal file
View File

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

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 548
versionName '5.1.4'
versionCode 552
versionName '5.2'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -42,6 +42,7 @@ android {
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
main.java.srcDirs += 'src/main/kotlin/'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -59,7 +60,7 @@ android {
}
lint {
abortOnError true
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged'
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled'
}
testOptions {
unitTests.includeAndroidResources true
@@ -78,17 +79,17 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:44e28b40d3') {
implementation('com.github.KotatsuApp:kotatsu-parsers:f732582d55') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.activity:activity-ktx:1.7.2'
implementation 'androidx.fragment:fragment-ktx:1.5.7'
implementation 'androidx.fragment:fragment-ktx:1.6.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-service:2.6.1'
@@ -105,7 +106,7 @@ dependencies {
implementation 'androidx.work:work-runtime-ktx:2.8.1'
//noinspection GradleDependency
implementation('com.google.guava:guava:31.1-android') {
implementation('com.google.guava:guava:32.0.0-android') {
exclude group: 'com.google.guava', module: 'failureaccess'
exclude group: 'org.checkerframework', module: 'checker-qual'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'

View File

@@ -8,7 +8,7 @@
public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...);
}
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
-keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.**

View File

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

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.data.HistoryRepository
import java.io.File
import javax.inject.Inject

View File

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

View File

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

View File

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

View File

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

View File

@@ -102,6 +102,10 @@
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"

View File

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

View File

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

View File

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

View File

@@ -1,30 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.RecyclerView
class ScrollListenerInvalidationObserver(
private val recyclerView: RecyclerView,
private val scrollListener: RecyclerView.OnScrollListener,
) : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
super.onChanged()
invalidateScroll()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
invalidateScroll()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
invalidateScroll()
}
private fun invalidateScroll() {
recyclerView.post {
scrollListener.onScrolled(recyclerView, 0, 0)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,37 +0,0 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
class MangaCategoriesAdapter(
clickListener: OnListItemClickListener<MangaCategoryItem>
) : AsyncListDifferDelegationAdapter<MangaCategoryItem>(DiffCallback()) {
init {
delegatesManager.addDelegate(mangaCategoryAD(clickListener))
}
private class DiffCallback : DiffUtil.ItemCallback<MangaCategoryItem>() {
override fun areItemsTheSame(
oldItem: MangaCategoryItem,
newItem: MangaCategoryItem
): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(
oldItem: MangaCategoryItem,
newItem: MangaCategoryItem
): Boolean = oldItem == newItem
override fun getChangePayload(
oldItem: MangaCategoryItem,
newItem: MangaCategoryItem
): Any? {
if (oldItem.isChecked != newItem.isChecked) {
return newItem.isChecked
}
return super.getChangePayload(oldItem, newItem)
}
}
}

View File

@@ -1,8 +0,0 @@
package org.koitharu.kotatsu.list.domain
interface ListExtraProvider {
suspend fun getCounter(mangaId: Long): Int
suspend fun getProgress(mangaId: Long): Float
}

View File

@@ -1,21 +0,0 @@
package org.koitharu.kotatsu.list.ui.filter
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
class FilterAdapter(
listener: OnFilterChangedListener,
listListener: ListListener<FilterItem>,
) : AsyncListDifferDelegationAdapter<FilterItem>(
FilterDiffCallback(),
filterSortDelegate(listener),
filterTagDelegate(listener),
filterHeaderDelegate(),
filterLoadingDelegate(),
filterErrorDelegate(),
) {
init {
differ.addListListener(listListener)
}
}

View File

@@ -1,66 +0,0 @@
package org.koitharu.kotatsu.list.ui.filter
import android.widget.TextView
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
fun filterSortDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.Sort, FilterItem, ItemCheckableNewBinding>(
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }
) {
itemView.setOnClickListener {
listener.onSortItemClick(item)
}
bind {
binding.root.setText(item.order.titleRes)
binding.root.isChecked = item.isSelected
}
}
fun filterTagDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.Tag, FilterItem, ItemCheckableNewBinding>(
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }
) {
itemView.setOnClickListener {
listener.onTagItemClick(item)
}
bind {
binding.root.text = item.tag.title
binding.root.isChecked = item.isChecked
}
}
fun filterHeaderDelegate() = adapterDelegateViewBinding<FilterItem.Header, FilterItem, ItemFilterHeaderBinding>(
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
) {
bind {
binding.textViewTitle.setText(item.titleResId)
binding.badge.isVisible = if (item.counter == 0) {
false
} else {
binding.badge.text = item.counter.toString()
true
}
}
}
fun filterLoadingDelegate() = adapterDelegate<FilterItem.Loading, FilterItem>(R.layout.item_loading_footer) {}
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, FilterItem>(R.layout.item_sources_empty) {
bind {
(itemView as TextView).setText(item.textResId)
}
}

View File

@@ -1,91 +0,0 @@
package org.koitharu.kotatsu.list.ui.filter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.LinearLayoutManager
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.util.CollapseActionViewCallback
import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels
class FilterBottomSheet :
BaseBottomSheet<SheetFilterBinding>(),
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener,
AsyncListDiffer.ListListener<FilterItem> {
private val viewModel by parentFragmentViewModels<RemoteListViewModel>()
private var collapsibleActionViewCallback: CollapseActionViewCallback? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
return SheetFilterBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = FilterAdapter(viewModel, this)
binding.recyclerView.adapter = adapter
viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems)
initOptionsMenu()
}
override fun onDestroyView() {
super.onDestroyView()
collapsibleActionViewCallback = null
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
setExpanded(isExpanded = true, isLocked = true)
collapsibleActionViewCallback?.onMenuItemActionExpand(item)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
val searchView = (item.actionView as? SearchView) ?: return false
searchView.setQuery("", false)
searchView.post { setExpanded(isExpanded = false, isLocked = false) }
collapsibleActionViewCallback?.onMenuItemActionCollapse(item)
return true
}
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.filterSearch(newText?.trim().orEmpty())
return true
}
override fun onCurrentListChanged(previousList: MutableList<FilterItem>, currentList: MutableList<FilterItem>) {
if (currentList.size > previousList.size && view != null) {
(binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
}
}
private fun initOptionsMenu() {
binding.headerBar.inflateMenu(R.menu.opt_filter)
val searchMenuItem = binding.headerBar.menu.findItem(R.id.action_search)
searchMenuItem.setOnActionExpandListener(this)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title
collapsibleActionViewCallback = CollapseActionViewCallback(searchMenuItem).also {
onBackPressedDispatcher.addCallback(it)
}
}
companion object {
private const val TAG = "FilterBottomSheet"
fun show(fm: FragmentManager) = FilterBottomSheet().show(fm, TAG)
}
}

View File

@@ -1,59 +0,0 @@
package org.koitharu.kotatsu.list.ui.filter
import androidx.recyclerview.widget.DiffUtil
class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
return when {
oldItem === newItem -> true
oldItem.javaClass != newItem.javaClass -> false
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
oldItem.titleResId == newItem.titleResId
}
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
oldItem.tag == newItem.tag
}
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
oldItem.order == newItem.order
}
oldItem is FilterItem.Error && newItem is FilterItem.Error -> {
oldItem.textResId == newItem.textResId
}
else -> false
}
}
override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
return when {
oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
oldItem.counter == newItem.counter
}
oldItem is FilterItem.Error && newItem is FilterItem.Error -> true
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
oldItem.isChecked == newItem.isChecked
}
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
oldItem.isSelected == newItem.isSelected
}
else -> false
}
}
override fun getChangePayload(oldItem: FilterItem, newItem: FilterItem): Any? {
val hasPayload = when {
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
oldItem.isChecked != newItem.isChecked
}
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
oldItem.isSelected != newItem.isSelected
}
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
oldItem.counter != newItem.counter
}
else -> false
}
return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem)
}
}

View File

@@ -1,29 +0,0 @@
package org.koitharu.kotatsu.list.ui.filter
import androidx.annotation.StringRes
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
sealed interface FilterItem {
class Header(
@StringRes val titleResId: Int,
val counter: Int,
) : FilterItem
class Sort(
val order: SortOrder,
val isSelected: Boolean,
) : FilterItem
class Tag(
val tag: MangaTag,
val isChecked: Boolean,
) : FilterItem
object Loading : FilterItem
class Error(
@StringRes val textResId: Int,
) : FilterItem
}

View File

@@ -1,6 +0,0 @@
package org.koitharu.kotatsu.list.ui.model
object LoadingFooter : ListModel {
override fun equals(other: Any?): Boolean = other === LoadingFooter
}

View File

@@ -1,194 +0,0 @@
package org.koitharu.kotatsu.local.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader2
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.io.IOException
import java.util.LinkedList
import javax.inject.Inject
@HiltViewModel
class LocalListViewModel @Inject constructor(
private val repository: LocalMangaRepository,
private val historyRepository: HistoryRepository,
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
private val tagHighlighter: MangaTagHighlighter,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider {
val onMangaRemoved = SingleLiveEvent<Unit>()
val sortOrder = MutableLiveData(settings.localListOrder)
private val listError = MutableStateFlow<Throwable?>(null)
private val mangaList = MutableStateFlow<List<Manga>?>(null)
private val selectedTags = MutableStateFlow<Set<MangaTag>>(emptySet())
private var refreshJob: Job? = null
override val content = combine(
mangaList,
listModeFlow,
sortOrder.asFlow(),
selectedTags,
listError,
) { list, mode, order, tags, error ->
when {
error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState)
list.isEmpty() -> listOf(
EmptyState(
icon = R.drawable.ic_empty_local,
textPrimary = R.string.text_local_holder_primary,
textSecondary = R.string.text_local_holder_secondary,
actionStringRes = R.string._import,
),
)
else -> buildList(list.size + 1) {
add(createHeader(list, tags, order))
list.toUi(this, mode, this@LocalListViewModel, tagHighlighter)
}
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
init {
onRefresh()
launchJob(Dispatchers.Default) {
localStorageChanges
.collectLatest {
if (refreshJob?.isActive != true) {
doRefresh()
}
}
}
}
override fun onUpdateFilter(tags: Set<MangaTag>) {
selectedTags.value = tags
onRefresh()
}
override fun onRefresh() {
val prevJob = refreshJob
refreshJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
doRefresh()
}
}
override fun onRetry() = onRefresh()
fun setSortOrder(value: SortOrder) {
sortOrder.value = value
settings.localListOrder = value
onRefresh()
}
fun delete(ids: Set<Long>) {
launchLoadingJob(Dispatchers.Default) {
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
for (manga in itemsToRemove) {
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file")
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
mangaList.update { list ->
list?.filterNot { it.id == manga.id }
}
}
onMangaRemoved.emitCall(Unit)
}
}
private suspend fun doRefresh() {
try {
listError.value = null
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
listError.value = e
}
}
private fun createHeader(mangaList: List<Manga>, selectedTags: Set<MangaTag>, order: SortOrder): ListHeader2 {
val tags = HashMap<MangaTag, Int>()
for (item in mangaList) {
for (tag in item.tags) {
tags[tag] = tags[tag]?.plus(1) ?: 1
}
}
val topTags = tags.entries.sortedByDescending { it.value }.take(6)
val chips = LinkedList<ChipsView.ChipModel>()
for ((tag, _) in topTags) {
val model = ChipsView.ChipModel(
tint = 0,
title = tag.title,
isCheckable = true,
isChecked = tag in selectedTags,
data = tag,
)
if (model.isChecked) {
chips.addFirst(model)
} else {
chips.addLast(model)
}
}
return ListHeader2(
chips = chips,
sortOrder = order,
hasSelectedTags = selectedTags.isNotEmpty(),
)
}
override suspend fun getCounter(mangaId: Long): Int {
return if (settings.isTrackerEnabled) {
trackingRepository.getNewChaptersCount(mangaId)
} else {
0
}
}
override suspend fun getProgress(mangaId: Long): Float {
return if (settings.isReadingIndicatorsEnabled) {
historyRepository.getProgress(mangaId)
} else {
PROGRESS_NONE
}
}
}

View File

@@ -1,8 +0,0 @@
package org.koitharu.kotatsu.main.ui.owners
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
interface NoModalBottomSheetOwner {
val bsHeader: BottomSheetHeaderBar?
}

View File

@@ -1,78 +0,0 @@
package org.koitharu.kotatsu.reader.ui.colorfilter
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.emitValue
import javax.inject.Inject
@HiltViewModel
class ColorFilterConfigViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val mangaDataRepository: MangaDataRepository,
) : BaseViewModel() {
private val manga = checkNotNull(savedStateHandle.get<ParcelableManga>(EXTRA_MANGA)?.manga)
private var initialColorFilter: ReaderColorFilter? = null
val colorFilter = MutableLiveData<ReaderColorFilter?>(null)
val onDismiss = SingleLiveEvent<Unit>()
val preview = MutableLiveData<MangaPage?>(null)
val isChanged: Boolean
get() = colorFilter.value != initialColorFilter
init {
val page = checkNotNull(
savedStateHandle.get<ParcelableMangaPages>(ColorFilterConfigActivity.EXTRA_PAGES)?.pages?.firstOrNull(),
)
launchLoadingJob {
initialColorFilter = mangaDataRepository.getColorFilter(manga.id)
colorFilter.value = initialColorFilter
}
launchLoadingJob(Dispatchers.Default) {
val repository = mangaRepositoryFactory.create(page.source)
val url = repository.getPageUrl(page)
preview.emitValue(
MangaPage(
id = page.id,
url = url,
preview = page.preview,
source = page.source,
),
)
}
}
fun setBrightness(brightness: Float) {
val cf = colorFilter.value
colorFilter.value = ReaderColorFilter(brightness, cf?.contrast ?: 0f).takeUnless { it.isEmpty }
}
fun setContrast(contrast: Float) {
val cf = colorFilter.value
colorFilter.value = ReaderColorFilter(cf?.brightness ?: 0f, contrast).takeUnless { it.isEmpty }
}
fun reset() {
colorFilter.value = null
}
fun save() {
launchLoadingJob(Dispatchers.Default) {
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
onDismiss.emitCall(Unit)
}
}
}

View File

@@ -1,8 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails
import org.koitharu.kotatsu.parsers.model.MangaPage
fun interface OnPageSelectListener {
fun onPageSelected(page: MangaPage)
}

View File

@@ -1,11 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
data class PageThumbnail(
val number: Int,
val isCurrent: Boolean,
val repository: MangaRepository,
val page: MangaPage
)

View File

@@ -1,146 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.GridLayoutManager
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.SheetPagesBinding
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
@AndroidEntryPoint
class PagesThumbnailsSheet :
BaseBottomSheet<SheetPagesBinding>(),
OnListItemClickListener<MangaPage>,
BottomSheetHeaderBar.OnExpansionChangeListener {
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
@Inject
lateinit var pageLoader: PageLoader
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
private lateinit var thumbnails: List<PageThumbnail>
private var spanResolver: MangaListSpanResolver? = null
private var currentPageIndex = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pages = arguments?.getParcelableCompat<ParcelableMangaPages>(ARG_PAGES)?.pages
if (pages.isNullOrEmpty()) {
dismissAllowingStateLoss()
return
}
currentPageIndex = requireArguments().getInt(ARG_CURRENT, currentPageIndex)
val repository = mangaRepositoryFactory.create(pages.first().source)
thumbnails = pages.mapIndexed { i, x ->
PageThumbnail(
number = i + 1,
isCurrent = i == currentPageIndex,
repository = repository,
page = x,
)
}
}
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
return SheetPagesBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
spanResolver = MangaListSpanResolver(view.resources)
with(binding.headerBar) {
title = arguments?.getString(ARG_TITLE)
subtitle = null
addOnExpansionChangeListener(this@PagesThumbnailsSheet)
}
with(binding.recyclerView) {
addItemDecoration(
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
)
adapter = PageThumbnailAdapter(
dataSet = thumbnails,
coil = coil,
scope = viewLifecycleScope,
loader = pageLoader,
clickListener = this@PagesThumbnailsSheet,
)
addOnLayoutChangeListener(spanResolver)
spanResolver?.setGridSize(settings.gridSize / 100f, this)
if (currentPageIndex > 0) {
val offset = resources.getDimensionPixelOffset(R.dimen.preferred_grid_width)
(layoutManager as GridLayoutManager).scrollToPositionWithOffset(currentPageIndex, offset)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
spanResolver = null
}
override fun onItemClick(item: MangaPage, view: View) {
(
(parentFragment as? OnPageSelectListener)
?: (activity as? OnPageSelectListener)
)?.run {
onPageSelected(item)
dismiss()
}
}
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
if (isExpanded) {
headerBar.subtitle = resources.getQuantityString(
R.plurals.pages,
thumbnails.size,
thumbnails.size,
)
} else {
headerBar.subtitle = null
}
}
companion object {
private const val ARG_PAGES = "pages"
private const val ARG_TITLE = "title"
private const val ARG_CURRENT = "current"
private const val TAG = "PagesThumbnailsSheet"
fun show(fm: FragmentManager, pages: List<MangaPage>, title: String, currentPage: Int) =
PagesThumbnailsSheet().withArgs(3) {
putParcelable(ARG_PAGES, ParcelableMangaPages(pages))
putString(ARG_TITLE, title)
putInt(ARG_CURRENT, currentPage)
}.show(fm, TAG)
}
}

View File

@@ -1,91 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import android.graphics.drawable.Drawable
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import coil.size.Size
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
import com.google.android.material.R as materialR
fun pageThumbnailAD(
coil: ImageLoader,
scope: CoroutineScope,
loader: PageLoader,
clickListener: OnListItemClickListener<MangaPage>,
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) },
) {
var job: Job? = null
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
val thumbSize = Size(
width = gridWidth,
height = (gridWidth / 13f * 18f).toInt(),
)
suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) {
item.page.preview?.let { url ->
coil.execute(
ImageRequest.Builder(context)
.data(url)
.tag(item.page.source)
.size(thumbSize)
.scale(Scale.FILL)
.allowRgb565(true)
.build(),
).drawable
}?.let { drawable ->
return@withContext drawable
}
val file = loader.loadPage(item.page, force = false)
coil.execute(
ImageRequest.Builder(context)
.data(file)
.size(thumbSize)
.decodeRegion(0)
.allowRgb565(isLowRamDevice(context))
.build(),
).drawable
}
binding.root.setOnClickListener {
clickListener.onItemClick(item.page, itemView)
}
bind {
job?.cancel()
binding.imageViewThumb.setImageDrawable(null)
with(binding.textViewNumber) {
setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty)
setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary)
text = (item.number).toString()
}
job = scope.launch {
val drawable = runCatchingCancellable {
loadPageThumbnail(item)
}.getOrNull()
binding.imageViewThumb.setImageDrawable(drawable)
}
}
onViewRecycled {
job?.cancel()
job = null
binding.imageViewThumb.setImageDrawable(null)
}
}

View File

@@ -1,23 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import kotlinx.coroutines.CoroutineScope
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
class PageThumbnailAdapter(
dataSet: List<PageThumbnail>,
coil: ImageLoader,
scope: CoroutineScope,
loader: PageLoader,
clickListener: OnListItemClickListener<MangaPage>
) : ListDelegationAdapter<List<PageThumbnail>>() {
init {
delegatesManager.addDelegate(pageThumbnailAD(coil, scope, loader, clickListener))
setItems(dataSet)
}
}

View File

@@ -1,87 +0,0 @@
package org.koitharu.kotatsu.search.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.local.ui.LocalListFragment
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat
@AndroidEntryPoint
class MangaListActivity :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner {
override val appBar: AppBarLayout
get() = binding.appbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
val tags = intent.getParcelableExtraCompat<ParcelableMangaTags>(EXTRA_TAGS)?.tags
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val source = intent.getSerializableExtraCompat(EXTRA_SOURCE) ?: tags?.firstOrNull()?.source
if (source == null) {
finishAfterTransition()
return
}
title = if (source == MangaSource.LOCAL) getString(R.string.local_storage) else source.title
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
val fragment = if (source == MangaSource.LOCAL) {
LocalListFragment.newInstance()
} else {
RemoteListFragment.newInstance(source)
}
replace(R.id.container, fragment)
if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) {
runOnCommit(ApplyFilterRunnable(fragment, tags))
}
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
}
private class ApplyFilterRunnable(
private val fragment: RemoteListFragment,
private val tags: Set<MangaTag>,
) : Runnable {
override fun run() {
fragment.viewModel.applyFilter(tags)
}
}
companion object {
private const val EXTRA_TAGS = "tags"
private const val EXTRA_SOURCE = "source"
fun newIntent(context: Context, tags: Set<MangaTag>) = Intent(context, MangaListActivity::class.java)
.putExtra(EXTRA_TAGS, ParcelableMangaTags(tags))
fun newIntent(context: Context, source: MangaSource) = Intent(context, MangaListActivity::class.java)
.putExtra(EXTRA_SOURCE, source)
}
}

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.settings
import android.os.Bundle
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
class RootSettingsFragment : BasePreferenceFragment(R.string.settings) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_root)
}
}

View File

@@ -1,49 +0,0 @@
package org.koitharu.kotatsu.settings
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceHeaderFragmentCompat
import androidx.slidingpanelayout.widget.SlidingPaneLayout
import org.koitharu.kotatsu.R
class SettingsHeadersFragment : PreferenceHeaderFragmentCompat(), SlidingPaneLayout.PanelSlideListener {
private var currentTitle: CharSequence? = null
override fun onCreatePreferenceHeader(): PreferenceFragmentCompat = RootSettingsFragment()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
slidingPaneLayout.addPanelSlideListener(this)
}
override fun onPanelSlide(panel: View, slideOffset: Float) = Unit
override fun onPanelOpened(panel: View) {
activity?.title = currentTitle ?: getString(R.string.settings)
}
override fun onPanelClosed(panel: View) {
activity?.setTitle(R.string.settings)
}
fun setTitle(title: CharSequence?) {
currentTitle = title
if (slidingPaneLayout.width != 0 && slidingPaneLayout.isOpen) {
activity?.title = title
}
}
fun openFragment(fragment: Fragment) {
childFragmentManager.commit {
setReorderingAllowed(true)
replace(androidx.preference.R.id.preferences_detail, fragment)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
addToBackStack(null)
}
}
}

View File

@@ -1,133 +0,0 @@
package org.koitharu.kotatsu.settings
import android.os.Bundle
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
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.ext.awaitViewLifecycle
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.requireSerializable
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
@AndroidEntryPoint
class SourceSettingsFragment : BasePreferenceFragment(0) {
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
private lateinit var source: MangaSource
private var repository: RemoteMangaRepository? = null
private val exceptionResolver = ExceptionResolver(this)
override fun onCreate(savedInstanceState: Bundle?) {
source = requireArguments().requireSerializable(EXTRA_SOURCE)
repository = mangaRepositoryFactory.create(source) as? RemoteMangaRepository
super.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
setTitle(source.title)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.sharedPreferencesName = source.name
val repo = repository ?: return
addPreferencesFromResource(R.xml.pref_source)
addPreferencesFromRepository(repo)
findPreference<Preference>(KEY_AUTH)?.run {
val authProvider = repo.getAuthProvider()
isVisible = authProvider != null
isEnabled = authProvider?.isAuthorized == false
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(KEY_AUTH)?.run {
if (isVisible) {
loadUsername(viewLifecycleOwner, this)
}
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
KEY_AUTH -> {
startActivity(SourceAuthActivity.newIntent(preference.context, source))
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch {
runCatchingCancellable {
preference.summary = null
withContext(Dispatchers.Default) {
requireNotNull(repository?.getAuthProvider()?.getUsername())
}
}.onSuccess { username ->
preference.title = getString(R.string.logged_in_as, username)
}.onFailure { error ->
when {
error is AuthRequiredException -> Unit
ExceptionResolver.canResolve(error) -> {
ensureActive()
Snackbar.make(
listView ?: return@onFailure,
error.getDisplayMessage(preference.context.resources),
Snackbar.LENGTH_INDEFINITE,
).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
.show()
}
else -> preference.summary = error.getDisplayMessage(preference.context.resources)
}
error.printStackTraceDebug()
}
}
private fun resolveError(error: Throwable) {
view ?: return
viewLifecycleScope.launch {
if (exceptionResolver.resolve(error)) {
val pref = findPreference<Preference>(KEY_AUTH) ?: return@launch
val lifecycleOwner = awaitViewLifecycle()
loadUsername(lifecycleOwner, pref)
}
}
}
companion object {
private const val KEY_AUTH = "auth"
private const val EXTRA_SOURCE = "source"
fun newInstance(source: MangaSource) = SourceSettingsFragment().withArgs(1) {
putSerializable(EXTRA_SOURCE, source)
}
}
}

View File

@@ -1,53 +0,0 @@
package org.koitharu.kotatsu.settings.backup
import android.content.ActivityNotFoundException
import android.net.Uri
import android.os.Bundle
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
class BackupSettingsFragment :
BasePreferenceFragment(R.string.backup_restore),
ActivityResultCallback<Uri?> {
private val backupSelectCall = registerForActivityResult(
ActivityResultContracts.OpenDocument(),
this
)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_backup)
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_BACKUP -> {
BackupDialogFragment().show(childFragmentManager, BackupDialogFragment.TAG)
true
}
AppSettings.KEY_RESTORE -> {
try {
backupSelectCall.launch(arrayOf("*/*"))
} catch (e: ActivityNotFoundException) {
e.printStackTraceDebug()
Snackbar.make(
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT
).show()
}
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
override fun onActivityResult(result: Uri?) {
RestoreDialogFragment.newInstance(result ?: return)
.show(childFragmentManager, BackupDialogFragment.TAG)
}
}

View File

@@ -1,86 +0,0 @@
package org.koitharu.kotatsu.utils
import androidx.lifecycle.LiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
private const val DEFAULT_TIMEOUT = 5_000L
/**
* Similar to a CoroutineLiveData but optimized for using within infinite flows
*/
class FlowLiveData<T>(
private val flow: Flow<T>,
defaultValue: T,
context: CoroutineContext = EmptyCoroutineContext,
private val timeoutInMs: Long = DEFAULT_TIMEOUT,
) : LiveData<T>(defaultValue) {
private val scope = CoroutineScope(Dispatchers.Main.immediate + context + SupervisorJob(context[Job]))
private var job: Job? = null
private var cancellationJob: Job? = null
override fun onActive() {
super.onActive()
cancellationJob?.cancel()
cancellationJob = null
if (job?.isActive == true) {
return
}
job = scope.launch {
flow.collect(Collector())
}
}
override fun onInactive() {
super.onInactive()
cancellationJob?.cancel()
cancellationJob = scope.launch(Dispatchers.Main.immediate) {
delay(timeoutInMs)
if (!hasActiveObservers()) {
job?.cancel()
job = null
}
}
}
private inner class Collector : FlowCollector<T> {
private var previousValue: Any? = value
private val dispatcher = Dispatchers.Main.immediate
override suspend fun emit(value: T) {
if (previousValue != value) {
previousValue = value
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
withContext(dispatcher) {
setValue(value)
}
} else {
setValue(value)
}
}
}
}
}
fun <T> Flow<T>.asFlowLiveData(
context: CoroutineContext = EmptyCoroutineContext,
defaultValue: T,
timeoutInMs: Long = DEFAULT_TIMEOUT,
): LiveData<T> = FlowLiveData(this, defaultValue, context, timeoutInMs)
fun <T> StateFlow<T>.asFlowLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
): LiveData<T> = FlowLiveData(this, value, context, timeoutInMs)

View File

@@ -1,50 +0,0 @@
package org.koitharu.kotatsu.utils
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.EmptyCoroutineContext
class SingleLiveEvent<T> : LiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) {
if (pending.compareAndSet(true, false)) {
observer.onChanged(it)
}
}
}
override fun setValue(value: T) {
pending.set(true)
super.setValue(value)
}
@MainThread
fun call(newValue: T) {
setValue(newValue)
}
@AnyThread
fun postCall(newValue: T) {
postValue(newValue)
}
suspend fun emitCall(newValue: T) {
val dispatcher = Dispatchers.Main.immediate
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
withContext(dispatcher) {
setValue(newValue)
}
} else {
setValue(newValue)
}
}
}

View File

@@ -1,11 +0,0 @@
package org.koitharu.kotatsu.utils
import android.app.Activity
class TaggedActivityResult(
val tag: String,
val result: Int,
)
val TaggedActivityResult.isSuccess: Boolean
get() = this.result == Activity.RESULT_OK

View File

@@ -1,19 +0,0 @@
package org.koitharu.kotatsu.utils.ext
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
internal val RecyclerView.LayoutManager?.firstVisibleItemPosition
get() = when (this) {
is LinearLayoutManager -> findFirstVisibleItemPosition()
is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null)[0]
else -> 0
}
internal val RecyclerView.LayoutManager?.isLayoutReversed
get() = when (this) {
is LinearLayoutManager -> reverseLayout
is StaggeredGridLayoutManager -> reverseLayout
else -> false
}

View File

@@ -1,32 +0,0 @@
package org.koitharu.kotatsu.utils.ext
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.utils.BufferedObserver
import kotlin.coroutines.EmptyCoroutineContext
fun <T> LiveData<T>.requireValue(): T = checkNotNull(value) {
"LiveData value is null"
}
fun <T> LiveData<T>.observeWithPrevious(owner: LifecycleOwner, observer: BufferedObserver<T>) {
var previous: T? = null
this.observe(owner) {
observer.onChanged(it, previous)
previous = it
}
}
suspend fun <T> MutableLiveData<T>.emitValue(newValue: T) {
val dispatcher = Dispatchers.Main.immediate
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
withContext(dispatcher) {
value = newValue
}
} else {
value = newValue
}
}

View File

@@ -1,9 +0,0 @@
package org.koitharu.kotatsu.utils.ext
@Suppress("UNCHECKED_CAST")
fun <T> Class<T>.castOrNull(obj: Any?): T? {
if (obj == null || !isInstance(obj)) {
return null
}
return obj as T
}

View File

@@ -20,12 +20,12 @@ import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.utils.WorkServiceStopHelper
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import javax.inject.Inject
@HiltAndroidApp
@@ -67,6 +67,10 @@ class KotatsuApp : Application(), Configuration.Provider {
reportFormat = StringFormat.JSON
excludeMatchingSharedPreferencesKeys = listOf(
"sources_\\w+",
AppSettings.KEY_APP_PASSWORD,
AppSettings.KEY_PROXY_LOGIN,
AppSettings.KEY_PROXY_ADDRESS,
AppSettings.KEY_PROXY_PASSWORD,
)
httpSender {
uri = getString(R.string.url_error_report)
@@ -83,6 +87,7 @@ class KotatsuApp : Application(), Configuration.Provider {
ReportField.PHONE_MODEL,
ReportField.STACK_TRACE,
ReportField.CRASH_CONFIGURATION,
ReportField.CUSTOM_DATA,
ReportField.SHARED_PREFERENCES,
)
dialog {

View File

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

View File

@@ -1,7 +1,8 @@
package org.koitharu.kotatsu.bookmarks.domain
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
import org.koitharu.kotatsu.parsers.model.MangaPage
import java.util.Date
class Bookmark(
val manga: Manga,
@@ -14,6 +15,20 @@ class Bookmark(
val percent: Float,
) {
val directImageUrl: String?
get() = if (isImageUrlDirect()) imageUrl else null
fun toMangaPage() = MangaPage(
id = pageId,
url = imageUrl,
preview = null,
source = manga.source,
)
private fun isImageUrlDirect(): Boolean {
return imageUrl.substringAfterLast('.').length in 2..4
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -27,9 +42,7 @@ class Bookmark(
if (scroll != other.scroll) return false
if (imageUrl != other.imageUrl) return false
if (createdAt != other.createdAt) return false
if (percent != other.percent) return false
return true
return percent == other.percent
}
override fun hashCode(): Int {
@@ -43,4 +56,4 @@ class Bookmark(
result = 31 * result + percent.hashCode()
return result
}
}
}

View File

@@ -5,7 +5,6 @@ import androidx.room.withTransaction
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.bookmarks.data.toBookmark
import org.koitharu.kotatsu.bookmarks.data.toBookmarks
@@ -14,9 +13,10 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toEntities
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import javax.inject.Inject
@Reusable
@@ -52,8 +52,14 @@ class BookmarksRepository @Inject constructor(
}
}
suspend fun removeBookmark(mangaId: Long, pageId: Long) {
db.bookmarksDao.delete(mangaId, pageId)
suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) {
"Bookmark not found"
}
}
suspend fun removeBookmark(bookmark: Bookmark) {
removeBookmark(bookmark.manga.id, bookmark.chapterId, bookmark.page)
}
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {

View File

@@ -3,16 +3,14 @@ package org.koitharu.kotatsu.bookmarks.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
@@ -24,10 +22,10 @@ class BookmarksActivity :
SnackbarOwner {
override val appBar: AppBarLayout
get() = binding.appbar
get() = viewBinding.appbar
override val snackbarHost: CoordinatorLayout
get() = binding.root
get() = viewBinding.root
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -43,7 +41,7 @@ class BookmarksActivity :
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding(
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
)

View File

@@ -16,19 +16,23 @@ import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.bookmarks.data.ids
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.ui.util.reverseAsync
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
@@ -37,8 +41,6 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
import javax.inject.Inject
@AndroidEntryPoint
@@ -56,12 +58,12 @@ class BookmarksFragment :
private var adapter: BookmarksGroupAdapter? = null
private var selectionController: SectionedSelectionController<Manga>? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
return FragmentListSimpleBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onViewBindingCreated(binding: FragmentListSimpleBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
selectionController = SectionedSelectionController(
activity = requireActivity(),
owner = this,
@@ -77,12 +79,12 @@ class BookmarksFragment :
)
binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
val spacingDecoration = SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
val spacingDecoration = SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
binding.recyclerView.addItemDecoration(spacingDecoration)
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ::onActionDone)
}
override fun onDestroyView() {
@@ -93,8 +95,11 @@ class BookmarksFragment :
override fun onItemClick(item: Bookmark, view: View) {
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
val intent = ReaderActivity.newIntent(view.context, item)
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
val intent = ReaderActivity.IntentBuilder(view.context)
.bookmark(item)
.incognito(true)
.build()
startActivity(intent, scaleUpActivityOptionsOf(view))
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}
}
@@ -114,7 +119,7 @@ class BookmarksFragment :
override fun onFastScrollStop(fastScroller: FastScroller) = Unit
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
binding.recyclerView.invalidateNestedItemDecorations()
requireViewBinding().recyclerView.invalidateNestedItemDecorations()
}
override fun onCreateActionMode(
@@ -149,10 +154,10 @@ class BookmarksFragment :
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding(
requireViewBinding().recyclerView.updatePadding(
bottom = insets.bottom,
)
binding.recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
requireViewBinding().recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
}

View File

@@ -4,8 +4,8 @@ import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.util.ext.getItem
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.utils.ext.getItem
class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(context) {
@@ -14,5 +14,4 @@ class BookmarksSelectionDecoration(context: Context) : MangaSelectionDecoration(
val item = holder.getItem(Bookmark::class.java) ?: return RecyclerView.NO_ID
return item.pageId
}
}
}

View File

@@ -1,23 +1,26 @@
package org.koitharu.kotatsu.bookmarks.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import javax.inject.Inject
@HiltViewModel
@@ -25,9 +28,9 @@ class BookmarksViewModel @Inject constructor(
private val repository: BookmarksRepository,
) : BaseViewModel() {
val onActionDone = SingleLiveEvent<ReversibleAction>()
val onActionDone = MutableEventFlow<ReversibleAction>()
val content: LiveData<List<ListModel>> = repository.observeBookmarks()
val content: StateFlow<List<ListModel>> = repository.observeBookmarks()
.map { list ->
if (list.isEmpty()) {
listOf(
@@ -43,12 +46,12 @@ class BookmarksViewModel @Inject constructor(
}
}
.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
launchJob(Dispatchers.Default) {
val handle = repository.removeBookmarks(ids)
onActionDone.emitCall(ReversibleAction(R.string.bookmarks_removed, handle))
onActionDone.call(ReversibleAction(R.string.bookmarks_removed, handle))
}
}
}

View File

@@ -4,16 +4,16 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.source
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
fun bookmarkListAD(
coil: ImageLoader,
@@ -28,7 +28,8 @@ fun bookmarkListAD(
binding.root.setOnLongClickListener(listener)
bind {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageUrl)?.run {
val data: Any = item.directImageUrl ?: item.toMangaPage()
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
size(CoverSizeResolver(binding.imageViewThumb))
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)

View File

@@ -4,8 +4,8 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
class BookmarksAdapter(
coil: ImageLoader,
@@ -13,13 +13,15 @@ class BookmarksAdapter(
clickListener: OnListItemClickListener<Bookmark>,
) : AsyncListDifferDelegationAdapter<Bookmark>(
DiffCallback(),
bookmarkListAD(coil, lifecycleOwner, clickListener)
bookmarkListAD(coil, lifecycleOwner, clickListener),
) {
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
return oldItem.manga.id == newItem.manga.id && oldItem.pageId == newItem.pageId
return oldItem.manga.id == newItem.manga.id &&
oldItem.chapterId == newItem.chapterId &&
oldItem.page == newItem.page
}
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
@@ -27,4 +29,4 @@ class BookmarksAdapter(
}
}
}
}

View File

@@ -6,20 +6,20 @@ import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.util.ext.clearItemDecorations
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.source
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
fun bookmarksGroupAD(
coil: ImageLoader,

View File

@@ -5,16 +5,17 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.jvm.internal.Intrinsics
@@ -54,6 +55,10 @@ class BookmarksGroupAdapter(
oldItem.manga.id == newItem.manga.id
}
oldItem is LoadingFooter && newItem is LoadingFooter -> {
oldItem.key == newItem.key
}
else -> oldItem.javaClass == newItem.javaClass
}
}

View File

@@ -12,10 +12,10 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability
import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled")
@@ -32,13 +32,13 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
with(binding.webView.settings) {
with(viewBinding.webView.settings) {
javaScriptEnabled = true
userAgentString = CommonHeadersInterceptor.userAgentChrome
}
binding.webView.webViewClient = BrowserClient(this)
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(binding.webView)
viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
onBackPressedDispatcher.addCallback(onBackPressedCallback)
if (savedInstanceState != null) {
return
@@ -51,18 +51,18 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
url,
)
binding.webView.loadUrl(url)
viewBinding.webView.loadUrl(url)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
binding.webView.saveState(outState)
viewBinding.webView.saveState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
binding.webView.restoreState(savedInstanceState)
viewBinding.webView.restoreState(savedInstanceState)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -73,14 +73,14 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
binding.webView.stopLoading()
viewBinding.webView.stopLoading()
finishAfterTransition()
true
}
R.id.action_browser -> {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(binding.webView.url)
intent.data = Uri.parse(viewBinding.webView.url)
try {
startActivity(Intent.createChooser(intent, item.title))
} catch (_: ActivityNotFoundException) {
@@ -92,22 +92,23 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
override fun onPause() {
binding.webView.onPause()
viewBinding.webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
binding.webView.onResume()
viewBinding.webView.onResume()
}
override fun onDestroy() {
super.onDestroy()
binding.webView.destroy()
viewBinding.webView.stopLoading()
viewBinding.webView.destroy()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
binding.progressBar.isVisible = isLoading
viewBinding.progressBar.isVisible = isLoading
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
@@ -120,10 +121,10 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.appbar.updatePadding(
viewBinding.appbar.updatePadding(
top = insets.top,
)
binding.root.updatePadding(
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,

View File

@@ -0,0 +1,175 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCallback {
private var pendingResult = RESULT_CANCELED
@Inject
lateinit var cookieJar: MutableCookieJar
private var onBackPressedCallback: WebViewBackPressedCallback? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
return
}
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
val url = intent?.dataString.orEmpty()
with(viewBinding.webView.settings) {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
domStorageEnabled = true
databaseEnabled = true
userAgentString = intent?.getStringExtra(ARG_UA) ?: CommonHeadersInterceptor.userAgentFallback
}
viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
onBackPressedDispatcher.addCallback(it)
}
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
if (savedInstanceState != null) {
return
}
if (url.isEmpty()) {
finishAfterTransition()
} else {
onTitleChanged(getString(R.string.loading_), url)
viewBinding.webView.loadUrl(url)
}
}
override fun onDestroy() {
viewBinding.webView.run {
stopLoading()
destroy()
}
super.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
viewBinding.webView.saveState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
viewBinding.webView.restoreState(savedInstanceState)
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.appbar.updatePadding(
top = insets.top,
)
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
viewBinding.webView.stopLoading()
finishAfterTransition()
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onResume() {
super.onResume()
viewBinding.webView.onResume()
}
override fun onPause() {
viewBinding.webView.onPause()
super.onPause()
}
override fun finish() {
setResult(pendingResult)
super.finish()
}
override fun onPageLoaded() {
viewBinding.progressBar.isInvisible = true
}
override fun onCheckPassed() {
pendingResult = RESULT_OK
finishAfterTransition()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
viewBinding.progressBar.isVisible = isLoading
}
override fun onHistoryChanged() {
onBackPressedCallback?.onHistoryChanged()
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
setTitle(title)
supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
}
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() {
override fun createIntent(context: Context, input: Pair<String, Headers?>): Intent {
return newIntent(context, input.first, input.second)
}
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
return TaggedActivityResult(TAG, resultCode)
}
}
companion object {
const val TAG = "CloudFlareActivity"
private const val ARG_UA = "ua"
fun newIntent(
context: Context,
url: String,
headers: Headers?,
) = Intent(context, CloudFlareActivity::class.java).apply {
data = url.toUri()
headers?.get(CommonHeaders.USER_AGENT)?.let {
putExtra(ARG_UA, it)
}
}
}
}

View File

@@ -4,7 +4,6 @@ import android.app.Application
import android.content.Context
import android.provider.SearchRecentSuggestions
import android.text.Html
import android.util.AndroidRuntimeException
import androidx.collection.arraySetOf
import androidx.room.InvalidationTracker
import coil.ComponentRegistry
@@ -23,51 +22,42 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.Cache
import okhttp3.CookieJar
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.StubContentCache
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.*
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.AcraScreenLogger
import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
import org.koitharu.kotatsu.core.util.ext.activityManager
import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.settings.backup.BackupObserver
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.utils.IncognitoModeIndicator
import org.koitharu.kotatsu.utils.ext.activityManager
import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.image.CoilImageGetter
import org.koitharu.kotatsu.widget.WidgetUpdater
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface AppModule {
@Binds
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
@Binds
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
@@ -76,53 +66,6 @@ interface AppModule {
companion object {
@Provides
@Singleton
fun provideCookieJar(
@ApplicationContext context: Context
): MutableCookieJar = try {
AndroidCookieJar()
} catch (e: AndroidRuntimeException) {
// WebView is not available
PreferencesCookieJar(context)
}
@Provides
@Singleton
fun provideHttpCache(
localStorageManager: LocalStorageManager,
): Cache = localStorageManager.createHttpCache()
@Provides
@Singleton
fun provideOkHttpClient(
cache: Cache,
commonHeadersInterceptor: CommonHeadersInterceptor,
mirrorSwitchInterceptor: MirrorSwitchInterceptor,
cookieJar: CookieJar,
settings: AppSettings,
): OkHttpClient {
return OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(cookieJar)
dns(DoHManager(cache, settings))
if (settings.isSSLBypassEnabled) {
bypassSSLErrors()
}
cache(cache)
addNetworkInterceptor(CacheLimitInterceptor())
addInterceptor(GZipInterceptor())
addInterceptor(commonHeadersInterceptor)
addInterceptor(CloudFlareInterceptor())
addInterceptor(mirrorSwitchInterceptor)
if (BuildConfig.DEBUG) {
addInterceptor(CurlLoggingInterceptor())
}
}.build()
}
@Provides
@Singleton
fun provideNetworkState(
@@ -141,14 +84,11 @@ interface AppModule {
@Singleton
fun provideCoil(
@ApplicationContext context: Context,
okHttpClient: OkHttpClient,
@MangaHttpClient okHttpClient: OkHttpClient,
mangaRepositoryFactory: MangaRepository.Factory,
imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory,
): ImageLoader {
val httpClientFactory = {
okHttpClient.newBuilder()
.cache(null)
.build()
}
val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir
DiskCache.Builder()
@@ -156,19 +96,21 @@ interface AppModule {
.build()
}
return ImageLoader.Builder(context)
.okHttpClient(httpClientFactory)
.okHttpClient(okHttpClient.newBuilder().cache(null).build())
.interceptorDispatcher(Dispatchers.Default)
.fetcherDispatcher(Dispatchers.IO)
.decoderDispatcher(Dispatchers.Default)
.transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(isLowRamDevice(context))
.allowRgb565(context.isLowRamDevice())
.components(
ComponentRegistry.Builder()
.add(SvgDecoder.Factory())
.add(CbzFetcher.Factory())
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
.add(pageFetcherFactory)
.add(imageProxyInterceptor)
.build(),
).build()
}
@@ -184,12 +126,12 @@ interface AppModule {
@ElementsIntoSet
fun provideDatabaseObservers(
widgetUpdater: WidgetUpdater,
shortcutsUpdater: ShortcutsUpdater,
appShortcutManager: AppShortcutManager,
backupObserver: BackupObserver,
syncController: SyncController,
): Set<@JvmSuppressWildcards InvalidationTracker.Observer> = arraySetOf(
widgetUpdater,
shortcutsUpdater,
appShortcutManager,
backupObserver,
syncController,
)
@@ -200,10 +142,12 @@ interface AppModule {
appProtectHelper: AppProtectHelper,
activityRecreationHandle: ActivityRecreationHandle,
incognitoModeIndicator: IncognitoModeIndicator,
acraScreenLogger: AcraScreenLogger,
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
appProtectHelper,
activityRecreationHandle,
incognitoModeIndicator,
acraScreenLogger,
)
@Provides

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
private const val PAGE_SIZE = 10

View File

@@ -5,10 +5,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.Closeable
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.format
import org.koitharu.kotatsu.core.zip.ZipOutput
import org.koitharu.kotatsu.utils.ext.format
import java.io.File
import java.util.*
import java.util.Date
import java.util.Locale
import java.util.zip.Deflater
class BackupZipOutput(val file: File) : Closeable {
@@ -42,4 +43,4 @@ suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptibl
append(".bk.zip")
}
BackupZipOutput(File(dir, filename))
}
}

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration11To12
import org.koitharu.kotatsu.core.db.migrations.Migration12To13
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
@@ -33,6 +34,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration6To7
import org.koitharu.kotatsu.core.db.migrations.Migration7To8
import org.koitharu.kotatsu.core.db.migrations.Migration8To9
import org.koitharu.kotatsu.core.db.migrations.Migration9To10
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
@@ -46,9 +48,8 @@ import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
const val DATABASE_VERSION = 15
const val DATABASE_VERSION = 16
@Database(
entities = [
@@ -100,6 +101,7 @@ val databaseMigrations: Array<Migration>
Migration12To13(),
Migration13To14(),
Migration14To15(),
Migration15To16(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -1,13 +1,13 @@
package org.koitharu.kotatsu.core.db.entity
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.utils.ext.longHashCode
// Entity to model

View File

@@ -23,4 +23,5 @@ data class MangaPrefsEntity(
@ColumnInfo(name = "mode") val mode: Int,
@ColumnInfo(name = "cf_brightness") val cfBrightness: Float,
@ColumnInfo(name = "cf_contrast") val cfContrast: Float,
@ColumnInfo(name = "cf_invert") val cfInvert: Boolean,
)

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