Compare commits

...

68 Commits

Author SHA1 Message Date
cd23b044df Merge branch 'devel' 2023-06-13 19:08:45 +03:00
4922881343 Update parsers 2023-06-13 18:41:32 +03:00
ff0d04bea6 Add current screen info to crash reports 2023-06-13 18:32:11 +03:00
97de629c3b Fix BottomSheet duplication 2023-06-13 10:45:29 +03:00
7b482e5bcf Fix shortcuts icons size 2023-06-12 16:16:33 +03:00
fd575b8131 Merge branch 'devel' 2023-06-12 12:57:29 +03:00
c77e023bef Fix color filter preview 2023-06-12 12:54:24 +03:00
a3cf52859b Reader refactoring 2023-06-12 12:47:05 +03:00
5e55bce529 Use indirect image url in bookmarks 2023-06-12 10:54:12 +03:00
b1ba70bf77 Fix settings issues 2023-06-10 15:48:30 +03:00
b930272221 Fix settings background 2023-06-09 16:34:29 +03:00
75305c0b94 Fix fast scroll issues 2023-06-09 16:18:58 +03:00
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
6a2e12dc29 Read button tip in DetailsActivity 2023-06-06 12:35:36 +03:00
9587cb439c Android 5 fixes 2023-06-06 10:10:24 +03:00
c42d0824b0 Add summaries to settings root 2023-06-05 13:54:49 +03:00
09f6dd9b4e Remove unused resources 2023-06-05 13:53:52 +03:00
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
0f6d56ee2d Merge remote-tracking branch 'weblate/devel' into devel 2023-06-05 13:23:14 +03:00
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
bc3a7fc211 Reorganize settings 2023-06-05 12:53:12 +03:00
e794f84c6f Get rid of SlidingPaneLayout in settings 2023-06-05 11:52:56 +03:00
76709dda21 Remove replacement characters from manga description 2023-06-05 10:57:35 +03:00
6dc460bc20 Replace CloudFlareDialog with activity 2023-06-05 10:45:23 +03:00
c2ee548f0a Show errors in global search results 2023-06-05 09:34:56 +03:00
1847759ec3 Refactor list extra provider 2023-06-04 17:37:54 +03:00
02d5dfb375 Check if page file valid before page display 2023-06-04 16:55:51 +03:00
ef9b16da0b Fix favourite categories covers loading 2023-06-02 17:07:26 +03:00
5d1ef983e9 Check available ram before pages prefetch 2023-06-02 17:06:37 +03:00
eb78a776cf Update parsers 2023-06-02 17:04:46 +03:00
216 changed files with 4350 additions and 3346 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' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 547 versionCode 552
versionName '5.1.3' versionName '5.2'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -79,17 +79,17 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:44e28b40d3') { implementation('com.github.KotatsuApp:kotatsu-parsers:f732582d55') {
exclude group: 'org.json', module: 'json' 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 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.activity:activity-ktx:1.7.2' 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-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-service:2.6.1' implementation 'androidx.lifecycle:lifecycle-service:2.6.1'

View File

@@ -23,7 +23,7 @@ import javax.inject.Inject
@HiltAndroidTest @HiltAndroidTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ShortcutsUpdaterTest { class AppShortcutManagerTest {
@get:Rule @get:Rule
var hiltRule = HiltAndroidRule(this) var hiltRule = HiltAndroidRule(this)
@@ -32,7 +32,7 @@ class ShortcutsUpdaterTest {
lateinit var historyRepository: HistoryRepository lateinit var historyRepository: HistoryRepository
@Inject @Inject
lateinit var shortcutsUpdater: ShortcutsUpdater lateinit var appShortcutManager: AppShortcutManager
@Inject @Inject
lateinit var database: MangaDatabase lateinit var database: MangaDatabase
@@ -72,6 +72,6 @@ class ShortcutsUpdaterTest {
private suspend fun awaitUpdate() { private suspend fun awaitUpdate() {
val instrumentation = InstrumentationRegistry.getInstrumentation() val instrumentation = InstrumentationRegistry.getInstrumentation()
instrumentation.awaitForIdle() instrumentation.awaitForIdle()
shortcutsUpdater.await() appShortcutManager.await()
} }
} }

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.util package org.koitharu.kotatsu.core.util
import android.util.Log import android.util.Log
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver

View File

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

View File

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

View File

@@ -67,6 +67,10 @@ class KotatsuApp : Application(), Configuration.Provider {
reportFormat = StringFormat.JSON reportFormat = StringFormat.JSON
excludeMatchingSharedPreferencesKeys = listOf( excludeMatchingSharedPreferencesKeys = listOf(
"sources_\\w+", "sources_\\w+",
AppSettings.KEY_APP_PASSWORD,
AppSettings.KEY_PROXY_LOGIN,
AppSettings.KEY_PROXY_ADDRESS,
AppSettings.KEY_PROXY_PASSWORD,
) )
httpSender { httpSender {
uri = getString(R.string.url_error_report) uri = getString(R.string.url_error_report)
@@ -83,6 +87,7 @@ class KotatsuApp : Application(), Configuration.Provider {
ReportField.PHONE_MODEL, ReportField.PHONE_MODEL,
ReportField.STACK_TRACE, ReportField.STACK_TRACE,
ReportField.CRASH_CONFIGURATION, ReportField.CRASH_CONFIGURATION,
ReportField.CUSTOM_DATA,
ReportField.SHARED_PREFERENCES, ReportField.SHARED_PREFERENCES,
) )
dialog { dialog {

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.bookmarks.domain package org.koitharu.kotatsu.bookmarks.domain
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import java.util.Date import java.util.Date
class Bookmark( class Bookmark(
@@ -14,6 +15,20 @@ class Bookmark(
val percent: Float, 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View File

@@ -15,8 +15,8 @@ import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems 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.parsers.model.Manga
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
@Reusable @Reusable

View File

@@ -95,8 +95,11 @@ class BookmarksFragment :
override fun onItemClick(item: Bookmark, view: View) { override fun onItemClick(item: Bookmark, view: View) {
if (selectionController?.onItemClick(item.manga, item.pageId) != true) { if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
val intent = ReaderActivity.newIntent(view.context, item) val intent = ReaderActivity.IntentBuilder(view.context)
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle()) .bookmark(item)
.incognito(true)
.build()
startActivity(intent, scaleUpActivityOptionsOf(view))
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show() Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
} }
} }

View File

@@ -28,7 +28,8 @@ fun bookmarkListAD(
binding.root.setOnLongClickListener(listener) binding.root.setOnLongClickListener(listener)
bind { 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)) size(CoverSizeResolver(binding.imageViewThumb))
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)

View File

@@ -103,6 +103,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
viewBinding.webView.stopLoading()
viewBinding.webView.destroy() viewBinding.webView.destroy()
} }

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

@@ -1,123 +0,0 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
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.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.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
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 onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
) = FragmentCloudflareBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: FragmentCloudflareBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, 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() {
requireViewBinding().webView.stopLoading()
requireViewBinding().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(requireViewBinding().webView).also {
dialog.onBackPressedDispatcher.addCallback(it)
}
}
override fun onResume() {
super.onResume()
requireViewBinding().webView.onResume()
}
override fun onPause() {
requireViewBinding().webView.onPause()
super.onPause()
}
override fun onDismiss(dialog: DialogInterface) {
setFragmentResult(TAG, pendingResult)
super.onDismiss(dialog)
}
override fun onPageLoaded() {
viewBinding?.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

@@ -29,19 +29,18 @@ import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.StubContentCache import org.koitharu.kotatsu.core.cache.StubContentCache
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.* import org.koitharu.kotatsu.core.network.*
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.os.NetworkState 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.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle 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.IncognitoModeIndicator
import org.koitharu.kotatsu.core.util.ext.activityManager import org.koitharu.kotatsu.core.util.ext.activityManager
import org.koitharu.kotatsu.core.util.ext.connectivityManager import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.domain.ListExtraProviderImpl
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
@@ -65,9 +64,6 @@ interface AppModule {
@Binds @Binds
fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter
@Binds
fun bindListExtraProvider(impl: ListExtraProviderImpl): ListExtraProvider
companion object { companion object {
@Provides @Provides
@@ -130,12 +126,12 @@ interface AppModule {
@ElementsIntoSet @ElementsIntoSet
fun provideDatabaseObservers( fun provideDatabaseObservers(
widgetUpdater: WidgetUpdater, widgetUpdater: WidgetUpdater,
shortcutsUpdater: ShortcutsUpdater, appShortcutManager: AppShortcutManager,
backupObserver: BackupObserver, backupObserver: BackupObserver,
syncController: SyncController, syncController: SyncController,
): Set<@JvmSuppressWildcards InvalidationTracker.Observer> = arraySetOf( ): Set<@JvmSuppressWildcards InvalidationTracker.Observer> = arraySetOf(
widgetUpdater, widgetUpdater,
shortcutsUpdater, appShortcutManager,
backupObserver, backupObserver,
syncController, syncController,
) )
@@ -146,10 +142,12 @@ interface AppModule {
appProtectHelper: AppProtectHelper, appProtectHelper: AppProtectHelper,
activityRecreationHandle: ActivityRecreationHandle, activityRecreationHandle: ActivityRecreationHandle,
incognitoModeIndicator: IncognitoModeIndicator, incognitoModeIndicator: IncognitoModeIndicator,
acraScreenLogger: AcraScreenLogger,
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf( ): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
appProtectHelper, appProtectHelper,
activityRecreationHandle, activityRecreationHandle,
incognitoModeIndicator, incognitoModeIndicator,
acraScreenLogger,
) )
@Provides @Provides

View File

@@ -6,15 +6,13 @@ import androidx.annotation.StringRes
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Headers import okhttp3.Headers
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.TaggedActivityResult import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.isSuccess
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -23,20 +21,26 @@ import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class ExceptionResolver private constructor( class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
private val activity: FragmentActivity?,
private val fragment: Fragment?,
) : ActivityResultCallback<TaggedActivityResult> {
private val continuations = ArrayMap<String, Continuation<Boolean>>(1) private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
private lateinit var sourceAuthContract: ActivityResultLauncher<MangaSource> private val activity: FragmentActivity?
private val fragment: Fragment?
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
private val cloudflareContract: ActivityResultLauncher<Pair<String, Headers?>>
constructor(activity: FragmentActivity) : this(activity = activity, fragment = null) { constructor(activity: FragmentActivity) {
this.activity = activity
fragment = null
sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this) sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this)
cloudflareContract = activity.registerForActivityResult(CloudFlareActivity.Contract(), this)
} }
constructor(fragment: Fragment) : this(activity = null, fragment = fragment) { constructor(fragment: Fragment) {
this.fragment = fragment
activity = null
sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this) sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this)
cloudflareContract = fragment.registerForActivityResult(CloudFlareActivity.Contract(), this)
} }
override fun onActivityResult(result: TaggedActivityResult) { override fun onActivityResult(result: TaggedActivityResult) {
@@ -58,22 +62,9 @@ class ExceptionResolver private constructor(
else -> false else -> false
} }
private suspend fun resolveCF(url: String, headers: Headers): Boolean { private suspend fun resolveCF(url: String, headers: Headers): Boolean = suspendCoroutine { cont ->
val dialog = CloudFlareDialog.newInstance(url, headers) continuations[CloudFlareActivity.TAG] = cont
val fm = getFragmentManager() cloudflareContract.launch(url to headers)
return suspendCancellableCoroutine { cont ->
fm.clearFragmentResult(CloudFlareDialog.TAG)
continuations[CloudFlareDialog.TAG] = cont
fm.setFragmentResultListener(CloudFlareDialog.TAG, checkNotNull(fragment ?: activity)) { key, result ->
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
}
dialog.show(fm, CloudFlareDialog.TAG)
cont.invokeOnCancellation {
continuations.remove(CloudFlareDialog.TAG, cont)
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
dialog.dismissAllowingStateLoss()
}
}
} }
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont -> private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->

View File

@@ -16,12 +16,12 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.BaseHttpClient import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.asArrayList import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
import org.koitharu.kotatsu.parsers.util.parseJsonArray import org.koitharu.kotatsu.parsers.util.parseJsonArray
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.core.util.ext.subdir import org.koitharu.kotatsu.core.util.ext.subdir
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat

View File

@@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.network package org.koitharu.kotatsu.core.network
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.IOException import java.io.IOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy

View File

@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mergeWith import org.koitharu.kotatsu.parsers.util.mergeWith
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.net.IDN import java.net.IDN
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject

View File

@@ -6,7 +6,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps import okhttp3.dnsoverhttps.DnsOverHttps
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.net.InetAddress import java.net.InetAddress
import java.net.UnknownHostException import java.net.UnknownHostException

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.network
import android.annotation.SuppressLint import android.annotation.SuppressLint
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.security.SecureRandom import java.security.SecureRandom
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext

View File

@@ -8,7 +8,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
private const val PREFS_NAME = "cookies" private const val PREFS_NAME = "cookies"

View File

@@ -15,7 +15,6 @@ import androidx.core.graphics.drawable.toBitmap
import androidx.room.InvalidationTracker import androidx.room.InvalidationTracker
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale import coil.size.Scale
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -25,18 +24,19 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_HISTORY import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.image.ThumbnailTransformation
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class ShortcutsUpdater @Inject constructor( class AppShortcutManager @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val coil: ImageLoader, private val coil: ImageLoader,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
@@ -128,8 +128,8 @@ class ShortcutsUpdater @Inject constructor(
.data(manga.coverUrl) .data(manga.coverUrl)
.size(iconSize.width, iconSize.height) .size(iconSize.width, iconSize.height)
.tag(manga.source) .tag(manga.source)
.precision(Precision.EXACT)
.scale(Scale.FILL) .scale(Scale.FILL)
.transformations(ThumbnailTransformation())
.build(), .build(),
).getDrawableOrThrow().toBitmap() ).getDrawableOrThrow().toBitmap()
}.fold( }.fold(
@@ -142,8 +142,9 @@ class ShortcutsUpdater @Inject constructor(
.setLongLabel(manga.title) .setLongLabel(manga.title)
.setIcon(icon) .setIcon(icon)
.setIntent( .setIntent(
ReaderActivity.newIntent(context, manga.id) ReaderActivity.IntentBuilder(context)
.setAction(ReaderActivity.ACTION_MANGA_READ), .mangaId(manga.id)
.build(),
) )
} }

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

@@ -27,6 +27,7 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
.setView(binding.root) .setView(binding.root)
.run(::onBuildDialog) .run(::onBuildDialog)
.create() .create()
.also(::onDialogCreated)
} }
final override fun onCreateView( final override fun onCreateView(

View File

@@ -14,7 +14,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@@ -45,7 +44,7 @@ abstract class BaseActivity<B : ViewBinding> :
protected val exceptionResolver = ExceptionResolver(this) protected val exceptionResolver = ExceptionResolver(this)
@JvmField @JvmField
protected val insetsDelegate = WindowInsetsDelegate(this) protected val insetsDelegate = WindowInsetsDelegate()
@JvmField @JvmField
val actionModeDelegate = ActionModeDelegate() val actionModeDelegate = ActionModeDelegate()
@@ -62,6 +61,7 @@ abstract class BaseActivity<B : ViewBinding> :
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
insetsDelegate.handleImeInsets = true insetsDelegate.handleImeInsets = true
insetsDelegate.addInsetsListener(this)
putDataToExtras(intent) putDataToExtras(intent)
} }
@@ -103,7 +103,8 @@ abstract class BaseActivity<B : ViewBinding> :
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
ActivityCompat.recreate(this) // ActivityCompat.recreate(this)
error("Test")
return true return true
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)

View File

@@ -26,7 +26,7 @@ abstract class BaseFragment<B : ViewBinding> :
protected val exceptionResolver = ExceptionResolver(this) protected val exceptionResolver = ExceptionResolver(this)
@JvmField @JvmField
protected val insetsDelegate = WindowInsetsDelegate(this) protected val insetsDelegate = WindowInsetsDelegate()
protected val actionModeDelegate: ActionModeDelegate protected val actionModeDelegate: ActionModeDelegate
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
@@ -44,11 +44,13 @@ abstract class BaseFragment<B : ViewBinding> :
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
insetsDelegate.onViewCreated(view) insetsDelegate.onViewCreated(view)
insetsDelegate.addInsetsListener(this)
onViewBindingCreated(requireViewBinding(), savedInstanceState) onViewBindingCreated(requireViewBinding(), savedInstanceState)
} }
override fun onDestroyView() { override fun onDestroyView() {
viewBinding = null viewBinding = null
insetsDelegate.removeInsetsListener(this)
insetsDelegate.onDestroyView() insetsDelegate.onDestroyView()
super.onDestroyView() super.onDestroyView()
} }

View File

@@ -4,9 +4,11 @@ import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager import android.view.WindowManager
import androidx.core.content.ContextCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.R
abstract class BaseFullscreenActivity<B : ViewBinding> : abstract class BaseFullscreenActivity<B : ViewBinding> :
BaseActivity<B>() { BaseActivity<B>() {
@@ -18,7 +20,11 @@ abstract class BaseFullscreenActivity<B : ViewBinding> :
with(window) { with(window) {
insetsControllerCompat = WindowInsetsControllerCompat(this, decorView) insetsControllerCompat = WindowInsetsControllerCompat(this, decorView)
statusBarColor = Color.TRANSPARENT statusBarColor = Color.TRANSPARENT
navigationBarColor = Color.TRANSPARENT navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim)
} else {
Color.TRANSPARENT
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
attributes.layoutInDisplayCutoutMode = attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

View File

@@ -12,10 +12,10 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.settings.SettingsHeadersFragment import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.settings.SettingsActivity
import javax.inject.Inject import javax.inject.Inject
@Suppress("LeakingThis")
@AndroidEntryPoint @AndroidEntryPoint
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
@@ -26,27 +26,28 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
lateinit var settings: AppSettings lateinit var settings: AppSettings
@JvmField @JvmField
protected val insetsDelegate = WindowInsetsDelegate(this) protected val insetsDelegate = WindowInsetsDelegate()
override val recyclerView: RecyclerView override val recyclerView: RecyclerView
get() = listView get() = listView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(view.context.getThemeColor(android.R.attr.colorBackground))
listView.clipToPadding = false listView.clipToPadding = false
insetsDelegate.onViewCreated(view) insetsDelegate.onViewCreated(view)
insetsDelegate.addInsetsListener(this)
} }
override fun onDestroyView() { override fun onDestroyView() {
insetsDelegate.removeInsetsListener(this)
insetsDelegate.onDestroyView() insetsDelegate.onDestroyView()
super.onDestroyView() super.onDestroyView()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (titleId != 0) { setTitle(if (titleId != 0) getString(titleId) else null)
setTitle(getString(titleId))
}
} }
@CallSuper @CallSuper
@@ -56,9 +57,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
) )
} }
@Suppress("UsePropertyAccessSyntax") protected fun setTitle(title: CharSequence?) {
protected fun setTitle(title: CharSequence) { (activity as? SettingsActivity)?.setSectionTitle(title)
(parentFragment as? SettingsHeadersFragment)?.setTitle(title)
?: activity?.setTitle(title)
} }
} }

View File

@@ -17,7 +17,7 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.util.ext.EventFlow import org.koitharu.kotatsu.core.util.ext.EventFlow
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext

View File

@@ -9,7 +9,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
abstract class CoroutineIntentService : BaseService() { abstract class CoroutineIntentService : BaseService() {

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.core.ui.image
import android.graphics.Bitmap
import android.media.ThumbnailUtils
import coil.size.Size
import coil.size.pxOrElse
import coil.transform.Transformation
class ThumbnailTransformation : Transformation {
override val cacheKey: String = javaClass.name
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
return ThumbnailUtils.extractThumbnail(
input,
size.width.pxOrElse { input.width },
size.height.pxOrElse { input.height },
)
}
override fun equals(other: Any?) = other is ThumbnailTransformation
override fun hashCode() = javaClass.hashCode()
}

View File

@@ -9,6 +9,7 @@ import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.annotation.* import androidx.annotation.*
@@ -24,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.isLayoutReversed import org.koitharu.kotatsu.core.util.ext.isLayoutReversed
import org.koitharu.kotatsu.core.util.ext.parents
import org.koitharu.kotatsu.databinding.FastScrollerBinding import org.koitharu.kotatsu.databinding.FastScrollerBinding
import kotlin.math.roundToInt import kotlin.math.roundToInt
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -115,6 +117,9 @@ class FastScroller @JvmOverloads constructor(
return viewHeight * proportion return viewHeight * proportion
} }
val isScrollbarVisible: Boolean
get() = binding.scrollbar.isVisible
init { init {
clipChildren = false clipChildren = false
orientation = HORIZONTAL orientation = HORIZONTAL
@@ -165,7 +170,9 @@ class FastScroller @JvmOverloads constructor(
when (event.actionMasked) { when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
if (event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) return false if (!isScrollbarVisible || event.x.toInt() !in binding.scrollbar.left..binding.scrollbar.right) {
return false
}
requestDisallowInterceptTouchEvent(true) requestDisallowInterceptTouchEvent(true)
setHandleSelected(true) setHandleSelected(true)
@@ -296,10 +303,12 @@ class FastScroller @JvmOverloads constructor(
if (parent is ViewGroup) { if (parent is ViewGroup) {
setLayoutParams(parent as ViewGroup) setLayoutParams(parent as ViewGroup)
} else if (recyclerView.parent is ViewGroup) { } else {
val viewGroup = recyclerView.parent as ViewGroup val viewGroup = findValidParent(recyclerView)
viewGroup.addView(this) if (viewGroup != null) {
setLayoutParams(viewGroup) viewGroup.addView(this)
setLayoutParams(viewGroup)
}
} }
recyclerView.addOnScrollListener(scrollListener) recyclerView.addOnScrollListener(scrollListener)
@@ -513,6 +522,14 @@ class FastScroller @JvmOverloads constructor(
return BubbleSize.values().getOrNull(ordinal) ?: defaultValue return BubbleSize.values().getOrNull(ordinal) ?: defaultValue
} }
private fun findValidParent(view: View): ViewGroup? = view.parents.firstNotNullOfOrNull { p ->
if (p is FrameLayout || p is ConstraintLayout || p is CoordinatorLayout || p is RelativeLayout) {
p as ViewGroup
} else {
null
}
}
private val BubbleSize.textSize private val BubbleSize.textSize
@Px get() = resources.getDimension(textSizeId) @Px get() = resources.getDimension(textSizeId)

View File

@@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import androidx.activity.ComponentDialog
import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcher
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
@@ -36,7 +35,7 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
val onBackPressedDispatcher: OnBackPressedDispatcher val onBackPressedDispatcher: OnBackPressedDispatcher
get() = (requireDialog() as ComponentDialog).onBackPressedDispatcher get() = requireComponentDialog().onBackPressedDispatcher
final override fun onCreateView( final override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,

View File

@@ -14,7 +14,7 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
get() = activeActionMode != null get() = activeActionMode != null
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
activeActionMode?.finish() finishActionMode()
} }
fun onSupportActionModeStarted(mode: ActionMode) { fun onSupportActionModeStarted(mode: ActionMode) {
@@ -45,6 +45,10 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
owner.lifecycle.addObserver(ListenerLifecycleObserver(listener)) owner.lifecycle.addObserver(ListenerLifecycleObserver(listener))
} }
fun finishActionMode() {
activeActionMode?.finish()
}
private inner class ListenerLifecycleObserver( private inner class ListenerLifecycleObserver(
private val listener: ActionModeListener, private val listener: ActionModeListener,
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {

View File

@@ -6,7 +6,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
fun interface ReversibleHandle { fun interface ReversibleHandle {

View File

@@ -5,10 +5,9 @@ import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import java.util.LinkedList
class WindowInsetsDelegate( class WindowInsetsDelegate : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
private val listener: WindowInsetsListener,
) : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
@JvmField @JvmField
var handleImeInsets: Boolean = false var handleImeInsets: Boolean = false
@@ -16,6 +15,7 @@ class WindowInsetsDelegate(
@JvmField @JvmField
var interceptingWindowInsetsListener: OnApplyWindowInsetsListener? = null var interceptingWindowInsetsListener: OnApplyWindowInsetsListener? = null
private val listeners = LinkedList<WindowInsetsListener>()
private var lastInsets: Insets? = null private var lastInsets: Insets? = null
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
@@ -29,7 +29,7 @@ class WindowInsetsDelegate(
handledInsets.getInsets(WindowInsetsCompat.Type.systemBars()) handledInsets.getInsets(WindowInsetsCompat.Type.systemBars())
} }
if (newInsets != lastInsets) { if (newInsets != lastInsets) {
listener.onWindowInsetsChanged(newInsets) listeners.forEach { it.onWindowInsetsChanged(newInsets) }
lastInsets = newInsets lastInsets = newInsets
} }
return handledInsets return handledInsets
@@ -52,6 +52,15 @@ class WindowInsetsDelegate(
} }
} }
fun addInsetsListener(listener: WindowInsetsListener) {
listeners.add(listener)
lastInsets?.let { listener.onWindowInsetsChanged(it) }
}
fun removeInsetsListener(listener: WindowInsetsListener) {
listeners.remove(listener)
}
fun onViewCreated(view: View) { fun onViewCreated(view: View) {
ViewCompat.setOnApplyWindowInsetsListener(view, this) ViewCompat.setOnApplyWindowInsetsListener(view, this)
view.addOnLayoutChangeListener(this) view.addOnLayoutChangeListener(this)

View File

@@ -92,6 +92,14 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
) )
} }
fun showOrHide(show: Boolean) {
if (show) {
show()
} else {
hide()
}
}
private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) { private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
currentAnimator = animate() currentAnimator = animate()
.translationY(targetY) .translationY(targetY)

View File

@@ -0,0 +1,50 @@
package org.koitharu.kotatsu.core.util
import android.app.Activity
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
import org.acra.ACRA
import org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AcraScreenLogger @Inject constructor() : FragmentLifecycleCallbacks(), DefaultActivityLifecycleCallbacks {
private val timeFormat = SimpleDateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.ROOT)
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
super.onFragmentAttached(fm, f, context)
ACRA.errorReporter.putCustomData(f.key(), "${time()}: ${f.arguments}")
}
override fun onFragmentDetached(fm: FragmentManager, f: Fragment) {
super.onFragmentDetached(fm, f)
ACRA.errorReporter.removeCustomData(f.key())
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
super.onActivityCreated(activity, savedInstanceState)
ACRA.errorReporter.putCustomData(activity.key(), "${time()}: ${activity.intent}")
(activity as? FragmentActivity)?.supportFragmentManager?.registerFragmentLifecycleCallbacks(this, true)
}
override fun onActivityDestroyed(activity: Activity) {
super.onActivityDestroyed(activity)
ACRA.errorReporter.removeCustomData(activity.key())
}
private fun Activity.key() = "Activity[${javaClass.simpleName}]"
private fun Fragment.key() = "Fragment[${javaClass.simpleName}]"
private fun time() = timeFormat.format(Date())
}

View File

@@ -5,7 +5,8 @@ import android.app.Activity
class TaggedActivityResult( class TaggedActivityResult(
val tag: String, val tag: String,
val result: Int, val result: Int,
) ) {
val TaggedActivityResult.isSuccess: Boolean val isSuccess: Boolean
get() = this.result == Activity.RESULT_OK get() = result == Activity.RESULT_OK
}

View File

@@ -16,6 +16,7 @@ import android.database.SQLException
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import android.view.ViewPropertyAnimator import android.view.ViewPropertyAnimator
@@ -43,7 +44,6 @@ import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import kotlin.math.roundToLong import kotlin.math.roundToLong
@@ -148,13 +148,17 @@ val Context.ramAvailable: Long
return result.availMem return result.availMem
} }
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation( fun scaleUpActivityOptionsOf(view: View): Bundle? = if (view.context.isAnimationsEnabled) {
view, ActivityOptions.makeScaleUpAnimation(
0, view,
0, 0,
view.width, 0,
view.height, view.width,
) view.height,
).toBundle()
} else {
null
}
fun Resources.getLocalesConfig(): LocaleListCompat { fun Resources.getLocalesConfig(): LocaleListCompat {
val tagsList = StringJoiner(",") val tagsList = StringJoiner(",")

View File

@@ -26,6 +26,8 @@ fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
fun File.takeIfWriteable() = takeIf { it.exists() && it.canWrite() } fun File.takeIfWriteable() = takeIf { it.exists() && it.canWrite() }
fun File.isNotEmpty() = length() != 0L
fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use { fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use {
it.readText() it.readText()
} }

View File

@@ -14,9 +14,7 @@ import org.koitharu.kotatsu.core.util.Event
fun <T> Flow<T>.observe(owner: LifecycleOwner, collector: FlowCollector<T>) { fun <T> Flow<T>.observe(owner: LifecycleOwner, collector: FlowCollector<T>) {
val start = if (this is StateFlow) CoroutineStart.UNDISPATCHED else CoroutineStart.DEFAULT val start = if (this is StateFlow) CoroutineStart.UNDISPATCHED else CoroutineStart.DEFAULT
owner.lifecycleScope.launch(start = start) { owner.lifecycleScope.launch(start = start) {
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { collect(collector)
collect(collector)
}
} }
} }

View File

@@ -52,3 +52,11 @@ suspend fun Fragment.awaitViewLifecycle(): LifecycleOwner {
} }
} }
} }
fun DialogFragment.showDistinct(fm: FragmentManager, tag: String) {
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
return
}
show(fm, tag)
}

View File

@@ -2,10 +2,9 @@ package org.koitharu.kotatsu.core.util.ext
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.util.UUID import java.util.UUID
inline fun String?.ifNullOrEmpty(defaultValue: () -> String): String { inline fun <C : CharSequence> C?.ifNullOrEmpty(defaultValue: () -> C): C {
return if (this.isNullOrEmpty()) defaultValue() else this return if (this.isNullOrEmpty()) defaultValue() else this
} }
@@ -35,3 +34,9 @@ fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float)
val diff = lowercase().levenshteinDistance(other.lowercase()) / ((length + other.length) / 2f) val diff = lowercase().levenshteinDistance(other.lowercase()) / ((length + other.length) / 2f)
return diff < threshold return diff < threshold
} }
fun CharSequence.sanitize(): CharSequence {
return filterNot { c -> c.isReplacement() }
}
fun Char.isReplacement() = this in '\uFFF0'..'\uFFFF'

View File

@@ -87,3 +87,6 @@ fun Throwable.isWebViewUnavailable(): Boolean {
return (this is AndroidRuntimeException && message?.contains("WebView") == true) || return (this is AndroidRuntimeException && message?.contains("WebView") == true) ||
cause?.isWebViewUnavailable() == true cause?.isWebViewUnavailable() == true
} }
@Suppress("FunctionName")
fun NoSpaceLeftException() = IOException(MSG_NO_SPACE_LEFT)

View File

@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint

View File

@@ -0,0 +1,92 @@
package org.koitharu.kotatsu.details.ui
import android.transition.TransitionManager
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets
import androidx.core.view.setMargins
import androidx.core.view.updateLayoutParams
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelSize
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.databinding.ItemTipBinding
import com.google.android.material.R as materialR
class ButtonTip(
private val root: ViewGroup,
private val insetsDelegate: WindowInsetsDelegate,
private val viewModel: DetailsViewModel,
) : View.OnClickListener, WindowInsetsDelegate.WindowInsetsListener {
private var selfBinding = ItemTipBinding.inflate(LayoutInflater.from(root.context), root, false)
private val actionBarSize = root.context.getThemeDimensionPixelSize(materialR.attr.actionBarSize)
init {
selfBinding.textView.setText(R.string.details_button_tip)
selfBinding.imageViewIcon.setImageResource(R.drawable.ic_tap)
selfBinding.root.id = R.id.layout_tip
selfBinding.buttonClose.setOnClickListener(this)
}
override fun onClick(v: View?) {
remove()
}
override fun onWindowInsetsChanged(insets: Insets) {
if (root is CoordinatorLayout) {
selfBinding.root.updateLayoutParams<CoordinatorLayout.LayoutParams> {
bottomMargin = topMargin + insets.bottom + insets.top + actionBarSize
}
}
}
fun addToRoot() {
val lp: ViewGroup.LayoutParams = when (root) {
is CoordinatorLayout -> CoordinatorLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
).apply {
// anchorId = R.id.layout_bottom
// anchorGravity = Gravity.TOP
gravity = Gravity.BOTTOM
setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal))
bottomMargin += actionBarSize
}
is ConstraintLayout -> ConstraintLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
).apply {
width = root.resources.getDimensionPixelSize(R.dimen.m3_side_sheet_width)
setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal))
}
else -> ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
root.addView(selfBinding.root, lp)
if (root is ConstraintLayout) {
val cs = ConstraintSet()
cs.clone(root)
cs.connect(R.id.layout_tip, ConstraintSet.TOP, R.id.appbar, ConstraintSet.BOTTOM)
cs.connect(R.id.layout_tip, ConstraintSet.START, R.id.card_chapters, ConstraintSet.START)
cs.connect(R.id.layout_tip, ConstraintSet.END, R.id.card_chapters, ConstraintSet.END)
cs.applyTo(root)
}
insetsDelegate.addInsetsListener(this)
}
fun remove() {
if (root.context.isAnimationsEnabled) {
TransitionManager.beginDelayedTransition(root)
}
insetsDelegate.removeInsetsListener(this)
root.removeView(selfBinding.root)
viewModel.onButtonTipClosed()
}
}

View File

@@ -25,7 +25,7 @@ import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -79,12 +79,11 @@ class ChaptersFragment :
return return
} }
startActivity( startActivity(
ReaderActivity.newIntent( IntentBuilder(view.context)
context = view.context, .manga(viewModel.manga.value ?: return)
manga = viewModel.manga.value ?: return, .state(ReaderState(item.chapter.id, 0, 0))
state = ReaderState(item.chapter.id, 0, 0), .build(),
), scaleUpActivityOptionsOf(view),
scaleUpActivityOptionsOf(view).toBundle(),
) )
} }

View File

@@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
@@ -53,8 +53,9 @@ import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import java.lang.ref.WeakReference
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -67,9 +68,10 @@ class DetailsActivity :
PopupMenu.OnMenuItemClickListener { PopupMenu.OnMenuItemClickListener {
@Inject @Inject
lateinit var shortcutsUpdater: ShortcutsUpdater lateinit var appShortcutManager: AppShortcutManager
private lateinit var viewBadge: ViewBadge private lateinit var viewBadge: ViewBadge
private var buttonTip: WeakReference<ButtonTip>? = null
private val viewModel: DetailsViewModel by viewModels() private val viewModel: DetailsViewModel by viewModels()
private lateinit var chaptersMenuProvider: ChaptersMenuProvider private lateinit var chaptersMenuProvider: ChaptersMenuProvider
@@ -122,6 +124,7 @@ class DetailsActivity :
viewModel.onShowToast.observeEvent(this) { viewModel.onShowToast.observeEvent(this) {
makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show() makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show()
} }
viewModel.onShowTip.observeEvent(this) { showTip() }
viewModel.historyInfo.observe(this, ::onHistoryChanged) viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranch.observe(this) { viewModel.selectedBranch.observe(this) {
viewBinding.toolbarChapters?.subtitle = it viewBinding.toolbarChapters?.subtitle = it
@@ -144,7 +147,7 @@ class DetailsActivity :
activity = this, activity = this,
viewModel = viewModel, viewModel = viewModel,
snackbarHost = viewBinding.containerChapters, snackbarHost = viewBinding.containerChapters,
shortcutsUpdater = shortcutsUpdater, appShortcutManager = appShortcutManager,
), ),
) )
} }
@@ -162,6 +165,8 @@ class DetailsActivity :
override fun onLongClick(v: View): Boolean = when (v.id) { override fun onLongClick(v: View): Boolean = when (v.id) {
R.id.button_read -> { R.id.button_read -> {
buttonTip?.get()?.remove()
buttonTip = null
val menu = PopupMenu(v.context, v) val menu = PopupMenu(v.context, v)
menu.inflate(R.menu.popup_read) menu.inflate(R.menu.popup_read)
menu.setOnMenuItemClickListener(this) menu.setOnMenuItemClickListener(this)
@@ -244,7 +249,7 @@ class DetailsActivity :
bottomMargin = insets.bottom + marginEnd bottomMargin = insets.bottom + marginEnd
} }
viewBinding.dragHandle?.updateLayoutParams<MarginLayoutParams> { viewBinding.dragHandle?.updateLayoutParams<MarginLayoutParams> {
bottomMargin = insets.bottom bottomMargin = insets.top
} }
} }
@@ -296,12 +301,11 @@ class DetailsActivity :
snackbar.show() snackbar.show()
} else { } else {
startActivity( startActivity(
ReaderActivity.newIntent( IntentBuilder(this)
context = this, .manga(manga)
manga = manga, .branch(viewModel.selectedBranchValue)
branch = viewModel.selectedBranchValue, .incognito(isIncognitoMode)
isIncognitoMode = isIncognitoMode, .build(),
),
) )
if (isIncognitoMode) { if (isIncognitoMode) {
Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
@@ -345,8 +349,16 @@ class DetailsActivity :
} }
} }
private fun showTip() {
val tip = ButtonTip(viewBinding.root as ViewGroup, insetsDelegate, viewModel)
tip.addToRoot()
buttonTip = WeakReference(tip)
}
companion object { companion object {
const val TIP_BUTTON = "btn_read"
fun newIntent(context: Context, manga: Manga): Intent { fun newIntent(context: Context, manga: Manga): Intent {
return Intent(context, DetailsActivity::class.java) return Intent(context, DetailsActivity::class.java)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))

View File

@@ -23,7 +23,6 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.core.model.countChaptersByBranch import org.koitharu.kotatsu.core.model.countChaptersByBranch
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@@ -45,6 +44,7 @@ import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -67,7 +67,7 @@ class DetailsFragment :
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@Inject @Inject
lateinit var tagHighlighter: MangaTagHighlighter lateinit var tagHighlighter: ListExtraProvider
private val viewModel by activityViewModels<DetailsViewModel>() private val viewModel by activityViewModels<DetailsViewModel>()
@@ -96,8 +96,8 @@ class DetailsFragment :
override fun onItemClick(item: Bookmark, view: View) { override fun onItemClick(item: Bookmark, view: View) {
startActivity( startActivity(
ReaderActivity.newIntent(view.context, item), ReaderActivity.IntentBuilder(view.context).bookmark(item).incognito(true).build(),
scaleUpActivityOptionsOf(view).toBundle(), scaleUpActivityOptionsOf(view),
) )
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show() Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
} }
@@ -257,7 +257,7 @@ class DetailsFragment :
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
manga.source, manga.source,
), ),
scaleUpActivityOptionsOf(v).toBundle(), scaleUpActivityOptionsOf(v),
) )
} }
} }
@@ -283,7 +283,7 @@ class DetailsFragment :
manga.tags.map { tag -> manga.tags.map { tag ->
ChipsView.ChipModel( ChipsView.ChipModel(
title = tag.title, title = tag.title,
tint = tagHighlighter.getTint(tag), tint = tagHighlighter.getTagTint(tag),
icon = 0, icon = 0,
data = tag, data = tag,
isCheckable = false, isCheckable = false,

View File

@@ -15,7 +15,7 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.details.ui.model.MangaBranch import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet
@@ -29,7 +29,7 @@ class DetailsMenuProvider(
private val activity: FragmentActivity, private val activity: FragmentActivity,
private val viewModel: DetailsViewModel, private val viewModel: DetailsViewModel,
private val snackbarHost: View, private val snackbarHost: View,
private val shortcutsUpdater: ShortcutsUpdater, private val appShortcutManager: AppShortcutManager,
) : MenuProvider { ) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
@@ -112,7 +112,7 @@ class DetailsMenuProvider(
R.id.action_shortcut -> { R.id.action_shortcut -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
activity.lifecycleScope.launch { activity.lifecycleScope.launch {
if (!shortcutsUpdater.requestPinShortcut(it)) { if (!appShortcutManager.requestPinShortcut(it)) {
Snackbar.make(snackbarHost, R.string.operation_not_supported, Snackbar.LENGTH_SHORT) Snackbar.make(snackbarHost, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
.show() .show()
} }

View File

@@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -37,6 +39,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.computeSize import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.details.domain.BranchComparator import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.domain.DetailsInteractor import org.koitharu.kotatsu.details.domain.DetailsInteractor
@@ -77,6 +80,7 @@ class DetailsViewModel @Inject constructor(
private var loadingJob: Job private var loadingJob: Job
val onShowToast = MutableEventFlow<Int>() val onShowToast = MutableEventFlow<Int>()
val onShowTip = MutableEventFlow<Unit>()
val onDownloadStarted = MutableEventFlow<Unit>() val onDownloadStarted = MutableEventFlow<Unit>()
val manga = doubleManga.map { it?.any } val manga = doubleManga.map { it?.any }
@@ -135,7 +139,7 @@ class DetailsViewModel @Inject constructor(
if (description.isNullOrEmpty()) { if (description.isNullOrEmpty()) {
emit(null) emit(null)
} else { } else {
emit(description.parseAsHtml().filterSpans()) emit(description.parseAsHtml().filterSpans().sanitize())
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans()) emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
} }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null)
@@ -190,6 +194,12 @@ class DetailsViewModel @Inject constructor(
localStorageChanges localStorageChanges
.collect { onDownloadComplete(it) } .collect { onDownloadComplete(it) }
} }
launchJob(Dispatchers.Default) {
if (settings.isTipEnabled(DetailsActivity.TIP_BUTTON)) {
manga.filterNot { it?.chapters.isNullOrEmpty() }.first()
onShowTip.call(Unit)
}
}
} }
fun reload() { fun reload() {
@@ -276,6 +286,10 @@ class DetailsViewModel @Inject constructor(
} }
} }
fun onButtonTipClosed() {
settings.closeTip(DetailsActivity.TIP_BUTTON)
}
private fun doLoad() = launchLoadingJob(Dispatchers.Default) { private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
val result = doubleMangaLoadUseCase(intent) val result = doubleMangaLoadUseCase(intent)
val manga = result.requireAny() val manga = result.requireAny()

View File

@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
@@ -109,7 +110,7 @@ class ScrobblingInfoSheet :
R.id.imageView_cover -> { R.id.imageView_cover -> {
val coverUrl = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.coverUrl ?: return val coverUrl = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.coverUrl ?: return
val options = scaleUpActivityOptionsOf(v) val options = scaleUpActivityOptionsOf(v)
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle()) startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options)
} }
} }
} }
@@ -122,7 +123,7 @@ class ScrobblingInfoSheet :
} }
requireViewBinding().textViewTitle.text = scrobbling.title requireViewBinding().textViewTitle.text = scrobbling.title
requireViewBinding().ratingBar.rating = scrobbling.rating * requireViewBinding().ratingBar.numStars requireViewBinding().ratingBar.rating = scrobbling.rating * requireViewBinding().ratingBar.numStars
requireViewBinding().textViewDescription.text = scrobbling.description requireViewBinding().textViewDescription.text = scrobbling.description?.sanitize()
requireViewBinding().spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1) requireViewBinding().spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
requireViewBinding().imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId) requireViewBinding().imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId)
requireViewBinding().imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId) requireViewBinding().imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId)

View File

@@ -7,6 +7,7 @@ import android.view.MenuItem
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.settings.SettingsActivity
class DownloadsMenuProvider( class DownloadsMenuProvider(
private val context: Context, private val context: Context,
@@ -23,6 +24,10 @@ class DownloadsMenuProvider(
R.id.action_resume -> viewModel.resumeAll() R.id.action_resume -> viewModel.resumeAll()
R.id.action_cancel_all -> confirmCancelAll() R.id.action_cancel_all -> confirmCancelAll()
R.id.action_remove_completed -> confirmRemoveCompleted() R.id.action_remove_completed -> confirmRemoveCompleted()
R.id.action_settings -> {
context.startActivity(SettingsActivity.newDownloadsSettingsIntent(context))
}
else -> return false else -> return false
} }
return true return true

View File

@@ -32,7 +32,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.util.UUID import java.util.UUID
import com.google.android.material.R as materialR import com.google.android.material.R as materialR

View File

@@ -60,7 +60,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit

View File

@@ -9,7 +9,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
class ExploreRepository @Inject constructor( class ExploreRepository @Inject constructor(

View File

@@ -112,7 +112,7 @@ class FavouriteCategoriesActivity :
} }
val intent = FavouritesActivity.newIntent(this, item) val intent = FavouritesActivity.newIntent(this, item)
val options = scaleUpActivityOptionsOf(view) val options = scaleUpActivityOptionsOf(view)
startActivity(intent, options.toBundle()) startActivity(intent, options)
} }
override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean { override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean {

View File

@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
@@ -81,6 +82,6 @@ class FavouriteCategoriesSheet :
) )
}, },
) )
}.show(fm, TAG) }.showDistinct(fm, TAG)
} }
} }

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet.Companion.KEY_MANGA_LIST import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet.Companion.KEY_MANGA_LIST
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
@@ -25,7 +26,7 @@ class MangaCategoriesViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository, private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() { ) : BaseViewModel() {
private val manga = requireNotNull(savedStateHandle.get<List<ParcelableManga>>(KEY_MANGA_LIST)).map { it.manga } private val manga = savedStateHandle.require<List<ParcelableManga>>(KEY_MANGA_LIST).map { it.manga }
private val header = CategoriesHeaderItem() private val header = CategoriesHeaderItem()
val content: StateFlow<List<ListModel>> = combine( val content: StateFlow<List<ListModel>> = combine(

View File

@@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
@@ -21,8 +20,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
@@ -30,19 +27,16 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FavouritesListViewModel @Inject constructor( class FavouritesListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val repository: FavouritesRepository, private val repository: FavouritesRepository,
private val trackingRepository: TrackingRepository, private val listExtraProvider: ListExtraProvider,
private val historyRepository: HistoryRepository, settings: AppSettings,
private val settings: AppSettings,
private val tagHighlighter: MangaTagHighlighter,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider { ) : MangaListViewModel(settings, downloadScheduler) {
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
@@ -76,7 +70,7 @@ class FavouritesListViewModel @Inject constructor(
), ),
) )
else -> list.toUi(mode, this, tagHighlighter) else -> list.toUi(mode, listExtraProvider)
} }
}.catch { }.catch {
emit(listOf(it.toErrorState(canRetry = false))) emit(listOf(it.toErrorState(canRetry = false)))
@@ -108,20 +102,4 @@ class FavouritesListViewModel @Inject constructor(
repository.setCategoryOrder(categoryId, order) repository.setCategoryOrder(categoryId, order)
} }
} }
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

@@ -35,7 +35,7 @@ import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.text.Collator import java.text.Collator
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale import java.util.Locale

View File

@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.databinding.SheetFilterBinding import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -54,6 +55,6 @@ class FilterSheetFragment :
private const val TAG = "FilterBottomSheet" private const val TAG = "FilterBottomSheet"
fun show(fm: FragmentManager) = FilterSheetFragment().show(fm, TAG) fun show(fm: FragmentManager) = FilterSheetFragment().showDistinct(fm, TAG)
} }
} }

View File

@@ -1,6 +1,10 @@
package org.koitharu.kotatsu.history.data package org.koitharu.kotatsu.history.data
import androidx.room.* import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
@@ -20,6 +24,10 @@ abstract class HistoryDao {
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC") @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")
abstract fun observeAll(): Flow<List<HistoryWithManga>> abstract fun observeAll(): Flow<List<HistoryWithManga>>
@Transaction
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history WHERE deleted_at = 0)") @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history WHERE deleted_at = 0)")
abstract suspend fun findAllManga(): List<MangaEntity> abstract suspend fun findAllManga(): List<MangaEntity>

View File

@@ -51,6 +51,12 @@ class HistoryRepository @Inject constructor(
} }
} }
fun observeAll(limit: Int): Flow<List<Manga>> {
return db.historyDao.observeAll(limit).mapItems {
it.manga.toManga(it.tags.toMangaTags())
}
}
fun observeAllWithHistory(): Flow<List<MangaWithHistory>> { fun observeAllWithHistory(): Flow<List<MangaWithHistory>> {
return db.historyDao.observeAll().mapItems { return db.historyDao.observeAll().mapItems {
MangaWithHistory( MangaWithHistory(

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
class HistoryUpdateUseCase @Inject constructor( class HistoryUpdateUseCase @Inject constructor(

View File

@@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
@@ -21,8 +20,8 @@ import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -31,7 +30,6 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toGridModel import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
import org.koitharu.kotatsu.list.ui.model.toListModel import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@@ -40,8 +38,7 @@ import javax.inject.Inject
class HistoryListViewModel @Inject constructor( class HistoryListViewModel @Inject constructor(
private val repository: HistoryRepository, private val repository: HistoryRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val trackingRepository: TrackingRepository, private val extraProvider: ListExtraProvider,
private val tagHighlighter: MangaTagHighlighter,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) { ) : MangaListViewModel(settings, downloadScheduler) {
@@ -106,7 +103,6 @@ class HistoryListViewModel @Inject constructor(
mode: ListMode, mode: ListMode,
): List<ListModel> { ): List<ListModel> {
val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1) val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1)
val showPercent = settings.isReadingIndicatorsEnabled
var prevDate: DateTimeAgo? = null var prevDate: DateTimeAgo? = null
for ((manga, history) in list) { for ((manga, history) in list) {
if (grouped) { if (grouped) {
@@ -116,16 +112,10 @@ class HistoryListViewModel @Inject constructor(
} }
prevDate = date prevDate = date
} }
val counter = if (settings.isTrackerEnabled) {
trackingRepository.getNewChaptersCount(manga.id)
} else {
0
}
val percent = if (showPercent) history.percent else PROGRESS_NONE
result += when (mode) { result += when (mode) {
ListMode.LIST -> manga.toListModel(counter, percent) ListMode.LIST -> manga.toListModel(extraProvider)
ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, percent, tagHighlighter) ListMode.DETAILED_LIST -> manga.toListDetailedModel(extraProvider)
ListMode.GRID -> manga.toGridModel(counter, percent) ListMode.GRID -> manga.toGridModel(extraProvider)
} }
} }
return result return result

View File

@@ -1,8 +1,60 @@
package org.koitharu.kotatsu.list.domain package org.koitharu.kotatsu.list.domain
interface ListExtraProvider { 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.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import javax.inject.Inject
suspend fun getCounter(mangaId: Long): Int @Reusable
class ListExtraProvider @Inject constructor(
@ApplicationContext context: Context,
private val settings: AppSettings,
private val trackingRepository: TrackingRepository,
private val historyRepository: HistoryRepository,
) {
suspend fun getProgress(mangaId: Long): Float 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
}
}
suspend fun getCounter(mangaId: Long): Int {
return if (settings.isTrackerEnabled) {
trackingRepository.getNewChaptersCount(mangaId)
} else {
0
}
}
suspend fun getProgress(mangaId: Long): Float {
return if (settings.isReadingIndicatorsEnabled) {
historyRepository.getProgress(mangaId)
} else {
PROGRESS_NONE
}
}
@ColorRes
fun getTagTint(tag: MangaTag): Int {
return if (tag.title.lowercase() in dict) {
R.color.warning
} else {
0
}
}
}

View File

@@ -1,32 +0,0 @@
package org.koitharu.kotatsu.list.domain
import dagger.Reusable
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import javax.inject.Inject
@Reusable
class ListExtraProviderImpl @Inject constructor(
private val settings: AppSettings,
private val trackingRepository: TrackingRepository,
private val historyRepository: HistoryRepository,
) : ListExtraProvider {
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

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.setValueRounded import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
import org.koitharu.kotatsu.databinding.DialogListModeBinding import org.koitharu.kotatsu.databinding.DialogListModeBinding
import javax.inject.Inject import javax.inject.Inject
@@ -72,6 +73,6 @@ class ListModeBottomSheet :
private const val TAG = "ListModeSelectDialog" private const val TAG = "ListModeSelectDialog"
fun show(fm: FragmentManager) = ListModeBottomSheet().show(fm, TAG) fun show(fm: FragmentManager) = ListModeBottomSheet().showDistinct(fm, TAG)
} }
} }

View File

@@ -35,7 +35,6 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.clearItemDecorations import org.koitharu.kotatsu.core.util.ext.clearItemDecorations
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.measureHeight import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
@@ -56,7 +55,7 @@ import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
import javax.inject.Inject import javax.inject.Inject
@@ -114,8 +113,6 @@ abstract class MangaListFragment :
fastScroller.setFastScrollListener(this@MangaListFragment) fastScroller.setFastScrollListener(this@MangaListFragment)
} }
with(binding.swipeRefreshLayout) { with(binding.swipeRefreshLayout) {
setProgressBackgroundColorSchemeColor(context.getThemeColor(com.google.android.material.R.attr.colorPrimary))
setColorSchemeColors(context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary))
setOnRefreshListener(this@MangaListFragment) setOnRefreshListener(this@MangaListFragment)
isEnabled = isSwipeRefreshEnabled isEnabled = isSwipeRefreshEnabled
} }
@@ -151,8 +148,8 @@ abstract class MangaListFragment :
override fun onReadClick(manga: Manga, view: View) { override fun onReadClick(manga: Manga, view: View) {
if (selectionController?.onItemClick(manga.id) != true) { if (selectionController?.onItemClick(manga.id) != true) {
val intent = ReaderActivity.newIntent(context ?: return, manga) val intent = IntentBuilder(view.context).manga(manga).build()
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle()) startActivity(intent, scaleUpActivityOptionsOf(view))
} }
} }

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.ifZero import org.koitharu.kotatsu.core.util.ext.ifZero
@@ -14,34 +13,31 @@ import org.koitharu.kotatsu.parsers.model.Manga
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
fun Manga.toListModel( suspend fun Manga.toListModel(
counter: Int, extraProvider: ListExtraProvider?
progress: Float,
) = MangaListModel( ) = MangaListModel(
id = id, id = id,
title = title, title = title,
subtitle = tags.joinToString(", ") { it.title }, subtitle = tags.joinToString(", ") { it.title },
coverUrl = coverUrl, coverUrl = coverUrl,
manga = this, manga = this,
counter = counter, counter = extraProvider?.getCounter(id) ?: 0,
progress = progress, progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
) )
fun Manga.toListDetailedModel( suspend fun Manga.toListDetailedModel(
counter: Int, extraProvider: ListExtraProvider?,
progress: Float,
tagHighlighter: MangaTagHighlighter?,
) = MangaListDetailedModel( ) = MangaListDetailedModel(
id = id, id = id,
title = title, title = title,
subtitle = altTitle, subtitle = altTitle,
coverUrl = coverUrl, coverUrl = coverUrl,
manga = this, manga = this,
counter = counter, counter = extraProvider?.getCounter(id) ?: 0,
progress = progress, progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
tags = tags.map { tags = tags.map {
ChipsView.ChipModel( ChipsView.ChipModel(
tint = tagHighlighter?.getTint(it) ?: 0, tint = extraProvider?.getTagTint(it) ?: 0,
title = it.title, title = it.title,
icon = 0, icon = 0,
isCheckable = false, isCheckable = false,
@@ -51,53 +47,30 @@ fun Manga.toListDetailedModel(
}, },
) )
fun Manga.toGridModel(counter: Int, progress: Float) = MangaGridModel( suspend fun Manga.toGridModel(
extraProvider: ListExtraProvider?,
) = MangaGridModel(
id = id, id = id,
title = title, title = title,
coverUrl = coverUrl, coverUrl = coverUrl,
manga = this, manga = this,
counter = counter, counter = extraProvider?.getCounter(id) ?: 0,
progress = progress, progress = extraProvider?.getProgress(id) ?: PROGRESS_NONE,
) )
suspend fun List<Manga>.toUi( suspend fun List<Manga>.toUi(
mode: ListMode, mode: ListMode,
extraProvider: ListExtraProvider, extraProvider: ListExtraProvider,
tagHighlighter: MangaTagHighlighter?, ): List<MangaItemModel> = toUi(ArrayList(size), mode, extraProvider)
): List<MangaItemModel> = toUi(ArrayList(size), mode, extraProvider, tagHighlighter)
fun List<Manga>.toUi(
mode: ListMode,
tagHighlighter: MangaTagHighlighter?,
): List<MangaItemModel> = toUi(ArrayList(size), mode, tagHighlighter)
fun <C : MutableCollection<in MangaItemModel>> List<Manga>.toUi(
destination: C,
mode: ListMode,
tagHighlighter: MangaTagHighlighter?,
): C = when (mode) {
ListMode.LIST -> mapTo(destination) { it.toListModel(0, PROGRESS_NONE) }
ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(0, PROGRESS_NONE, tagHighlighter) }
ListMode.GRID -> mapTo(destination) { it.toGridModel(0, PROGRESS_NONE) }
}
suspend fun <C : MutableCollection<in MangaItemModel>> List<Manga>.toUi( suspend fun <C : MutableCollection<in MangaItemModel>> List<Manga>.toUi(
destination: C, destination: C,
mode: ListMode, mode: ListMode,
extraProvider: ListExtraProvider, extraProvider: ListExtraProvider,
tagHighlighter: MangaTagHighlighter?,
): C = when (mode) { ): C = when (mode) {
ListMode.LIST -> mapTo(destination) { ListMode.LIST -> mapTo(destination) { it.toListModel(extraProvider) }
it.toListModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id)) ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(extraProvider) }
} ListMode.GRID -> mapTo(destination) { it.toGridModel(extraProvider) }
ListMode.DETAILED_LIST -> mapTo(destination) {
it.toListDetailedModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id), tagHighlighter)
}
ListMode.GRID -> mapTo(destination) {
it.toGridModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id))
}
} }
fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState( fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState(

View File

@@ -27,7 +27,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File import java.io.File
import java.util.EnumSet import java.util.EnumSet
import javax.inject.Inject import javax.inject.Inject

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.local.data package org.koitharu.kotatsu.local.data
import android.content.Context import android.content.Context
import android.os.StatFs
import com.tomclaw.cache.DiskLruCache import com.tomclaw.cache.DiskLruCache
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -17,7 +18,7 @@ import org.koitharu.kotatsu.core.util.ext.takeIfWriteable
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -33,7 +34,8 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
} }
private val lruCache = SuspendLazy { private val lruCache = SuspendLazy {
val dir = cacheDir.get() val dir = cacheDir.get()
val size = FileSize.MEGABYTES.convert(200, FileSize.BYTES) val availableSize = (getAvailableSize() * 0.8).toLong()
val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN)
runCatchingCancellable { runCatchingCancellable {
DiskLruCache.create(dir, size) DiskLruCache.create(dir, size)
}.recoverCatching { error -> }.recoverCatching { error ->
@@ -63,4 +65,20 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
file.delete() file.delete()
} }
} }
private suspend fun getAvailableSize(): Long = runCatchingCancellable {
val statFs = StatFs(cacheDir.get().absolutePath)
statFs.availableBytes
}.onFailure {
it.printStackTraceDebug()
}.getOrDefault(SIZE_DEFAULT)
private companion object {
val SIZE_MIN
get() = FileSize.MEGABYTES.convert(20, FileSize.BYTES)
val SIZE_DEFAULT
get() = FileSize.MEGABYTES.convert(200, FileSize.BYTES)
}
} }

View File

@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject

View File

@@ -12,7 +12,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.databinding.DialogImportBinding import org.koitharu.kotatsu.databinding.DialogImportBinding
import org.koitharu.kotatsu.settings.backup.BackupDialogFragment
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.OnClickListener { class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.OnClickListener {
@@ -64,9 +63,10 @@ class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.On
} }
private fun restoreBackup(uri: Uri?) { private fun restoreBackup(uri: Uri?) {
RestoreDialogFragment.newInstance(uri ?: return) if (uri != null) {
.show(parentFragmentManager, BackupDialogFragment.TAG) RestoreDialogFragment.show(parentFragmentManager, uri)
dismiss() dismiss()
}
} }
companion object { companion object {

View File

@@ -6,7 +6,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
@@ -25,7 +24,6 @@ class LocalListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
filter: FilterCoordinator, filter: FilterCoordinator,
tagHighlighter: MangaTagHighlighter,
settings: AppSettings, settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
listExtraProvider: ListExtraProvider, listExtraProvider: ListExtraProvider,
@@ -35,7 +33,6 @@ class LocalListViewModel @Inject constructor(
savedStateHandle, savedStateHandle,
mangaRepositoryFactory, mangaRepositoryFactory,
filter, filter,
tagHighlighter,
settings, settings,
listExtraProvider, listExtraProvider,
downloadScheduler, downloadScheduler,

View File

@@ -60,7 +60,7 @@ import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
@@ -126,8 +126,10 @@ class MainActivity :
viewBinding.navRail?.headerView?.setOnClickListener(this) viewBinding.navRail?.headerView?.setOnClickListener(this)
viewBinding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null viewBinding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
navigationDelegate = navigationDelegate = MainNavigationDelegate(
MainNavigationDelegate(checkNotNull(bottomNav ?: viewBinding.navRail), supportFragmentManager) navBar = checkNotNull(bottomNav ?: viewBinding.navRail),
fragmentManager = supportFragmentManager,
)
navigationDelegate.addOnFragmentChangedListener(this) navigationDelegate.addOnFragmentChangedListener(this)
navigationDelegate.onCreate() navigationDelegate.onCreate()
@@ -156,6 +158,8 @@ class MainActivity :
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) { override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
adjustFabVisibility(topFragment = fragment) adjustFabVisibility(topFragment = fragment)
if (fromUser) { if (fromUser) {
actionModeDelegate.finishActionMode()
closeSearchCallback.handleOnBackPressed()
viewBinding.appbar.setExpanded(true) viewBinding.appbar.setExpanded(true)
} }
} }
@@ -243,21 +247,21 @@ class MainActivity :
override fun onSupportActionModeStarted(mode: ActionMode) { override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode) super.onSupportActionModeStarted(mode)
adjustFabVisibility() adjustFabVisibility()
showNav(false) bottomNav?.hide()
} }
override fun onSupportActionModeFinished(mode: ActionMode) { override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode) super.onSupportActionModeFinished(mode)
adjustFabVisibility() adjustFabVisibility()
showNav(true) bottomNav?.show()
} }
private fun onOpenReader(manga: Manga) { private fun onOpenReader(manga: Manga) {
val fab = viewBinding.fab ?: viewBinding.navRail?.headerView val fab = viewBinding.fab ?: viewBinding.navRail?.headerView
val options = fab?.let { val options = fab?.let {
scaleUpActivityOptionsOf(it).toBundle() scaleUpActivityOptionsOf(it)
} }
startActivity(ReaderActivity.newIntent(this, manga), options) startActivity(IntentBuilder(this).manga(manga).build(), options)
} }
private fun onCountersChanged(counters: SparseIntArray) { private fun onCountersChanged(counters: SparseIntArray) {
@@ -301,17 +305,6 @@ class MainActivity :
closeSearchCallback.isEnabled = false closeSearchCallback.isEnabled = false
} }
private fun showNav(visible: Boolean) {
bottomNav?.run {
if (visible) {
show()
} else {
hide()
}
}
viewBinding.navRail?.isVisible = visible
}
private fun isSearchOpened(): Boolean { private fun isSearchOpened(): Boolean {
return supportFragmentManager.findFragmentByTag(TAG_SEARCH) != null return supportFragmentManager.findFragmentByTag(TAG_SEARCH) != null
} }
@@ -340,7 +333,7 @@ class MainActivity :
} }
private fun adjustFabVisibility( private fun adjustFabVisibility(
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true, isResumeEnabled: Boolean = viewModel.isResumeEnabled.value,
topFragment: Fragment? = navigationDelegate.primaryFragment, topFragment: Fragment? = navigationDelegate.primaryFragment,
isSearchOpened: Boolean = isSearchOpened(), isSearchOpened: Boolean = isSearchOpened(),
) { ) {
@@ -383,7 +376,7 @@ class MainActivity :
supportActionBar?.setHomeAsUpIndicator( supportActionBar?.setHomeAsUpIndicator(
if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material, if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material,
) )
showNav(!isOpened) bottomNav?.showOrHide(!isOpened)
} }
private fun requestNotificationsPermission() { private fun requestNotificationsPermission() {

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipFile import java.util.zip.ZipFile
import javax.inject.Inject import javax.inject.Inject

View File

@@ -12,6 +12,7 @@ import dagger.hilt.android.lifecycle.RetainedLifecycle
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -32,6 +33,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
import org.koitharu.kotatsu.core.util.ext.ramAvailable import org.koitharu.kotatsu.core.util.ext.ramAvailable
import org.koitharu.kotatsu.core.util.ext.withProgress import org.koitharu.kotatsu.core.util.ext.withProgress
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
@@ -40,7 +42,7 @@ import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File import java.io.File
import java.util.LinkedList import java.util.LinkedList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@@ -103,7 +105,7 @@ class PageLoader @Inject constructor(
} }
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<File, Float> { fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<File, Float> {
var task = tasks[page.id] var task = tasks[page.id]?.takeIf { it.isValid() }
if (force) { if (force) {
task?.cancel() task?.cancel()
} else if (task?.isCancelled == false) { } else if (task?.isCancelled == false) {
@@ -216,6 +218,15 @@ class PageLoader @Inject constructor(
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES) return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
} }
private fun Deferred<File>.isValid(): Boolean {
return if (isCompleted) {
val file = getCompleted()
file.exists() && file.isNotEmpty()
} else {
true
}
}
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
CoroutineExceptionHandler { CoroutineExceptionHandler {

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetChaptersBinding import org.koitharu.kotatsu.databinding.SheetChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
@@ -87,6 +88,6 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(), OnListItemClick
) = ChaptersSheet().withArgs(2) { ) = ChaptersSheet().withArgs(2) {
putParcelable(ARG_CHAPTERS, ParcelableMangaChapters(chapters)) putParcelable(ARG_CHAPTERS, ParcelableMangaChapters(chapters))
putLong(ARG_CURRENT_ID, currentId) putLong(ARG_CURRENT_ID, currentId)
}.show(fm, TAG) }.showDistinct(fm, TAG)
} }
} }

View File

@@ -398,6 +398,44 @@ class ReaderActivity :
} }
} }
class IntentBuilder(context: Context) {
private val intent = Intent(context, ReaderActivity::class.java)
.setAction(ACTION_MANGA_READ)
fun manga(manga: Manga) = apply {
intent.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
}
fun mangaId(mangaId: Long) = apply {
intent.putExtra(MangaIntent.KEY_ID, mangaId)
}
fun incognito(incognito: Boolean) = apply {
intent.putExtra(EXTRA_INCOGNITO, incognito)
}
fun branch(branch: String?) = apply {
intent.putExtra(EXTRA_BRANCH, branch)
}
fun state(state: ReaderState?) = apply {
intent.putExtra(EXTRA_STATE, state)
}
fun bookmark(bookmark: Bookmark) = manga(
bookmark.manga,
).state(
ReaderState(
chapterId = bookmark.chapterId,
page = bookmark.page,
scroll = bookmark.scroll,
),
)
fun build() = intent
}
companion object { companion object {
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA" const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA"
@@ -405,38 +443,5 @@ class ReaderActivity :
const val EXTRA_BRANCH = "branch" const val EXTRA_BRANCH = "branch"
const val EXTRA_INCOGNITO = "incognito" const val EXTRA_INCOGNITO = "incognito"
private const val TOAST_DURATION = 1500L private const val TOAST_DURATION = 1500L
fun newIntent(context: Context, manga: Manga): Intent {
return Intent(context, ReaderActivity::class.java)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
}
fun newIntent(context: Context, manga: Manga, branch: String?, isIncognitoMode: Boolean): Intent {
return Intent(context, ReaderActivity::class.java)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
.putExtra(EXTRA_BRANCH, branch)
.putExtra(EXTRA_INCOGNITO, isIncognitoMode)
}
fun newIntent(context: Context, manga: Manga, state: ReaderState?): Intent {
return Intent(context, ReaderActivity::class.java)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
.putExtra(EXTRA_STATE, state)
}
fun newIntent(context: Context, bookmark: Bookmark): Intent {
val state = ReaderState(
chapterId = bookmark.chapterId,
page = bookmark.page,
scroll = bookmark.scroll,
)
return newIntent(context, bookmark.manga, state)
.putExtra(EXTRA_INCOGNITO, true)
}
fun newIntent(context: Context, mangaId: Long): Intent {
return Intent(context, ReaderActivity::class.java)
.putExtra(MangaIntent.KEY_ID, mangaId)
}
} }
} }

View File

@@ -22,7 +22,7 @@ import org.koitharu.kotatsu.core.util.ext.measureDimension
import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import com.google.android.material.R as materialR import com.google.android.material.R as materialR

View File

@@ -30,7 +30,7 @@ import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -42,6 +42,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.details.domain.model.DoubleManga import org.koitharu.kotatsu.details.domain.model.DoubleManga
@@ -56,7 +57,6 @@ import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@@ -73,7 +73,7 @@ class ReaderViewModel @Inject constructor(
private val pageSaveHelper: PageSaveHelper, private val pageSaveHelper: PageSaveHelper,
private val pageLoader: PageLoader, private val pageLoader: PageLoader,
private val chaptersLoader: ChaptersLoader, private val chaptersLoader: ChaptersLoader,
private val shortcutsUpdater: ShortcutsUpdater, private val appShortcutManager: AppShortcutManager,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase, private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
private val historyUpdateUseCase: HistoryUpdateUseCase, private val historyUpdateUseCase: HistoryUpdateUseCase,
private val detectReaderModeUseCase: DetectReaderModeUseCase, private val detectReaderModeUseCase: DetectReaderModeUseCase,
@@ -155,7 +155,7 @@ class ReaderViewModel @Inject constructor(
}.launchIn(viewModelScope + Dispatchers.Default) }.launchIn(viewModelScope + Dispatchers.Default)
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val mangaId = mangaFlow.filterNotNull().first().id val mangaId = mangaFlow.filterNotNull().first().id
shortcutsUpdater.notifyMangaOpened(mangaId) appShortcutManager.notifyMangaOpened(mangaId)
} }
} }
@@ -288,7 +288,7 @@ class ReaderViewModel @Inject constructor(
chapterId = state.chapterId, chapterId = state.chapterId,
page = state.page, page = state.page,
scroll = state.scroll, scroll = state.scroll,
imageUrl = page.preview.ifNullOrEmpty { pageLoader.getPageUrl(page) }, imageUrl = page.preview.ifNullOrEmpty { page.url },
createdAt = Date(), createdAt = Date(),
percent = computePercent(state.chapterId, state.page), percent = computePercent(state.chapterId, state.page),
) )
@@ -312,8 +312,10 @@ class ReaderViewModel @Inject constructor(
private fun loadImpl() { private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) { loadingJob = launchLoadingJob(Dispatchers.Default) {
var manga = var manga = DoubleManga(
DoubleManga(dataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")) dataRepository.resolveIntent(intent)
?: throw NotFoundException("Cannot find manga", ""),
)
mangaData.value = manga mangaData.value = manga
manga = doubleMangaLoadUseCase(intent) manga = doubleMangaLoadUseCase(intent)
chaptersLoader.init(manga) chaptersLoader.init(manga)

View File

@@ -68,10 +68,10 @@ class ColorFilterConfigActivity :
viewModel.colorFilter.observe(this, this::onColorFilterChanged) viewModel.colorFilter.observe(this, this::onColorFilterChanged)
viewModel.isLoading.observe(this, this::onLoadingChanged) viewModel.isLoading.observe(this, this::onLoadingChanged)
viewModel.preview.observe(this, this::onPreviewChanged)
viewModel.onDismiss.observeEvent(this) { viewModel.onDismiss.observeEvent(this) {
finishAfterTransition() finishAfterTransition()
} }
loadPreview(viewModel.preview)
} }
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
@@ -114,13 +114,13 @@ class ColorFilterConfigActivity :
viewBinding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter() viewBinding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
} }
private fun onPreviewChanged(preview: MangaPage?) { private fun loadPreview(page: MangaPage) {
if (preview == null) return val data: Any = page.preview?.takeUnless { it.isEmpty() } ?: page
ImageRequest.Builder(this@ColorFilterConfigActivity) ImageRequest.Builder(this@ColorFilterConfigActivity)
.data(preview) .data(data)
.scale(Scale.FILL) .scale(Scale.FILL)
.decodeRegion() .decodeRegion()
.tag(preview.source) .tag(page.source)
.indicator(listOf(viewBinding.progressBefore, viewBinding.progressAfter)) .indicator(listOf(viewBinding.progressBefore, viewBinding.progressAfter))
.error(R.drawable.ic_error_placeholder) .error(R.drawable.ic_error_placeholder)
.size(ViewSizeResolver(viewBinding.imageViewBefore)) .size(ViewSizeResolver(viewBinding.imageViewBefore))

View File

@@ -7,11 +7,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
import javax.inject.Inject import javax.inject.Inject
@@ -19,38 +18,24 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ColorFilterConfigViewModel @Inject constructor( class ColorFilterConfigViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val mangaDataRepository: MangaDataRepository, private val mangaDataRepository: MangaDataRepository,
) : BaseViewModel() { ) : BaseViewModel() {
private val manga = checkNotNull(savedStateHandle.get<ParcelableManga>(EXTRA_MANGA)?.manga) private val manga = savedStateHandle.require<ParcelableManga>(EXTRA_MANGA).manga
private var initialColorFilter: ReaderColorFilter? = null private var initialColorFilter: ReaderColorFilter? = null
val colorFilter = MutableStateFlow<ReaderColorFilter?>(null) val colorFilter = MutableStateFlow<ReaderColorFilter?>(null)
val onDismiss = MutableEventFlow<Unit>() val onDismiss = MutableEventFlow<Unit>()
val preview = MutableStateFlow<MangaPage?>(null) val preview = savedStateHandle.require<ParcelableMangaPages>(ColorFilterConfigActivity.EXTRA_PAGES).pages.first()
val isChanged: Boolean val isChanged: Boolean
get() = colorFilter.value != initialColorFilter get() = colorFilter.value != initialColorFilter
init { init {
val page = checkNotNull(
savedStateHandle.get<ParcelableMangaPages>(ColorFilterConfigActivity.EXTRA_PAGES)?.pages?.firstOrNull(),
)
launchLoadingJob { launchLoadingJob {
initialColorFilter = mangaDataRepository.getColorFilter(manga.id) initialColorFilter = mangaDataRepository.getColorFilter(manga.id)
colorFilter.value = initialColorFilter colorFilter.value = initialColorFilter
} }
launchLoadingJob(Dispatchers.Default) {
val repository = mangaRepositoryFactory.create(page.source)
val url = repository.getPageUrl(page)
preview.value = MangaPage(
id = page.id,
url = url,
preview = page.preview,
source = page.source,
)
}
} }
fun setBrightness(brightness: Float) { fun setBrightness(brightness: Float) {

View File

@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ScreenOrientationHelper import org.koitharu.kotatsu.core.util.ScreenOrientationHelper
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
@@ -192,6 +193,6 @@ class ReaderConfigSheet :
fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigSheet().withArgs(1) { fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigSheet().withArgs(1) {
putInt(ARG_MODE, mode.id) putInt(ARG_MODE, mode.id)
}.show(fm, TAG) }.showDistinct(fm, TAG)
} }
} }

View File

@@ -17,9 +17,13 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
protected val viewModel by activityViewModels<ReaderViewModel>() protected val viewModel by activityViewModels<ReaderViewModel>()
private var stateToSave: ReaderState? = null private var stateToSave: ReaderState? = null
protected var readerAdapter: BaseReaderAdapter<*>? = null
private set
override fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
var restoredState = savedInstanceState?.getParcelableCompat<ReaderState>(KEY_STATE) var restoredState = savedInstanceState?.getParcelableCompat<ReaderState>(KEY_STATE)
readerAdapter = onCreateAdapter()
viewModel.content.observe(viewLifecycleOwner) { viewModel.content.observe(viewLifecycleOwner) {
onPagesChanged(it.pages, restoredState ?: it.state) onPagesChanged(it.pages, restoredState ?: it.state)
@@ -34,6 +38,7 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
override fun onDestroyView() { override fun onDestroyView() {
stateToSave = getCurrentState() stateToSave = getCurrentState()
readerAdapter = null
super.onDestroyView() super.onDestroyView()
} }
@@ -45,6 +50,10 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
outState.putParcelable(KEY_STATE, stateToSave) outState.putParcelable(KEY_STATE, stateToSave)
} }
protected fun requireAdapter() = checkNotNull(readerAdapter) {
"Adapter was not created or already destroyed"
}
override fun onWindowInsetsChanged(insets: Insets) = Unit override fun onWindowInsetsChanged(insets: Insets) = Unit
abstract fun switchPageBy(delta: Int) abstract fun switchPageBy(delta: Int)
@@ -55,5 +64,7 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
abstract fun getCurrentState(): ReaderState? abstract fun getCurrentState(): ReaderState?
protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) protected abstract fun onCreateAdapter(): BaseReaderAdapter<*>
protected abstract suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)
} }

View File

@@ -21,7 +21,7 @@ import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException

View File

@@ -4,16 +4,18 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.recyclerView import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.resetTransformations import org.koitharu.kotatsu.core.util.ext.resetTransformations
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -33,8 +35,6 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
@Inject @Inject
lateinit var pageLoader: PageLoader lateinit var pageLoader: PageLoader
private var pagerAdapter: ReversedPagesAdapter? = null
override fun onCreateViewBinding( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -42,15 +42,8 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
pagerAdapter = ReversedPagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
with(binding.pager) { with(binding.pager) {
adapter = pagerAdapter adapter = readerAdapter
offscreenPageLimit = 2 offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged) doOnPageChanged(::notifyPageChanged)
} }
@@ -67,11 +60,18 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
} }
override fun onDestroyView() { override fun onDestroyView() {
pagerAdapter = null
requireViewBinding().pager.adapter = null requireViewBinding().pager.adapter = null
super.onDestroyView() super.onDestroyView()
} }
override fun onCreateAdapter() = ReversedPagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
override fun switchPageBy(delta: Int) { override fun switchPageBy(delta: Int) {
with(requireViewBinding().pager) { with(requireViewBinding().pager) {
setCurrentItem(currentItem - delta, context.isAnimationsEnabled) setCurrentItem(currentItem - delta, context.isAnimationsEnabled)
@@ -87,24 +87,26 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
} }
} }
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) { override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
val reversedPages = pages.asReversed() val reversedPages = pages.asReversed()
viewLifecycleScope.launch { val items = async {
val items = async { requireAdapter().setItems(reversedPages)
pagerAdapter?.setItems(reversedPages) yield()
}
if (pendingState != null) {
val position = reversedPages.indexOfLast {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
} }
if (pendingState != null) { items.await()
val position = reversedPages.indexOfLast { if (position != -1) {
it.chapterId == pendingState.chapterId && it.index == pendingState.page requireViewBinding().pager.setCurrentItem(position, false)
} notifyPageChanged(position)
items.await() ?: return@launch
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
}
} else { } else {
items.await() Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
} }
} else {
items.await()
} }
} }
@@ -123,6 +125,6 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
} }
private fun reversed(position: Int): Int { private fun reversed(position: Int): Int {
return ((pagerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0) return ((readerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0)
} }
} }

View File

@@ -20,7 +20,6 @@ import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.util.ext.*
open class PageHolder( open class PageHolder(
owner: LifecycleOwner, owner: LifecycleOwner,

View File

@@ -4,16 +4,18 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.children import androidx.core.view.children
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.recyclerView import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.resetTransformations import org.koitharu.kotatsu.core.util.ext.resetTransformations
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -32,8 +34,6 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
@Inject @Inject
lateinit var pageLoader: PageLoader lateinit var pageLoader: PageLoader
private var pagesAdapter: PagesAdapter? = null
override fun onCreateViewBinding( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -41,15 +41,8 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentReaderStandardBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
pagesAdapter = PagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
with(binding.pager) { with(binding.pager) {
adapter = pagesAdapter adapter = readerAdapter
offscreenPageLimit = 2 offscreenPageLimit = 2
doOnPageChanged(::notifyPageChanged) doOnPageChanged(::notifyPageChanged)
} }
@@ -66,31 +59,40 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>()
} }
override fun onDestroyView() { override fun onDestroyView() {
pagesAdapter = null
requireViewBinding().pager.adapter = null requireViewBinding().pager.adapter = null
super.onDestroyView() super.onDestroyView()
} }
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) { override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
viewLifecycleScope.launch { val items = async {
val items = async { requireAdapter().setItems(pages)
pagesAdapter?.setItems(pages) yield()
}
if (pendingState != null) {
val position = pages.indexOfFirst {
it.chapterId == pendingState.chapterId && it.index == pendingState.page
} }
if (pendingState != null) { items.await()
val position = pages.indexOfFirst { if (position != -1) {
it.chapterId == pendingState.chapterId && it.index == pendingState.page requireViewBinding().pager.setCurrentItem(position, false)
} notifyPageChanged(position)
items.await() ?: return@launch
if (position != -1) {
requireViewBinding().pager.setCurrentItem(position, false)
notifyPageChanged(position)
}
} else { } else {
items.await() Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
} }
} else {
items.await()
} }
} }
override fun onCreateAdapter() = PagesAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
override fun switchPageBy(delta: Int) { override fun switchPageBy(delta: Int) {
with(requireViewBinding().pager) { with(requireViewBinding().pager) {
setCurrentItem(currentItem + delta, context.isAnimationsEnabled) setCurrentItem(currentItem + delta, context.isAnimationsEnabled)

View File

@@ -4,15 +4,17 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
@@ -31,7 +33,6 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
lateinit var pageLoader: PageLoader lateinit var pageLoader: PageLoader
private val scrollInterpolator = AccelerateDecelerateInterpolator() private val scrollInterpolator = AccelerateDecelerateInterpolator()
private var webtoonAdapter: WebtoonAdapter? = null
override fun onCreateViewBinding( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -40,16 +41,9 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
webtoonAdapter = WebtoonAdapter(
lifecycleOwner = viewLifecycleOwner,
loader = pageLoader,
settings = viewModel.readerSettings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
adapter = webtoonAdapter adapter = readerAdapter
addOnPageScrollListener(PageScrollListener()) addOnPageScrollListener(PageScrollListener())
} }
@@ -59,32 +53,43 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
} }
override fun onDestroyView() { override fun onDestroyView() {
webtoonAdapter = null
requireViewBinding().recyclerView.adapter = null requireViewBinding().recyclerView.adapter = null
super.onDestroyView() super.onDestroyView()
} }
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) { override fun onCreateAdapter() = WebtoonAdapter(
viewLifecycleScope.launch { lifecycleOwner = viewLifecycleOwner,
val setItems = async { webtoonAdapter?.setItems(pages) } loader = pageLoader,
if (pendingState != null) { settings = viewModel.readerSettings,
val position = pages.indexOfFirst { networkState = networkState,
it.chapterId == pendingState.chapterId && it.index == pendingState.page exceptionResolver = exceptionResolver,
} )
setItems.await() ?: return@launch
if (position != -1) { override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
with(requireViewBinding().recyclerView) { val setItems = async {
firstVisibleItemPosition = position requireAdapter().setItems(pages)
post { yield()
(findViewHolderForAdapterPosition(position) as? WebtoonHolder) }
?.restoreScroll(pendingState.scroll) if (pendingState != null) {
} val position = pages.indexOfFirst {
} it.chapterId == pendingState.chapterId && it.index == pendingState.page
notifyPageChanged(position)
}
} else {
setItems.await()
} }
setItems.await()
if (position != -1) {
with(requireViewBinding().recyclerView) {
firstVisibleItemPosition = position
post {
(findViewHolderForAdapterPosition(position) as? WebtoonHolder)
?.restoreScroll(pendingState.scroll)
}
}
notifyPageChanged(position)
} else {
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
.show()
}
} else {
setItems.await()
} }
} }

View File

@@ -25,12 +25,13 @@ import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.plus import org.koitharu.kotatsu.core.util.ext.plus
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetPagesBinding import org.koitharu.kotatsu.databinding.SheetPagesBinding
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
import javax.inject.Inject import javax.inject.Inject
@@ -101,8 +102,8 @@ class PagesThumbnailsSheet :
listener.onPageSelected(item.page) listener.onPageSelected(item.page)
} else { } else {
val state = ReaderState(item.page.chapterId, item.page.index, 0) val state = ReaderState(item.page.chapterId, item.page.index, 0)
val intent = ReaderActivity.newIntent(view.context, viewModel.manga, state) val intent = IntentBuilder(view.context).manga(viewModel.manga).state(state).build()
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle()) startActivity(intent, scaleUpActivityOptionsOf(view))
} }
dismiss() dismiss()
} }
@@ -194,7 +195,7 @@ class PagesThumbnailsSheet :
putParcelable(ARG_MANGA, ParcelableManga(manga, true)) putParcelable(ARG_MANGA, ParcelableManga(manga, true))
putLong(ARG_CHAPTER_ID, chapterId) putLong(ARG_CHAPTER_ID, chapterId)
putInt(ARG_CURRENT_PAGE, currentPage) putInt(ARG_CURRENT_PAGE, currentPage)
}.show(fm, TAG) }.showDistinct(fm, TAG)
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -26,7 +27,7 @@ class PagesThumbnailsViewModel @Inject constructor(
private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1 private val currentPageIndex: Int = savedStateHandle[PagesThumbnailsSheet.ARG_CURRENT_PAGE] ?: -1
private val initialChapterId: Long = savedStateHandle[PagesThumbnailsSheet.ARG_CHAPTER_ID] ?: 0L private val initialChapterId: Long = savedStateHandle[PagesThumbnailsSheet.ARG_CHAPTER_ID] ?: 0L
val manga = requireNotNull(savedStateHandle.get<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA)).manga val manga = savedStateHandle.require<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA).manga
private val repository = mangaRepositoryFactory.create(manga.source) private val repository = mangaRepositoryFactory.create(manga.source)
private val mangaDetails = SuspendLazy { private val mangaDetails = SuspendLazy {

View File

@@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
@@ -37,7 +36,7 @@ import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
private const val FILTER_MIN_INTERVAL = 250L private const val FILTER_MIN_INTERVAL = 250L
@@ -47,7 +46,6 @@ open class RemoteListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
private val filter: FilterCoordinator, private val filter: FilterCoordinator,
private val tagHighlighter: MangaTagHighlighter,
settings: AppSettings, settings: AppSettings,
listExtraProvider: ListExtraProvider, listExtraProvider: ListExtraProvider,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
@@ -72,7 +70,7 @@ open class RemoteListViewModel @Inject constructor(
list == null -> add(LoadingState) list == null -> add(LoadingState)
list.isEmpty() -> add(createEmptyState(header.value.hasSelectedTags)) list.isEmpty() -> add(createEmptyState(header.value.hasSelectedTags))
else -> { else -> {
list.toUi(this, mode, listExtraProvider, tagHighlighter) list.toUi(this, mode, listExtraProvider)
when { when {
error != null -> add(error.toErrorFooter()) error != null -> add(error.toErrorFooter())
hasNext -> add(LoadingFooter()) hasNext -> add(LoadingFooter())

View File

@@ -1,6 +1,6 @@
package org.koitharu.kotatsu.scrobbling.anilist.data package org.koitharu.kotatsu.scrobbling.anilist.data
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
enum class ScoreFormat { enum class ScoreFormat {

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.util.ext.findKeyByValue import org.koitharu.kotatsu.core.util.ext.findKeyByValue
import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
@@ -20,7 +21,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.util.EnumMap import java.util.EnumMap
abstract class Scrobbler( abstract class Scrobbler(
@@ -123,7 +124,7 @@ abstract class Scrobbler(
rating = rating, rating = rating,
title = mangaInfo.name, title = mangaInfo.name,
coverUrl = mangaInfo.cover, coverUrl = mangaInfo.cover,
description = mangaInfo.descriptionHtml.parseAsHtml(), description = mangaInfo.descriptionHtml.parseAsHtml().sanitize(),
externalUrl = mangaInfo.url, externalUrl = mangaInfo.url,
) )
} }

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