Compare commits
378 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61e9796269 | ||
|
|
54597eb8f0 | ||
|
|
e07ea0552f | ||
|
|
b8e90719ce | ||
|
|
2ec716973c | ||
|
|
0000c97e6a | ||
|
|
70684de683 | ||
|
|
5a2288eb2d | ||
|
|
6a023fa976 | ||
|
|
3cc0fbe7bc | ||
|
|
b3b022807a | ||
|
|
ba0ea5a9fc | ||
|
|
ca1380e2b1 | ||
|
|
05dbd11fc1 | ||
|
|
aa650d44d3 | ||
|
|
99b698ad12 | ||
|
|
2f9c2d9ab6 | ||
|
|
ab753787b0 | ||
|
|
478ca351eb | ||
|
|
559b2cfd64 | ||
|
|
a1554f81ff | ||
|
|
4ce7f74b9d | ||
|
|
0778f34db7 | ||
|
|
bc488a6878 | ||
|
|
856524c9f8 | ||
|
|
c75160d83c | ||
|
|
d9c826524f | ||
|
|
40196205eb | ||
|
|
681ac492d6 | ||
|
|
95f4606661 | ||
|
|
b406834d4a | ||
|
|
2693ec5335 | ||
|
|
ead8a3d6df | ||
|
|
df04dcc8a3 | ||
|
|
c103773c19 | ||
|
|
0a28f131ee | ||
|
|
c8e4842b6e | ||
|
|
d54d489494 | ||
|
|
c77cb4cb3c | ||
|
|
d951306a90 | ||
|
|
c871757893 | ||
|
|
64bf671e8b | ||
|
|
b00bc22ead | ||
|
|
fdea2b47da | ||
|
|
91266183c2 | ||
|
|
20a7e5a6a8 | ||
|
|
6fa99791b6 | ||
|
|
4090c8ad6a | ||
|
|
c72ed7cc34 | ||
|
|
925c24471e | ||
|
|
7e31b1384e | ||
|
|
e1efb5fb1e | ||
|
|
29b5655efb | ||
|
|
68bdd22634 | ||
|
|
2e7867f60c | ||
|
|
e4c2972cae | ||
|
|
af654b8c40 | ||
|
|
9eace89d4f | ||
|
|
7dbec8eb8f | ||
|
|
111f816f18 | ||
|
|
d4589716aa | ||
|
|
1c4bd6da28 | ||
|
|
c83538f66d | ||
|
|
885cae7635 | ||
|
|
c66d36abf9 | ||
|
|
4bd9c6df81 | ||
|
|
b835cb98b7 | ||
|
|
7cb1f90155 | ||
|
|
96bac81b84 | ||
|
|
b59f933031 | ||
|
|
caebca36de | ||
|
|
03cb458d92 | ||
|
|
0788f5f05e | ||
|
|
0271ed2ba9 | ||
|
|
788c7b862a | ||
|
|
f4f84099cc | ||
|
|
3bea94bf1f | ||
|
|
746eed698f | ||
|
|
fa0289eb27 | ||
|
|
c874d73c04 | ||
|
|
edb91c46d4 | ||
|
|
4b9f4f9af2 | ||
|
|
a07117087a | ||
|
|
ce0ffca197 | ||
|
|
eece4d8f00 | ||
|
|
419e2e578b | ||
|
|
0a7387c22e | ||
|
|
2a23c3b3b3 | ||
|
|
0566aa4e6a | ||
|
|
3f27acf1aa | ||
|
|
d48c3fbe1b | ||
|
|
6720474667 | ||
|
|
ad42ca5085 | ||
|
|
d5e40d79ec | ||
|
|
7853b9a73e | ||
|
|
cb71a24d81 | ||
|
|
a5d99db105 | ||
|
|
6cc13784e4 | ||
|
|
cf9aab9afe | ||
|
|
83a919f9f6 | ||
|
|
70bf80ad8f | ||
|
|
05a724eae0 | ||
|
|
496f3637c4 | ||
|
|
c5c907c8dc | ||
|
|
af4845a770 | ||
|
|
511f9af991 | ||
|
|
baf6f6d2c9 | ||
|
|
8b6a0a8c87 | ||
|
|
e7ee261680 | ||
|
|
2949fdd2c6 | ||
|
|
829ea01b18 | ||
|
|
08b173b94a | ||
|
|
7b090c4ccd | ||
|
|
bf1b8e8b75 | ||
|
|
bfad632b8c | ||
|
|
73e6f730e1 | ||
|
|
62d8b848b2 | ||
|
|
2793f6ce52 | ||
|
|
b107801188 | ||
|
|
97de27dfb3 | ||
|
|
cd4317dec5 | ||
|
|
50554c6936 | ||
|
|
694297f49b | ||
|
|
3e48ce85fd | ||
|
|
0f7bceb268 | ||
|
|
00187c0d17 | ||
|
|
2378d104c3 | ||
|
|
f105f4b496 | ||
|
|
01e27ba91f | ||
|
|
2342594885 | ||
|
|
61a7f1c830 | ||
|
|
01c23bc3b8 | ||
|
|
7c7106a63c | ||
|
|
ac1a919476 | ||
|
|
234f74aa0d | ||
|
|
1711ebe616 | ||
|
|
07af79a6bd | ||
|
|
e4942b0d93 | ||
|
|
5ca22f1419 | ||
|
|
345a878d83 | ||
|
|
42bb5a65ab | ||
|
|
0c37265a5b | ||
|
|
7a65ae3ea7 | ||
|
|
376cee1859 | ||
|
|
ee027cd64f | ||
|
|
7b2bb5ea8f | ||
|
|
eff2d6bcb6 | ||
|
|
03b92c4898 | ||
|
|
6dcb537a9a | ||
|
|
052cfe26b1 | ||
|
|
45b2f2337a | ||
|
|
5785a2d5d1 | ||
|
|
bc273bfb8f | ||
|
|
513aa1a285 | ||
|
|
82a3b93214 | ||
|
|
80149b1ce7 | ||
|
|
297029a659 | ||
|
|
08acf2d882 | ||
|
|
dafca9e1e1 | ||
|
|
e174bc68af | ||
|
|
1d78c64350 | ||
|
|
321a9ecf62 | ||
|
|
83cf6aa997 | ||
|
|
e7bd74429e | ||
|
|
9ba87640c0 | ||
|
|
fff77cf208 | ||
|
|
967e8df7c9 | ||
|
|
f86d873361 | ||
|
|
2d5332d8df | ||
|
|
439a01c43f | ||
|
|
3a9d0def7d | ||
|
|
33a45ac5b3 | ||
|
|
e4c80b4443 | ||
|
|
940d448e00 | ||
|
|
5ab48a7545 | ||
|
|
cb2bdbdd9a | ||
|
|
8fdaf92cc4 | ||
|
|
0416077964 | ||
|
|
a8176e6589 | ||
|
|
a2437dd27a | ||
|
|
9e56766e9e | ||
|
|
eec750789d | ||
|
|
44a2b6db11 | ||
|
|
55ca2b8d8d | ||
|
|
2d670418c7 | ||
|
|
4c201bf950 | ||
|
|
7b60ed6bad | ||
|
|
619be69580 | ||
|
|
9f3c3f8985 | ||
|
|
f345977858 | ||
|
|
9610caf002 | ||
|
|
b75220a1b7 | ||
|
|
ab2a6f5a17 | ||
|
|
2aeefc607b | ||
|
|
9af769bc69 | ||
|
|
46b78cfcd7 | ||
|
|
c24324de9a | ||
|
|
48b9c1236d | ||
|
|
c69d293caa | ||
|
|
0f4cca0e07 | ||
|
|
d6500b8fec | ||
|
|
86140cab1e | ||
|
|
46ab5af905 | ||
|
|
9a815f28fa | ||
|
|
394479192b | ||
|
|
7908eb1441 | ||
|
|
ed672feebe | ||
|
|
4739da2774 | ||
|
|
fb674b6028 | ||
|
|
df5f5ea737 | ||
|
|
e6facd4e41 | ||
|
|
942d4fe5ab | ||
|
|
80db817ff2 | ||
|
|
b2817a2ce7 | ||
|
|
e2835e3e95 | ||
|
|
91928d058b | ||
|
|
425e8a49c4 | ||
|
|
148dbfdf02 | ||
|
|
55fdd6b7b1 | ||
|
|
a726b4f499 | ||
|
|
39cd199044 | ||
|
|
98b8aa3f2d | ||
|
|
f5b8d41a86 | ||
|
|
90dfc84119 | ||
|
|
f01fd18711 | ||
|
|
0098bdd07e | ||
|
|
6a792f8ac3 | ||
|
|
c81e8749b6 | ||
|
|
5fa260a0c7 | ||
|
|
e0ba4e2686 | ||
|
|
f188d1c0f3 | ||
|
|
6de55afa27 | ||
|
|
21dcb5b754 | ||
|
|
9b3ea57db1 | ||
|
|
032a8607ba | ||
|
|
f7303c5957 | ||
|
|
d696606ef9 | ||
|
|
0a6e106a1d | ||
|
|
de1a7f0ca8 | ||
|
|
9d31e76cc7 | ||
|
|
20910ffb5d | ||
|
|
7497ee6364 | ||
|
|
0f2ed50e18 | ||
|
|
ba066b577b | ||
|
|
4496fe876f | ||
|
|
a9f5abebf0 | ||
|
|
bebee2ef27 | ||
|
|
4ec2b0c8fe | ||
|
|
4a7be70898 | ||
|
|
2bcba1eb21 | ||
|
|
feca7ba3fc | ||
|
|
745b349e5e | ||
|
|
13946783a5 | ||
|
|
84e5400522 | ||
|
|
02c9a933d2 | ||
|
|
92af851d3b | ||
|
|
009eb9fe44 | ||
|
|
fc8a5ccd9f | ||
|
|
91f46de547 | ||
|
|
d548993e14 | ||
|
|
4f32664b33 | ||
|
|
71b14a3aa8 | ||
|
|
183a61272e | ||
|
|
f1f208ad15 | ||
|
|
c6983d794c | ||
|
|
8228153c83 | ||
|
|
844bd13a07 | ||
|
|
60a5620134 | ||
|
|
dd09a39077 | ||
|
|
1511bd3279 | ||
|
|
259c335607 | ||
|
|
86367b6d3b | ||
|
|
19b893738d | ||
|
|
d817ae0394 | ||
|
|
d81c22b586 | ||
|
|
cd23b044df | ||
|
|
4922881343 | ||
|
|
ff0d04bea6 | ||
|
|
97de629c3b | ||
|
|
7b482e5bcf | ||
|
|
fd575b8131 | ||
|
|
c77e023bef | ||
|
|
a3cf52859b | ||
|
|
5e55bce529 | ||
|
|
b1ba70bf77 | ||
|
|
b930272221 | ||
|
|
75305c0b94 | ||
|
|
24b16e2ce2 | ||
|
|
0ccbba6787 | ||
|
|
ca314867f2 | ||
|
|
236e284360 | ||
|
|
e9a09b6be4 | ||
|
|
9e1be337ed | ||
|
|
104f2ebfae | ||
|
|
6a2e12dc29 | ||
|
|
9587cb439c | ||
|
|
c42d0824b0 | ||
|
|
09f6dd9b4e | ||
|
|
b494c96e31 | ||
|
|
0f6d56ee2d | ||
|
|
8d15691e17 | ||
|
|
bd8b251934 | ||
|
|
2f1b74e45a | ||
|
|
73217b8e11 | ||
|
|
759df969c9 | ||
|
|
466e35fffa | ||
|
|
f44db3dbff | ||
|
|
315870abcb | ||
|
|
3e46b3957c | ||
|
|
6dc81468d2 | ||
|
|
56bc0dbf07 | ||
|
|
7bc33adca8 | ||
|
|
c8794d59f7 | ||
|
|
9c2a57812e | ||
|
|
6bd5033858 | ||
|
|
e7c2a76219 | ||
|
|
0934363298 | ||
|
|
de29527805 | ||
|
|
f11e964f0b | ||
|
|
61a98f54b9 | ||
|
|
50e67daea4 | ||
|
|
0030706226 | ||
|
|
056ef5433d | ||
|
|
c14b2ceeff | ||
|
|
ff2cf9d18a | ||
|
|
96b6900c70 | ||
|
|
c6228d3fe1 | ||
|
|
8ac95e1608 | ||
|
|
69a9ec354b | ||
|
|
0639d3e6c1 | ||
|
|
ae5cebd42d | ||
|
|
cd8381cbfb | ||
|
|
3132049a63 | ||
|
|
bc3a7fc211 | ||
|
|
e794f84c6f | ||
|
|
76709dda21 | ||
|
|
6dc460bc20 | ||
|
|
c2ee548f0a | ||
|
|
1847759ec3 | ||
|
|
02d5dfb375 | ||
|
|
12d8d3e2d1 | ||
|
|
b5705b45df | ||
|
|
46b797fc67 | ||
|
|
5ec7fbed94 | ||
|
|
b48c6d7d38 | ||
|
|
da4aedca97 | ||
|
|
32695f9816 | ||
|
|
bece4cc15d | ||
|
|
548c41fbf9 | ||
|
|
ef9b16da0b | ||
|
|
5d1ef983e9 | ||
|
|
eb78a776cf | ||
|
|
661e502003 | ||
|
|
8c5c7d6b04 | ||
|
|
b1187c611a | ||
|
|
893ba37c86 | ||
|
|
b1bc94b1e9 | ||
|
|
2e3be00e26 | ||
|
|
84f41810c5 | ||
|
|
f0a4fa4e95 | ||
|
|
0c132a521e | ||
|
|
3d05541f61 | ||
|
|
2442e7cbe1 | ||
|
|
4522c478cb | ||
|
|
5a0c54e00f | ||
|
|
47f346b42c | ||
|
|
dc358ae6a2 | ||
|
|
bfa9feaef0 | ||
|
|
c3216871ed | ||
|
|
a8f5714b35 | ||
|
|
84567767a0 | ||
|
|
eb7efaaac9 | ||
|
|
3729b5f2f0 | ||
|
|
e4c2797f06 | ||
|
|
e02899c3f2 | ||
|
|
96c89a716e | ||
|
|
65ed5c7e6b | ||
|
|
3f96f34b8e |
@@ -13,6 +13,7 @@ disabled_rules = no-wildcard-imports, no-unused-imports
|
|||||||
|
|
||||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||||
ij_continuation_indent_size = 4
|
ij_continuation_indent_size = 4
|
||||||
|
ij_xml_attribute_wrap = on_every_item
|
||||||
|
|
||||||
[{*.kt,*.kts}]
|
[{*.kt,*.kts}]
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
4
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
@@ -61,4 +61,6 @@ body:
|
|||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
|
- label: If this is an issue with a parser, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
|||||||
1
.idea/.gitignore
generated
vendored
1
.idea/.gitignore
generated
vendored
@@ -1,3 +1,4 @@
|
|||||||
# Default ignored files
|
# Default ignored files
|
||||||
/shelf/
|
/shelf/
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
|
/migrations.xml
|
||||||
|
|||||||
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@@ -5,7 +5,6 @@
|
|||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="GRADLE" />
|
<option name="testRunner" value="GRADLE" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="jbr-17" />
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
@@ -14,6 +13,7 @@
|
|||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
3
.weblate
Normal file
3
.weblate
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[weblate]
|
||||||
|
url = https://hosted.weblate.org/api/
|
||||||
|
translation = kotatsu/strings
|
||||||
11
CONTRIBUTING.md
Normal file
11
CONTRIBUTING.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
## Kotatsu contribution guidelines
|
||||||
|
|
||||||
|
- If you want to fix bug or implement a new feature, that already mention in the [issues](https://github.com/KotatsuApp/Kotatsu/issues), please, assign this issue to you and/or comment about it.
|
||||||
|
- Whether you have to implement new feature, please, open an issue or discussion regarding it to ensure it will be accepted.
|
||||||
|
- Translations have to be managed using the [Weblate](https://hosted.weblate.org/engage/kotatsu/) platform.
|
||||||
|
- In case you want to add a new manga source, refer to the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers).
|
||||||
|
|
||||||
|
Refactoring or some dev-faces improvements are also might be accepted, however please stick to the following principles:
|
||||||
|
- Performance matters. In the case of choosing between source code beauty and performance, performance should be a priority.
|
||||||
|
- Please, do not modify readme and other information files (except for typos).
|
||||||
|
- Avoid adding new dependencies unless required. APK size is important.
|
||||||
@@ -39,6 +39,10 @@ Kotatsu is a free and open source manga reader for Android.
|
|||||||
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
|
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
|
||||||
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
|
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the guidelines.
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|||||||
@@ -2,28 +2,29 @@ plugins {
|
|||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlin-kapt'
|
id 'kotlin-kapt'
|
||||||
|
id 'com.google.devtools.ksp'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
id 'dagger.hilt.android.plugin'
|
id 'dagger.hilt.android.plugin'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 33
|
compileSdk = 34
|
||||||
buildToolsVersion = '33.0.2'
|
buildToolsVersion = '34.0.0'
|
||||||
namespace = 'org.koitharu.kotatsu'
|
namespace = 'org.koitharu.kotatsu'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdk = 21
|
||||||
targetSdkVersion 33
|
targetSdk = 34
|
||||||
versionCode 547
|
versionCode = 573
|
||||||
versionName '5.1.3'
|
versionName = '6.0'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
||||||
|
ksp {
|
||||||
kapt {
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
arguments {
|
}
|
||||||
arg 'room.schemaLocation', "$projectDir/schemas".toString()
|
androidResources {
|
||||||
}
|
generateLocaleConfig true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -39,9 +40,11 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
|
main.java.srcDirs += 'src/main/kotlin/'
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
@@ -59,7 +62,7 @@ android {
|
|||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
abortOnError true
|
abortOnError true
|
||||||
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged'
|
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled'
|
||||||
}
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.includeAndroidResources true
|
unitTests.includeAndroidResources true
|
||||||
@@ -78,80 +81,80 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:fa7ea5b16a') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:3a76504380') {
|
||||||
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.9.10'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||||
|
|
||||||
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.1'
|
||||||
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'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.1'
|
||||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
|
|
||||||
|
|
||||||
|
// TODO https://issuetracker.google.com/issues/254846063
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.google.guava:guava:31.1-android') {
|
implementation('com.google.guava:guava:32.0.1-android') {
|
||||||
exclude group: 'com.google.guava', module: 'failureaccess'
|
exclude group: 'com.google.guava', module: 'failureaccess'
|
||||||
exclude group: 'org.checkerframework', module: 'checker-qual'
|
exclude group: 'org.checkerframework', module: 'checker-qual'
|
||||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'androidx.room:room-runtime:2.5.1'
|
implementation 'androidx.room:room-runtime:2.5.2'
|
||||||
implementation 'androidx.room:room-ktx:2.5.1'
|
implementation 'androidx.room:room-ktx:2.5.2'
|
||||||
kapt 'androidx.room:room-compiler:2.5.1'
|
ksp 'androidx.room:room-compiler:2.5.2'
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
||||||
implementation 'com.squareup.okio:okio:3.3.0'
|
implementation 'com.squareup.okio:okio:3.5.0'
|
||||||
|
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
||||||
|
|
||||||
implementation 'com.google.dagger:hilt-android:2.46.1'
|
implementation 'com.google.dagger:hilt-android:2.47'
|
||||||
kapt 'com.google.dagger:hilt-compiler:2.46.1'
|
kapt 'com.google.dagger:hilt-compiler:2.47'
|
||||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||||
|
|
||||||
implementation 'io.coil-kt:coil-base:2.3.0'
|
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||||
implementation 'io.coil-kt:coil-svg:2.3.0'
|
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:9b1d20be67'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
|
|
||||||
implementation 'ch.acra:acra-http:5.9.7'
|
implementation 'ch.acra:acra-http:5.11.1'
|
||||||
implementation 'ch.acra:acra-dialog:5.9.7'
|
implementation 'ch.acra:acra-dialog:5.11.1'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.json:json:20230227'
|
testImplementation 'org.json:json:20230618'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||||
|
|
||||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
|
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.room:room-testing:2.5.1'
|
androidTestImplementation 'androidx.room:room-testing:2.5.2'
|
||||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||||
|
|
||||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
|
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.47'
|
||||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1'
|
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.47'
|
||||||
}
|
}
|
||||||
|
|||||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@@ -8,7 +8,7 @@
|
|||||||
public static void checkParameterIsNotNull(...);
|
public static void checkParameterIsNotNull(...);
|
||||||
public static void checkNotNullParameter(...);
|
public static void checkNotNullParameter(...);
|
||||||
}
|
}
|
||||||
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
|
-keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment
|
||||||
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
||||||
-dontwarn okhttp3.internal.platform.**
|
-dontwarn okhttp3.internal.platform.**
|
||||||
-dontwarn org.conscrypt.**
|
-dontwarn org.conscrypt.**
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.runner.AndroidJUnitRunner
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
|
||||||
|
class HiltTestRunner : AndroidJUnitRunner() {
|
||||||
|
|
||||||
|
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
|
||||||
|
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ class MangaDatabaseTest {
|
|||||||
MangaDatabase::class.java,
|
MangaDatabase::class.java,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val migrations = databaseMigrations
|
private val migrations = getDatabaseMigrations(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun versions() {
|
fun versions() {
|
||||||
@@ -8,7 +8,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
@@ -19,11 +18,12 @@ import org.junit.runner.RunWith
|
|||||||
import org.koitharu.kotatsu.SampleData
|
import org.koitharu.kotatsu.SampleData
|
||||||
import org.koitharu.kotatsu.awaitForIdle
|
import org.koitharu.kotatsu.awaitForIdle
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidTest
|
@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
|
||||||
@@ -48,6 +48,7 @@ class ShortcutsUpdaterTest {
|
|||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
|
||||||
return@runTest
|
return@runTest
|
||||||
}
|
}
|
||||||
|
database.invalidationTracker.addObserver(appShortcutManager)
|
||||||
awaitUpdate()
|
awaitUpdate()
|
||||||
assertTrue(getShortcuts().isEmpty())
|
assertTrue(getShortcuts().isEmpty())
|
||||||
historyRepository.addOrUpdate(
|
historyRepository.addOrUpdate(
|
||||||
@@ -72,6 +73,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.backup.BackupRepository
|
|||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.tracker.domain
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import javax.inject.Inject
|
|
||||||
import junit.framework.TestCase.*
|
import junit.framework.TestCase.*
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -11,8 +10,9 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koitharu.kotatsu.SampleData
|
import org.koitharu.kotatsu.SampleData
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
|
||||||
|
|
||||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
|
||||||
45
app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt
Normal file
45
app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package org.koitharu.kotatsu
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.StrictMode
|
||||||
|
import androidx.fragment.app.strictmode.FragmentStrictMode
|
||||||
|
import org.koitharu.kotatsu.core.BaseApp
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
|
|
||||||
|
class KotatsuApp : BaseApp() {
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
enableStrictMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableStrictMode() {
|
||||||
|
StrictMode.setThreadPolicy(
|
||||||
|
StrictMode.ThreadPolicy.Builder()
|
||||||
|
.detectAll()
|
||||||
|
.penaltyLog()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
StrictMode.setVmPolicy(
|
||||||
|
StrictMode.VmPolicy.Builder()
|
||||||
|
.detectAll()
|
||||||
|
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
|
||||||
|
.setClassInstanceLimit(PagesCache::class.java, 1)
|
||||||
|
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
|
||||||
|
.setClassInstanceLimit(PageLoader::class.java, 1)
|
||||||
|
.penaltyLog()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
|
||||||
|
.penaltyDeath()
|
||||||
|
.detectFragmentReuse()
|
||||||
|
// .detectWrongFragmentContainer() FIXME: migrate to ViewPager2
|
||||||
|
.detectRetainInstanceUsage()
|
||||||
|
.detectSetUserVisibleHint()
|
||||||
|
.detectFragmentTagUsage()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import java.util.EnumSet
|
|||||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
get() = ConfigKey.Domain()
|
get() = ConfigKey.Domain("")
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
override val sortOrders: Set<SortOrder>
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
get() = EnumSet.allOf(SortOrder::class.java)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||||
@@ -18,6 +18,23 @@
|
|||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.speech.action.RECOGNIZE_SPEECH" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="org.koitharu.kotatsu.KotatsuApp"
|
android:name="org.koitharu.kotatsu.KotatsuApp"
|
||||||
@@ -30,8 +47,8 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:localeConfig="@xml/locales"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Kotatsu"
|
android:theme="@style/Theme.Kotatsu"
|
||||||
@@ -54,6 +71,17 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="kotatsu.app" />
|
||||||
|
<data android:path="/manga" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
||||||
@@ -83,6 +111,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
|
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
|
||||||
android:label="@string/suggestions" />
|
android:label="@string/suggestions" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity"
|
||||||
|
android:label="@string/related_manga" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -98,18 +129,24 @@
|
|||||||
<data android:host="sync-settings" />
|
<data android:host="sync-settings" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity"
|
||||||
|
android:label="@string/local_manga_directories" />
|
||||||
<activity
|
<activity
|
||||||
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"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
|
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
|
||||||
android:label="@string/favourites"
|
android:label="@string/manage_categories" />
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -140,9 +177,6 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
|
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
|
||||||
android:label="@string/color_correction" />
|
android:label="@string/color_correction" />
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
|
|
||||||
android:label="@string/settings" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity"
|
android:name="org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -163,7 +197,13 @@
|
|||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
tools:node="merge" />
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
@@ -184,8 +224,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncService"
|
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/favourites"
|
android:label="@string/favourites">
|
||||||
android:process=":sync">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.SyncAdapter" />
|
<action android:name="android.content.SyncAdapter" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -196,8 +235,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncService"
|
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/history"
|
android:label="@string/history">
|
||||||
android:process=":sync">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.SyncAdapter" />
|
<action android:name="android.content.SyncAdapter" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -278,6 +316,660 @@
|
|||||||
android:name="com.samsung.android.icon_container.has_icon_container"
|
android:name="com.samsung.android.icon_container.has_icon_container"
|
||||||
android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
|
android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name="org.koitharu.kotatsu.details.ui.DetailsBYLinkActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:targetActivity="org.koitharu.kotatsu.details.ui.DetailsActivity">
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="false">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="1stkissmanga.me" />
|
||||||
|
<data android:host="3asq.org" />
|
||||||
|
<data android:host="18porncomic.com" />
|
||||||
|
<data android:host="212.32.226.234" />
|
||||||
|
<data android:host="247manga.com" />
|
||||||
|
<data android:host="365manga.com" />
|
||||||
|
<data android:host="2023.allhen.online" />
|
||||||
|
<data android:host="adultwebtoon.com" />
|
||||||
|
<data android:host="afroditscans.com" />
|
||||||
|
<data android:host="ainzscans.site" />
|
||||||
|
<data android:host="aiyumanga.com" />
|
||||||
|
<data android:host="alceascan.my.id" />
|
||||||
|
<data android:host="allporncomic.com" />
|
||||||
|
<data android:host="anibel.net" />
|
||||||
|
<data android:host="anigliscans.com" />
|
||||||
|
<data android:host="anikiga.com" />
|
||||||
|
<data android:host="animaregia.net" />
|
||||||
|
<data android:host="anisamanga.com" />
|
||||||
|
<data android:host="anshscans.org" />
|
||||||
|
<data android:host="apenasmaisumyaoi.com" />
|
||||||
|
<data android:host="apollcomics.com" />
|
||||||
|
<data android:host="aquamanga.com" />
|
||||||
|
<data android:host="arabtoons.net" />
|
||||||
|
<data android:host="araznovel.com" />
|
||||||
|
<data android:host="arcanescans.com" />
|
||||||
|
<data android:host="arenascans.net" />
|
||||||
|
<data android:host="arthurscan.xyz" />
|
||||||
|
<data android:host="astral-manga.fr" />
|
||||||
|
<data android:host="astrallibrary.net" />
|
||||||
|
<data android:host="astrumscans.xyz" />
|
||||||
|
<data android:host="asura.nacm.xyz" />
|
||||||
|
<data android:host="asurascanstr.com" />
|
||||||
|
<data android:host="athenafansub.com" />
|
||||||
|
<data android:host="ayatoon.com" />
|
||||||
|
<data android:host="azoranov.com" />
|
||||||
|
<data android:host="azuremanga.com" />
|
||||||
|
<data android:host="babeltoon.com" />
|
||||||
|
<data android:host="bakai.org" />
|
||||||
|
<data android:host="bakaman.net" />
|
||||||
|
<data android:host="bakamh.com" />
|
||||||
|
<data android:host="banana-scan.com" />
|
||||||
|
<data android:host="bato.to" />
|
||||||
|
<data android:host="batocomic.com" />
|
||||||
|
<data android:host="batocomic.net" />
|
||||||
|
<data android:host="batocomic.org" />
|
||||||
|
<data android:host="batotoo.com" />
|
||||||
|
<data android:host="batotwo.com" />
|
||||||
|
<data android:host="battwo.com" />
|
||||||
|
<data android:host="beast-scans.com" />
|
||||||
|
<data android:host="beehentai.com" />
|
||||||
|
<data android:host="bentomanga.com" />
|
||||||
|
<data android:host="bestmanga.club" />
|
||||||
|
<data android:host="bestmanhua.com" />
|
||||||
|
<data android:host="bibimanga.com" />
|
||||||
|
<data android:host="birdmanga.com" />
|
||||||
|
<data android:host="birdtoon.net" />
|
||||||
|
<data android:host="blogmanga.net" />
|
||||||
|
<data android:host="blogtruyenmoi.com" />
|
||||||
|
<data android:host="bokugents.com" />
|
||||||
|
<data android:host="boosei.net" />
|
||||||
|
<data android:host="boyslove.me" />
|
||||||
|
<data android:host="br.atlantisscan.com" />
|
||||||
|
<data android:host="br.ninemanga.com" />
|
||||||
|
<data android:host="cabaredowatame.site" />
|
||||||
|
<data android:host="cafecomyaoi.com.br" />
|
||||||
|
<data android:host="carteldemanhwas.com" />
|
||||||
|
<data android:host="cat300.com" />
|
||||||
|
<data android:host="cerisescans.com" />
|
||||||
|
<data android:host="chap.mangairo.com" />
|
||||||
|
<data android:host="chapmanganato.com" />
|
||||||
|
<data android:host="chapmanganato.com" />
|
||||||
|
<data android:host="cizgiromanarsivi.com" />
|
||||||
|
<data android:host="cmreader.info" />
|
||||||
|
<data android:host="cocorip.net" />
|
||||||
|
<data android:host="coffeemanga.io" />
|
||||||
|
<data android:host="coloredmanga.com" />
|
||||||
|
<data android:host="comick.app" />
|
||||||
|
<data android:host="comiko.net" />
|
||||||
|
<data android:host="comiko.org" />
|
||||||
|
<data android:host="copypastescan.xyz" />
|
||||||
|
<data android:host="cosmicscans.com" />
|
||||||
|
<data android:host="daprob.com" />
|
||||||
|
<data android:host="darkscans.com" />
|
||||||
|
<data android:host="de.ninemanga.com" />
|
||||||
|
<data android:host="desu.me" />
|
||||||
|
<data android:host="diamondfansub.com" />
|
||||||
|
<data android:host="dojing.net" />
|
||||||
|
<data android:host="dokkomanga.com" />
|
||||||
|
<data android:host="dokkomanga.com" />
|
||||||
|
<data android:host="doujin69.com" />
|
||||||
|
<data android:host="doujindesu.rip" />
|
||||||
|
<data android:host="doujinhentai.net" />
|
||||||
|
<data android:host="dragontea.ink" />
|
||||||
|
<data android:host="dragontranslation.net" />
|
||||||
|
<data android:host="drakescans.com" />
|
||||||
|
<data android:host="dto.to" />
|
||||||
|
<data android:host="duckmanga.com" />
|
||||||
|
<data android:host="duniakomik.id" />
|
||||||
|
<data android:host="dynasty-scans.com" />
|
||||||
|
<data android:host="e-hentai.org" />
|
||||||
|
<data android:host="elarcpage.com" />
|
||||||
|
<data android:host="en.leviatanscans.com" />
|
||||||
|
<data android:host="epsilonscan.fr" />
|
||||||
|
<data android:host="es.ninemanga.com" />
|
||||||
|
<data android:host="esomanga.com" />
|
||||||
|
<data android:host="exhentai.org" />
|
||||||
|
<data android:host="falconmanga.com" />
|
||||||
|
<data android:host="fbsquads.com" />
|
||||||
|
<data android:host="finalscans.com" />
|
||||||
|
<data android:host="flamescans.org" />
|
||||||
|
<data android:host="foxwhite.com.br" />
|
||||||
|
<data android:host="fr-scan.cc" />
|
||||||
|
<data android:host="fr.ninemanga.com" />
|
||||||
|
<data android:host="franxxmangas.net" />
|
||||||
|
<data android:host="freakscans.com" />
|
||||||
|
<data android:host="freemanga.me" />
|
||||||
|
<data android:host="freemangatop.com" />
|
||||||
|
<data android:host="freewebtooncoins.com" />
|
||||||
|
<data android:host="frscans.com" />
|
||||||
|
<data android:host="furyosociety.com" />
|
||||||
|
<data android:host="galaxymanga.org" />
|
||||||
|
<data android:host="gatemanga.com" />
|
||||||
|
<data android:host="gdscans.com" />
|
||||||
|
<data android:host="gekkou.com.br" />
|
||||||
|
<data android:host="glorymanga.com" />
|
||||||
|
<data android:host="goldenmanga.top" />
|
||||||
|
<data android:host="golgebahcesi.com" />
|
||||||
|
<data android:host="gooffansub.com" />
|
||||||
|
<data android:host="gourmetscans.net" />
|
||||||
|
<data android:host="grabber.zone" />
|
||||||
|
<data android:host="gremorymangas.com" />
|
||||||
|
<data android:host="guimah.com" />
|
||||||
|
<data android:host="guncelmanga.net" />
|
||||||
|
<data android:host="h.mangabat.com" />
|
||||||
|
<data android:host="hachiraw.com" />
|
||||||
|
<data android:host="harimanga.com" />
|
||||||
|
<data android:host="hayalistic.com" />
|
||||||
|
<data android:host="hensekai.com" />
|
||||||
|
<data android:host="hentai3z.cc" />
|
||||||
|
<data android:host="hentai3z.xyz" />
|
||||||
|
<data android:host="hentai4free.net" />
|
||||||
|
<data android:host="hentai20.io" />
|
||||||
|
<data android:host="hentai.gekkouscans.com.br" />
|
||||||
|
<data android:host="hentai.scantrad-vf.cc" />
|
||||||
|
<data android:host="hentaichan.live" />
|
||||||
|
<data android:host="hentaichan.pro" />
|
||||||
|
<data android:host="hentaicube.net" />
|
||||||
|
<data android:host="hentailib.me" />
|
||||||
|
<data android:host="hentaimanga.me" />
|
||||||
|
<data android:host="hentaiteca.net" />
|
||||||
|
<data android:host="hentaivn.autos" />
|
||||||
|
<data android:host="hentaivn.tv" />
|
||||||
|
<data android:host="hentaiwebtoon.com" />
|
||||||
|
<data android:host="hentaixcomic.com" />
|
||||||
|
<data android:host="hentaixdickgirl.com" />
|
||||||
|
<data android:host="hentaixyuri.com" />
|
||||||
|
<data android:host="hentaizone.xyz" />
|
||||||
|
<data android:host="herenscan.com" />
|
||||||
|
<data android:host="hhentai.fr" />
|
||||||
|
<data android:host="hikariscan.com.br" />
|
||||||
|
<data android:host="hipercool.xyz" />
|
||||||
|
<data android:host="hmanhwa.com" />
|
||||||
|
<data android:host="hni-scantrad.com" />
|
||||||
|
<data android:host="honey-manga.com.ua" />
|
||||||
|
<data android:host="hscans.com" />
|
||||||
|
<data android:host="hto.to" />
|
||||||
|
<data android:host="id.gourmetscans.net" />
|
||||||
|
<data android:host="illusionscan.com" />
|
||||||
|
<data android:host="immortalupdates.com" />
|
||||||
|
<data android:host="immortalupdates.id" />
|
||||||
|
<data android:host="imperiodabritannia.com" />
|
||||||
|
<data android:host="imperioscans.com.br" />
|
||||||
|
<data android:host="indo18h.com" />
|
||||||
|
<data android:host="infrafandub.xyz" />
|
||||||
|
<data android:host="isekaiscan.top" />
|
||||||
|
<data android:host="it.ninemanga.com" />
|
||||||
|
<data android:host="itsyourightmanhua.com" />
|
||||||
|
<data android:host="jaiminisbox.net" />
|
||||||
|
<data android:host="japscan.ws" />
|
||||||
|
<data android:host="jiangzaitoon.co" />
|
||||||
|
<data android:host="jimanga.com" />
|
||||||
|
<data android:host="jpmangas.xyz" />
|
||||||
|
<data android:host="kanzenin.xyz" />
|
||||||
|
<data android:host="karatcam-scans.fr" />
|
||||||
|
<data android:host="katakomik.online" />
|
||||||
|
<data android:host="kiryuu.id" />
|
||||||
|
<data android:host="kissmanga.in" />
|
||||||
|
<data android:host="klikmanga.id" />
|
||||||
|
<data android:host="klz9.com" />
|
||||||
|
<data android:host="koinoboriscan.com" />
|
||||||
|
<data android:host="kolmanga.com" />
|
||||||
|
<data android:host="komikav.com" />
|
||||||
|
<data android:host="komikcast.io" />
|
||||||
|
<data android:host="komikdewasa.cfd" />
|
||||||
|
<data android:host="komikgo.org" />
|
||||||
|
<data android:host="komikhentai.co" />
|
||||||
|
<data android:host="komikid.com" />
|
||||||
|
<data android:host="komikindo.co" />
|
||||||
|
<data android:host="komikindo.info" />
|
||||||
|
<data android:host="komiklab.com" />
|
||||||
|
<data android:host="komiklokal.cfd" />
|
||||||
|
<data android:host="komikmama.co" />
|
||||||
|
<data android:host="komikmanhwa.me" />
|
||||||
|
<data android:host="komikmirror.art" />
|
||||||
|
<data android:host="komiksan.link" />
|
||||||
|
<data android:host="komiksay.site" />
|
||||||
|
<data android:host="komikstation.co" />
|
||||||
|
<data android:host="komiktap.in" />
|
||||||
|
<data android:host="komiku.com" />
|
||||||
|
<data android:host="komikzoid.xyz" />
|
||||||
|
<data android:host="ksgroupscans.com" />
|
||||||
|
<data android:host="kumascans.com" />
|
||||||
|
<data android:host="kunmanga.com" />
|
||||||
|
<data android:host="ladymanga.com" />
|
||||||
|
<data android:host="lectortmo.com" />
|
||||||
|
<data android:host="lectorunitoon.com" />
|
||||||
|
<data android:host="legacy-scans.com" />
|
||||||
|
<data android:host="legionscans.com" />
|
||||||
|
<data android:host="leitor.kamisama.com.br" />
|
||||||
|
<data android:host="leitorizakaya.net" />
|
||||||
|
<data android:host="lelscanvf.cc" />
|
||||||
|
<data android:host="leryaoi.com" />
|
||||||
|
<data android:host="lilymanga.net" />
|
||||||
|
<data android:host="limascans.xyz/v2" />
|
||||||
|
<data android:host="lkscanlation.com" />
|
||||||
|
<data android:host="lolicon.mobi" />
|
||||||
|
<data android:host="lugnica-scans.com" />
|
||||||
|
<data android:host="lunarscan.org" />
|
||||||
|
<data android:host="luxmanga.net" />
|
||||||
|
<data android:host="lxmanga.net" />
|
||||||
|
<data android:host="lynxscans.com" />
|
||||||
|
<data android:host="m.isekaiscan.to" />
|
||||||
|
<data android:host="mafia-manga.com" />
|
||||||
|
<data android:host="maidscan.com.br" />
|
||||||
|
<data android:host="manga1st.online" />
|
||||||
|
<data android:host="manga3s.com" />
|
||||||
|
<data android:host="manga18.club" />
|
||||||
|
<data android:host="manga68.com" />
|
||||||
|
<data android:host="manga689.com" />
|
||||||
|
<data android:host="manga-chan.me" />
|
||||||
|
<data android:host="manga-crab.com" />
|
||||||
|
<data android:host="manga-diyari.com" />
|
||||||
|
<data android:host="manga-fast.com" />
|
||||||
|
<data android:host="manga-fr.me" />
|
||||||
|
<data android:host="manga-mate.org" />
|
||||||
|
<data android:host="manga-moons.net" />
|
||||||
|
<data android:host="manga-scan.co" />
|
||||||
|
<data android:host="manga-scantrad.io" />
|
||||||
|
<data android:host="manga-tx.com" />
|
||||||
|
<data android:host="manga-uptocats.com" />
|
||||||
|
<data android:host="manga.clone-army.org" />
|
||||||
|
<data android:host="manga.in.ua" />
|
||||||
|
<data android:host="manga.mundodrama.site" />
|
||||||
|
<data android:host="mangaaction.com" />
|
||||||
|
<data android:host="mangaatrend.net" />
|
||||||
|
<data android:host="mangabaz.net" />
|
||||||
|
<data android:host="mangabob.com" />
|
||||||
|
<data android:host="mangabuddy.com" />
|
||||||
|
<data android:host="mangacim.com" />
|
||||||
|
<data android:host="mangaclash.com" />
|
||||||
|
<data android:host="mangacultivator.com" />
|
||||||
|
<data android:host="mangacute.com" />
|
||||||
|
<data android:host="mangacv.com" />
|
||||||
|
<data android:host="mangadass.com" />
|
||||||
|
<data android:host="mangadeemak.com" />
|
||||||
|
<data android:host="mangadex.org" />
|
||||||
|
<data android:host="mangadistrict.com" />
|
||||||
|
<data android:host="mangadna.com" />
|
||||||
|
<data android:host="mangadoor.com" />
|
||||||
|
<data android:host="mangaeffect.com" />
|
||||||
|
<data android:host="mangaforest.me" />
|
||||||
|
<data android:host="mangaforfree.com" />
|
||||||
|
<data android:host="mangafoxfull.com" />
|
||||||
|
<data android:host="mangafreak.online" />
|
||||||
|
<data android:host="mangagalaxy.me" />
|
||||||
|
<data android:host="mangagg.com" />
|
||||||
|
<data android:host="mangagoyaoi.com" />
|
||||||
|
<data android:host="mangagreat.com" />
|
||||||
|
<data android:host="mangahentai.me" />
|
||||||
|
<data android:host="mangahub.fr" />
|
||||||
|
<data android:host="mangaid.click" />
|
||||||
|
<data android:host="mangaindo.me" />
|
||||||
|
<data android:host="mangak2.com" />
|
||||||
|
<data android:host="mangakakalot.com" />
|
||||||
|
<data android:host="mangakeyfi.net" />
|
||||||
|
<data android:host="mangaking.net" />
|
||||||
|
<data android:host="mangakio.me" />
|
||||||
|
<data android:host="mangakiss.org" />
|
||||||
|
<data android:host="mangakita.net" />
|
||||||
|
<data android:host="mangakomi.io" />
|
||||||
|
<data android:host="mangakyo.org" />
|
||||||
|
<data android:host="mangalek.com" />
|
||||||
|
<data android:host="mangaleks.com" />
|
||||||
|
<data android:host="mangaleveling.com" />
|
||||||
|
<data android:host="mangalib.me" />
|
||||||
|
<data android:host="mangalike.me" />
|
||||||
|
<data android:host="mangalink.online" />
|
||||||
|
<data android:host="mangalionz.com" />
|
||||||
|
<data android:host="mangamammy.ru" />
|
||||||
|
<data android:host="mangamanhua.online" />
|
||||||
|
<data android:host="mangamaniacs.org" />
|
||||||
|
<data android:host="manganato.com" />
|
||||||
|
<data android:host="mangaokutr.com" />
|
||||||
|
<data android:host="mangaonelove.site" />
|
||||||
|
<data android:host="mangaonlineteam.com" />
|
||||||
|
<data android:host="mangaowl.to" />
|
||||||
|
<data android:host="mangaprotm.com" />
|
||||||
|
<data android:host="mangapt.com" />
|
||||||
|
<data android:host="mangapuma.com" />
|
||||||
|
<data android:host="mangaread.co" />
|
||||||
|
<data android:host="mangareaderpro.com" />
|
||||||
|
<data android:host="mangareading.org" />
|
||||||
|
<data android:host="mangarockteam.com" />
|
||||||
|
<data android:host="mangarocky.com" />
|
||||||
|
<data android:host="mangarolls.net" />
|
||||||
|
<data android:host="mangarosie.in" />
|
||||||
|
<data android:host="mangas-origines.fr" />
|
||||||
|
<data android:host="mangas-origines.xyz" />
|
||||||
|
<data android:host="mangaschan.com" />
|
||||||
|
<data android:host="mangasehri.com" />
|
||||||
|
<data android:host="mangaspark.com" />
|
||||||
|
<data android:host="mangastarz.com" />
|
||||||
|
<data android:host="mangastic.cc" />
|
||||||
|
<data android:host="mangastic.cc" />
|
||||||
|
<data android:host="mangasushi.org" />
|
||||||
|
<data android:host="mangasusuku.xyz" />
|
||||||
|
<data android:host="mangatale.co" />
|
||||||
|
<data android:host="mangatone.com" />
|
||||||
|
<data android:host="mangatoto.com" />
|
||||||
|
<data android:host="mangatoto.net" />
|
||||||
|
<data android:host="mangatoto.org" />
|
||||||
|
<data android:host="mangatx.com" />
|
||||||
|
<data android:host="mangaus.xyz" />
|
||||||
|
<data android:host="mangavisa.com" />
|
||||||
|
<data android:host="mangaweebs.in" />
|
||||||
|
<data android:host="mangawt.com" />
|
||||||
|
<data android:host="mangax1.com" />
|
||||||
|
<data android:host="mangaxyz.com" />
|
||||||
|
<data android:host="mangayaro.net" />
|
||||||
|
<data android:host="mangazavr.ru" />
|
||||||
|
<data android:host="mangazodiac.com" />
|
||||||
|
<data android:host="manhatic.com" />
|
||||||
|
<data android:host="manhuaes.com" />
|
||||||
|
<data android:host="manhuafast.com" />
|
||||||
|
<data android:host="manhuafast.net" />
|
||||||
|
<data android:host="manhuaga.com" />
|
||||||
|
<data android:host="manhuahot.com" />
|
||||||
|
<data android:host="manhuamix.com" />
|
||||||
|
<data android:host="manhuaplus.com" />
|
||||||
|
<data android:host="manhuascan.us" />
|
||||||
|
<data android:host="manhuaus.com" />
|
||||||
|
<data android:host="manhuazone.net" />
|
||||||
|
<data android:host="manhwa18.app" />
|
||||||
|
<data android:host="manhwa18.com" />
|
||||||
|
<data android:host="manhwa18.net" />
|
||||||
|
<data android:host="manhwa18.org" />
|
||||||
|
<data android:host="manhwa68.com" />
|
||||||
|
<data android:host="manhwa-latino.com" />
|
||||||
|
<data android:host="manhwaclan.com" />
|
||||||
|
<data android:host="manhwadesu.top" />
|
||||||
|
<data android:host="manhwafull.com" />
|
||||||
|
<data android:host="manhwahentai.me" />
|
||||||
|
<data android:host="manhwaindo.icu" />
|
||||||
|
<data android:host="manhwaindo.id" />
|
||||||
|
<data android:host="manhwakool.com" />
|
||||||
|
<data android:host="manhwalist.xyz" />
|
||||||
|
<data android:host="manhwalover.com" />
|
||||||
|
<data android:host="manhwaplus.pro" />
|
||||||
|
<data android:host="manhwasco.net" />
|
||||||
|
<data android:host="manhwatop.com" />
|
||||||
|
<data android:host="manhwaworld.com" />
|
||||||
|
<data android:host="manhwax.org" />
|
||||||
|
<data android:host="manhwaz.com" />
|
||||||
|
<data android:host="mantrazscan.com" />
|
||||||
|
<data android:host="manwe.pro" />
|
||||||
|
<data android:host="manycomic.com" />
|
||||||
|
<data android:host="manytoon.com" />
|
||||||
|
<data android:host="manytoon.me" />
|
||||||
|
<data android:host="masterkomik.com" />
|
||||||
|
<data android:host="melokomik.xyz" />
|
||||||
|
<data android:host="mgkomik.com" />
|
||||||
|
<data android:host="miauscans.com" />
|
||||||
|
<data android:host="milftoon.xxx" />
|
||||||
|
<data android:host="mintmanga.com" />
|
||||||
|
<data android:host="mintmanga.live" />
|
||||||
|
<data android:host="mirrordesu.ink" />
|
||||||
|
<data android:host="mm-scans.org" />
|
||||||
|
<data android:host="momonohanascan.com" />
|
||||||
|
<data android:host="monarcamanga.com" />
|
||||||
|
<data android:host="moonloversscan.com.br" />
|
||||||
|
<data android:host="moonwitchinlovescan.com" />
|
||||||
|
<data android:host="mortalsgroove.com" />
|
||||||
|
<data android:host="mto.to" />
|
||||||
|
<data android:host="mundomangakun.com.br" />
|
||||||
|
<data android:host="mundomanhwa.com" />
|
||||||
|
<data android:host="murimscan.run" />
|
||||||
|
<data android:host="neatmangas.com" />
|
||||||
|
<data android:host="neoxscans.net" />
|
||||||
|
<data android:host="nettruyenin.com" />
|
||||||
|
<data android:host="nettruyento.com" />
|
||||||
|
<data android:host="neumanga.net" />
|
||||||
|
<data android:host="neumanga.xyz" />
|
||||||
|
<data android:host="nhattruyenmin.com" />
|
||||||
|
<data android:host="nhentai.net" />
|
||||||
|
<data android:host="nicovideo.jp" />
|
||||||
|
<data android:host="nightscans.org" />
|
||||||
|
<data android:host="niji-translations.com" />
|
||||||
|
<data android:host="ninjascan.site" />
|
||||||
|
<data android:host="niverafansub.com" />
|
||||||
|
<data android:host="nocsummer.com.br" />
|
||||||
|
<data android:host="noindexscan.com" />
|
||||||
|
<data android:host="nonbiri.space" />
|
||||||
|
<data android:host="novelcrow.com" />
|
||||||
|
<data android:host="novelmic.com" />
|
||||||
|
<data android:host="novelstown.cyou" />
|
||||||
|
<data android:host="nude-moon.net" />
|
||||||
|
<data android:host="nude-moon.org" />
|
||||||
|
<data android:host="nyxmanga.com" />
|
||||||
|
<data android:host="origami-orpheans.com.br" />
|
||||||
|
<data android:host="otsugami.id" />
|
||||||
|
<data android:host="oxapk.com" />
|
||||||
|
<data android:host="ozulmanga.com" />
|
||||||
|
<data android:host="painfulnightz.com" />
|
||||||
|
<data android:host="pantheon-scan.com" />
|
||||||
|
<data android:host="papscan.com" />
|
||||||
|
<data android:host="paragonscans.com" />
|
||||||
|
<data android:host="peacescans.com" />
|
||||||
|
<data android:host="phantomscans.com" />
|
||||||
|
<data android:host="phenixscans.fr" />
|
||||||
|
<data android:host="pianmanga.me" />
|
||||||
|
<data android:host="pirulitorosa.site" />
|
||||||
|
<data android:host="piscans.in" />
|
||||||
|
<data android:host="platinumscans.com" />
|
||||||
|
<data android:host="pojokmanga.net" />
|
||||||
|
<data android:host="popsmanga.com" />
|
||||||
|
<data android:host="portalyaoi.com" />
|
||||||
|
<data android:host="prismahentai.com" />
|
||||||
|
<data android:host="prismascans.net" />
|
||||||
|
<data android:host="projetoscanlator.com" />
|
||||||
|
<data android:host="psunicorn.com" />
|
||||||
|
<data android:host="queenscans.com" />
|
||||||
|
<data android:host="ragnarokscan.com" />
|
||||||
|
<data android:host="ragnarokscanlation.com" />
|
||||||
|
<data android:host="raijinscans.fr" />
|
||||||
|
<data android:host="raikiscan.com" />
|
||||||
|
<data android:host="rainbowfairyscan.com" />
|
||||||
|
<data android:host="randomscans.com" />
|
||||||
|
<data android:host="ravenscans.com" />
|
||||||
|
<data android:host="rawdex.net" />
|
||||||
|
<data android:host="rawkuma.com" />
|
||||||
|
<data android:host="read-nifteam.info" />
|
||||||
|
<data android:host="read.babelwuxia.com" />
|
||||||
|
<data android:host="readcomicsonline.ru" />
|
||||||
|
<data android:host="reader.deathtollscans.net" />
|
||||||
|
<data android:host="reader.decadencescans.com" />
|
||||||
|
<data android:host="reader.evilflowers.com" />
|
||||||
|
<data android:host="reader.mangatellers.gr" />
|
||||||
|
<data android:host="reader.onepiecenakama.pl" />
|
||||||
|
<data android:host="reader.powermanga.org" />
|
||||||
|
<data android:host="reader.silentsky-scans.net" />
|
||||||
|
<data android:host="readfreecomics.com" />
|
||||||
|
<data android:host="readkomik.com" />
|
||||||
|
<data android:host="readmanga.io" />
|
||||||
|
<data android:host="readmanga.live" />
|
||||||
|
<data android:host="readmanga.me" />
|
||||||
|
<data android:host="readmangabat.com" />
|
||||||
|
<data android:host="readmanhua.net" />
|
||||||
|
<data android:host="readtoto.com" />
|
||||||
|
<data android:host="readtoto.net" />
|
||||||
|
<data android:host="readtoto.org" />
|
||||||
|
<data android:host="realmscans.xyz" />
|
||||||
|
<data android:host="reaperscans.fr" />
|
||||||
|
<data android:host="remanga.org" />
|
||||||
|
<data android:host="rightdark-scan.com" />
|
||||||
|
<data android:host="rio2manga.com" />
|
||||||
|
<data android:host="rio2manga.net" />
|
||||||
|
<data android:host="rogmangas.com" />
|
||||||
|
<data android:host="romantikmanga.com" />
|
||||||
|
<data android:host="ru.ninemanga.com" />
|
||||||
|
<data android:host="s2manga.com" />
|
||||||
|
<data android:host="samuraiscan.com" />
|
||||||
|
<data android:host="sawamics.com" />
|
||||||
|
<data android:host="saytruyenhay.com" />
|
||||||
|
<data android:host="scambertraslator.com" />
|
||||||
|
<data android:host="scan.hentai.menu" />
|
||||||
|
<data android:host="scanmanga-vf.ws" />
|
||||||
|
<data android:host="scansmangas.me" />
|
||||||
|
<data android:host="scansraw.com" />
|
||||||
|
<data android:host="scantrad-union.com" />
|
||||||
|
<data android:host="scantrad-vf.co" />
|
||||||
|
<data android:host="sekaikomik.pro" />
|
||||||
|
<data android:host="sektedoujin.cc" />
|
||||||
|
<data android:host="sektekomik.xyz" />
|
||||||
|
<data android:host="selfmanga.live" />
|
||||||
|
<data android:host="senpaiediciones.com" />
|
||||||
|
<data android:host="shadowmangas.com" />
|
||||||
|
<data android:host="shadowtrad.net" />
|
||||||
|
<data android:host="sheakomik.com" />
|
||||||
|
<data android:host="shibamanga.com" />
|
||||||
|
<data android:host="shinigami.id" />
|
||||||
|
<data android:host="shirodoujin.com" />
|
||||||
|
<data android:host="shootingstarscans.com" />
|
||||||
|
<data android:host="silencescan.com.br" />
|
||||||
|
<data android:host="sinensisscans.com" />
|
||||||
|
<data android:host="skanlacje-feniksy.pl" />
|
||||||
|
<data android:host="skymanga.work" />
|
||||||
|
<data android:host="skymangas.com" />
|
||||||
|
<data android:host="sleepytranslations.com" />
|
||||||
|
<data android:host="soulscans.my.id" />
|
||||||
|
<data android:host="spartanmanga.com.tr" />
|
||||||
|
<data android:host="sssscanlator.com" />
|
||||||
|
<data android:host="summanga.com" />
|
||||||
|
<data android:host="suryascans.com" />
|
||||||
|
<data android:host="sushiscan.fr" />
|
||||||
|
<data android:host="sushiscan.net" />
|
||||||
|
<data android:host="swatop.club" />
|
||||||
|
<data android:host="tankouhentai.com" />
|
||||||
|
<data android:host="tatakaescan.com" />
|
||||||
|
<data android:host="tecnoscann.com" />
|
||||||
|
<data android:host="teenmanhua.com" />
|
||||||
|
<data android:host="tempestfansub.com" />
|
||||||
|
<data android:host="templescan.net" />
|
||||||
|
<data android:host="templescanesp.com" />
|
||||||
|
<data android:host="tenkaiscan.net" />
|
||||||
|
<data android:host="theguildscans.com" />
|
||||||
|
<data android:host="thesugarscan.com" />
|
||||||
|
<data android:host="timenaight.com" />
|
||||||
|
<data android:host="todaymic.com" />
|
||||||
|
<data android:host="tonizutoon.com" />
|
||||||
|
<data android:host="toonchill.com" />
|
||||||
|
<data android:host="toonfr.com" />
|
||||||
|
<data android:host="toonhunter.com" />
|
||||||
|
<data android:host="toonily.com" />
|
||||||
|
<data android:host="toonily.me" />
|
||||||
|
<data android:host="toonily.net" />
|
||||||
|
<data android:host="toonitube.com" />
|
||||||
|
<data android:host="tortuga-ceviri.com" />
|
||||||
|
<data android:host="traduccionesmoonlight.com" />
|
||||||
|
<data android:host="treemanga.com" />
|
||||||
|
<data android:host="tritinia.org" />
|
||||||
|
<data android:host="truemanga.com" />
|
||||||
|
<data android:host="truyentranhlh.net" />
|
||||||
|
<data android:host="tsundoku.com.br" />
|
||||||
|
<data android:host="tukangkomik.id" />
|
||||||
|
<data android:host="tumanhwas.club" />
|
||||||
|
<data android:host="turktoon.com" />
|
||||||
|
<data android:host="v2.comiz.net" />
|
||||||
|
<data android:host="valkyriescan.com" />
|
||||||
|
<data android:host="vercomicsporno.com" />
|
||||||
|
<data android:host="vermangasporno.com" />
|
||||||
|
<data android:host="vermanhwa.es" />
|
||||||
|
<data android:host="viyafansub.com" />
|
||||||
|
<data android:host="void-scans.com" />
|
||||||
|
<data android:host="w.mangairo.com" />
|
||||||
|
<data android:host="wakamics.net" />
|
||||||
|
<data android:host="webcomic.me" />
|
||||||
|
<data android:host="webtoon-tr.com" />
|
||||||
|
<data android:host="webtoon.uk" />
|
||||||
|
<data android:host="webtoonempire.org" />
|
||||||
|
<data android:host="webtoonhatti.com" />
|
||||||
|
<data android:host="webtoons.top" />
|
||||||
|
<data android:host="webtoonscan.com" />
|
||||||
|
<data android:host="weloma.art" />
|
||||||
|
<data android:host="welovemanga.one" />
|
||||||
|
<data android:host="westmanga.info" />
|
||||||
|
<data android:host="wickedwitchscan.com" />
|
||||||
|
<data android:host="winterscan.com" />
|
||||||
|
<data android:host="wonderlandscan.com" />
|
||||||
|
<data android:host="woopread.com" />
|
||||||
|
<data android:host="worldmanhwas.bar" />
|
||||||
|
<data android:host="wto.to" />
|
||||||
|
<data android:host="www1.bluesolo.org" />
|
||||||
|
<data android:host="www.areascans.net" />
|
||||||
|
<data android:host="www.bentomanga.com" />
|
||||||
|
<data android:host="www.eromiau.com" />
|
||||||
|
<data android:host="www.inu-manga.com" />
|
||||||
|
<data android:host="www.japscan.lol" />
|
||||||
|
<data android:host="www.kuroimanga.com" />
|
||||||
|
<data android:host="www.lami-manga.com" />
|
||||||
|
<data android:host="www.lelmanga.com" />
|
||||||
|
<data android:host="www.lianscans.my.id" />
|
||||||
|
<data android:host="www.maid.my.id" />
|
||||||
|
<data android:host="www.majorscans.com" />
|
||||||
|
<data android:host="www.mangadods.com" />
|
||||||
|
<data android:host="www.mangaread.org" />
|
||||||
|
<data android:host="www.mangascantrad.fr" />
|
||||||
|
<data android:host="www.mangatown.com" />
|
||||||
|
<data android:host="www.manhuabug.com" />
|
||||||
|
<data android:host="www.manhuakey.com" />
|
||||||
|
<data android:host="www.manhuasy.com" />
|
||||||
|
<data android:host="www.menudo-fansub.com" />
|
||||||
|
<data android:host="www.nettruyenmax.com" />
|
||||||
|
<data android:host="www.nettruyento.com" />
|
||||||
|
<data android:host="www.nightcomic.com" />
|
||||||
|
<data android:host="www.ninemanga.com" />
|
||||||
|
<data android:host="www.noblessetranslations.com" />
|
||||||
|
<data android:host="www.pantheon-scan.fr" />
|
||||||
|
<data android:host="www.paritehaber.com" />
|
||||||
|
<data android:host="www.peachscan.com" />
|
||||||
|
<data android:host="www.petrotechsociety.org" />
|
||||||
|
<data android:host="www.petrotechsociety.org" />
|
||||||
|
<data android:host="www.ramareader.it" />
|
||||||
|
<data android:host="www.rh2plusmanga.com" />
|
||||||
|
<data android:host="www.ruyamanga.com" />
|
||||||
|
<data android:host="www.scan-fr.org" />
|
||||||
|
<data android:host="www.scan-vf.net" />
|
||||||
|
<data android:host="www.thaimanga.net" />
|
||||||
|
<data android:host="www.topmanhua.com" />
|
||||||
|
<data android:host="www.vfscan.com" />
|
||||||
|
<data android:host="www.walpurgiscan.it" />
|
||||||
|
<data android:host="www.webtoon.xyz" />
|
||||||
|
<data android:host="www.witcomics.net" />
|
||||||
|
<data android:host="www.xn--l3c0azab5a2gta.com" />
|
||||||
|
<data android:host="www.yaoitoshokan.net" />
|
||||||
|
<data android:host="xbato.com" />
|
||||||
|
<data android:host="xbato.net" />
|
||||||
|
<data android:host="xbato.org" />
|
||||||
|
<data android:host="xoxocomics.net" />
|
||||||
|
<data android:host="xx.hentaichan.live" />
|
||||||
|
<data android:host="xxx.hentaichan.live" />
|
||||||
|
<data android:host="y.hentaichan.live" />
|
||||||
|
<data android:host="yaoi-chan.me" />
|
||||||
|
<data android:host="yaoi.mobi" />
|
||||||
|
<data android:host="yaoilib.me" />
|
||||||
|
<data android:host="yaoiscan.com" />
|
||||||
|
<data android:host="ycscan.com" />
|
||||||
|
<data android:host="yugenmangas.com.br" />
|
||||||
|
<data android:host="yuri.live" />
|
||||||
|
<data android:host="zahard.xyz" />
|
||||||
|
<data android:host="zandynofansub.aishiteru.org" />
|
||||||
|
<data android:host="zbato.com" />
|
||||||
|
<data android:host="zbato.net" />
|
||||||
|
<data android:host="zbato.org" />
|
||||||
|
<data android:host="zeroscan.com.br" />
|
||||||
|
<data android:host="zinmanga.com" />
|
||||||
|
<data android:host="zinmanhwa.com" />
|
||||||
|
<data android:host="zuttomanga.com" />
|
||||||
|
<data android:host="реманга.орг" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams
|
|
||||||
import androidx.activity.OnBackPressedDispatcher
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog
|
|
||||||
import org.koitharu.kotatsu.utils.ext.findActivity
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplaySize
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
|
|
||||||
|
|
||||||
private var viewBinding: B? = null
|
|
||||||
|
|
||||||
protected val binding: B
|
|
||||||
get() = checkNotNull(viewBinding)
|
|
||||||
|
|
||||||
protected val behavior: BottomSheetBehavior<*>?
|
|
||||||
get() = (dialog as? BottomSheetDialog)?.behavior
|
|
||||||
|
|
||||||
val isExpanded: Boolean
|
|
||||||
get() = behavior?.state == BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
|
|
||||||
val onBackPressedDispatcher: OnBackPressedDispatcher
|
|
||||||
get() = (requireDialog() as AppBottomSheetDialog).onBackPressedDispatcher
|
|
||||||
|
|
||||||
final override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?,
|
|
||||||
): View {
|
|
||||||
val binding = onInflateView(inflater, container)
|
|
||||||
viewBinding = binding
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
// Enforce max width for tablets
|
|
||||||
val width = resources.getDimensionPixelSize(R.dimen.bottom_sheet_width)
|
|
||||||
if (width > 0) {
|
|
||||||
behavior?.maxWidth = width
|
|
||||||
}
|
|
||||||
// Set peek height to 40% display height
|
|
||||||
binding.root.context.findActivity()?.getDisplaySize()?.let {
|
|
||||||
behavior?.peekHeight = (it.height() * 0.4).toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
viewBinding = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
return AppBottomSheetDialog(requireContext(), theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addBottomSheetCallback(callback: BottomSheetBehavior.BottomSheetCallback) {
|
|
||||||
val b = behavior ?: return
|
|
||||||
b.addBottomSheetCallback(callback)
|
|
||||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
|
||||||
if (rootView != null) {
|
|
||||||
callback.onStateChanged(rootView, b.state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
|
||||||
|
|
||||||
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
|
|
||||||
val b = behavior ?: return
|
|
||||||
if (isExpanded) {
|
|
||||||
b.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
}
|
|
||||||
b.isFitToContents = !isExpanded
|
|
||||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
|
||||||
rootView?.updateLayoutParams {
|
|
||||||
height = if (isExpanded) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
|
|
||||||
}
|
|
||||||
b.isDraggable = !isLocked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
abstract class BaseFragment<B : ViewBinding> :
|
|
||||||
Fragment(),
|
|
||||||
WindowInsetsDelegate.WindowInsetsListener {
|
|
||||||
|
|
||||||
private var viewBinding: B? = null
|
|
||||||
|
|
||||||
protected val binding: B
|
|
||||||
get() = checkNotNull(viewBinding)
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
protected val exceptionResolver = ExceptionResolver(this)
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
|
||||||
|
|
||||||
protected val actionModeDelegate: ActionModeDelegate
|
|
||||||
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val binding = onInflateView(inflater, container)
|
|
||||||
viewBinding = binding
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
insetsDelegate.onViewCreated(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
viewBinding = null
|
|
||||||
insetsDelegate.onDestroyView()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun bindingOrNull() = viewBinding
|
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
|
||||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
|
|
||||||
abstract class BaseFullscreenActivity<B : ViewBinding> :
|
|
||||||
BaseActivity<B>(),
|
|
||||||
View.OnSystemUiVisibilityChangeListener {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
with(window) {
|
|
||||||
statusBarColor = Color.TRANSPARENT
|
|
||||||
navigationBarColor = Color.TRANSPARENT
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
attributes.layoutInDisplayCutoutMode =
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
}
|
|
||||||
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
|
|
||||||
}
|
|
||||||
showSystemUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
final override fun onSystemUiVisibilityChange(visibility: Int) {
|
|
||||||
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO WindowInsetsControllerCompat works incorrect
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
protected fun hideSystemUI() {
|
|
||||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
protected fun showSystemUI() {
|
|
||||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleService
|
|
||||||
|
|
||||||
abstract class BaseService : LifecycleService()
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.CoroutineStart
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
abstract class BaseViewModel : ViewModel() {
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
protected val loadingCounter = CountedBooleanLiveData()
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
protected val errorEvent = SingleLiveEvent<Throwable>()
|
|
||||||
|
|
||||||
val onError: LiveData<Throwable>
|
|
||||||
get() = errorEvent
|
|
||||||
|
|
||||||
val isLoading: LiveData<Boolean>
|
|
||||||
get() = loadingCounter
|
|
||||||
|
|
||||||
protected fun launchJob(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
|
||||||
block: suspend CoroutineScope.() -> Unit
|
|
||||||
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
|
|
||||||
|
|
||||||
protected fun launchLoadingJob(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
|
||||||
block: suspend CoroutineScope.() -> Unit
|
|
||||||
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
|
|
||||||
loadingCounter.increment()
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} finally {
|
|
||||||
loadingCounter.decrement()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
|
||||||
throwable.printStackTraceDebug()
|
|
||||||
if (throwable !is CancellationException) {
|
|
||||||
errorEvent.postCall(throwable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.dialog
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.View
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
||||||
|
|
||||||
class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://github.com/material-components/material-components-android/issues/2582
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun onAttachedToWindow() {
|
|
||||||
val window = window
|
|
||||||
val initialSystemUiVisibility = window?.decorView?.systemUiVisibility ?: 0
|
|
||||||
super.onAttachedToWindow()
|
|
||||||
if (window != null) {
|
|
||||||
// If the navigation bar is translucent at all, the BottomSheet should be edge to edge
|
|
||||||
val drawEdgeToEdge = edgeToEdgeEnabled && Color.alpha(window.navigationBarColor) < 0xFF
|
|
||||||
if (drawEdgeToEdge) {
|
|
||||||
// Copied from super.onAttachedToWindow:
|
|
||||||
val edgeToEdgeFlags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
// Fix super-class's window flag bug by respecting the initial system UI visibility:
|
|
||||||
window.decorView.systemUiVisibility = edgeToEdgeFlags or initialSystemUiVisibility
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.dialog
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.BaseAdapter
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemStorageBinding
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
|
|
||||||
DialogInterface by delegate {
|
|
||||||
|
|
||||||
fun show() = delegate.show()
|
|
||||||
|
|
||||||
class Builder(context: Context, storageManager: LocalStorageManager, listener: OnStorageSelectListener) {
|
|
||||||
|
|
||||||
private val adapter = VolumesAdapter(storageManager)
|
|
||||||
private val delegate = MaterialAlertDialogBuilder(context)
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (adapter.isEmpty) {
|
|
||||||
delegate.setMessage(R.string.cannot_find_available_storage)
|
|
||||||
} else {
|
|
||||||
val defaultValue = runBlocking {
|
|
||||||
storageManager.getDefaultWriteableDir()
|
|
||||||
}
|
|
||||||
adapter.selectedItemPosition = adapter.volumes.indexOfFirst {
|
|
||||||
it.first.canonicalPath == defaultValue?.canonicalPath
|
|
||||||
}
|
|
||||||
delegate.setAdapter(adapter) { d, i ->
|
|
||||||
listener.onStorageSelected(adapter.getItem(i).first)
|
|
||||||
d.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
|
||||||
delegate.setTitle(titleResId)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTitle(title: CharSequence): Builder {
|
|
||||||
delegate.setTitle(title)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNegativeButton(@StringRes textId: Int): Builder {
|
|
||||||
delegate.setNegativeButton(textId, null)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun create() = StorageSelectDialog(delegate.create())
|
|
||||||
}
|
|
||||||
|
|
||||||
private class VolumesAdapter(storageManager: LocalStorageManager) : BaseAdapter() {
|
|
||||||
|
|
||||||
var selectedItemPosition: Int = -1
|
|
||||||
val volumes = getAvailableVolumes(storageManager)
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val view = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.item_storage, parent, false)
|
|
||||||
val binding = (view.tag as? ItemStorageBinding) ?: ItemStorageBinding.bind(view).also {
|
|
||||||
view.tag = it
|
|
||||||
}
|
|
||||||
val item = volumes[position]
|
|
||||||
binding.imageViewIndicator.isChecked = selectedItemPosition == position
|
|
||||||
binding.textViewTitle.text = item.second
|
|
||||||
binding.textViewSubtitle.text = item.first.path
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Pair<File, String> = volumes[position]
|
|
||||||
|
|
||||||
override fun getItemId(position: Int) = position.toLong()
|
|
||||||
|
|
||||||
override fun getCount() = volumes.size
|
|
||||||
|
|
||||||
override fun hasStableIds() = true
|
|
||||||
|
|
||||||
private fun getAvailableVolumes(storageManager: LocalStorageManager): List<Pair<File, String>> {
|
|
||||||
return runBlocking {
|
|
||||||
storageManager.getWriteableDirs().map {
|
|
||||||
it to storageManager.getStorageDisplayName(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface OnStorageSelectListener {
|
|
||||||
|
|
||||||
fun onStorageSelected(file: File)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class ScrollListenerInvalidationObserver(
|
|
||||||
private val recyclerView: RecyclerView,
|
|
||||||
private val scrollListener: RecyclerView.OnScrollListener,
|
|
||||||
) : RecyclerView.AdapterDataObserver() {
|
|
||||||
|
|
||||||
override fun onChanged() {
|
|
||||||
super.onChanged()
|
|
||||||
invalidateScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
|
||||||
super.onItemRangeInserted(positionStart, itemCount)
|
|
||||||
invalidateScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
|
||||||
super.onItemRangeRemoved(positionStart, itemCount)
|
|
||||||
invalidateScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invalidateScroll() {
|
|
||||||
recyclerView.post {
|
|
||||||
scrollListener.onScrolled(recyclerView, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list.decor
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.content.res.getColorOrThrow
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
@SuppressLint("PrivateResource")
|
|
||||||
abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
private val bounds = Rect()
|
|
||||||
private val thickness: Int
|
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
||||||
|
|
||||||
init {
|
|
||||||
paint.style = Paint.Style.FILL
|
|
||||||
val ta = context.obtainStyledAttributes(
|
|
||||||
null,
|
|
||||||
materialR.styleable.MaterialDivider,
|
|
||||||
materialR.attr.materialDividerStyle,
|
|
||||||
materialR.style.Widget_Material3_MaterialDivider,
|
|
||||||
)
|
|
||||||
paint.color = ta.getColorOrThrow(materialR.styleable.MaterialDivider_dividerColor)
|
|
||||||
thickness = ta.getDimensionPixelSize(
|
|
||||||
materialR.styleable.MaterialDivider_dividerThickness,
|
|
||||||
context.resources.getDimensionPixelSize(materialR.dimen.material_divider_thickness),
|
|
||||||
)
|
|
||||||
ta.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State,
|
|
||||||
) {
|
|
||||||
outRect.set(0, thickness, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement for horizontal lists on demand
|
|
||||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) {
|
|
||||||
if (parent.layoutManager == null || thickness == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
canvas.save()
|
|
||||||
val left: Float
|
|
||||||
val right: Float
|
|
||||||
if (parent.clipToPadding) {
|
|
||||||
left = parent.paddingLeft.toFloat()
|
|
||||||
right = (parent.width - parent.paddingRight).toFloat()
|
|
||||||
canvas.clipRect(
|
|
||||||
left,
|
|
||||||
parent.paddingTop.toFloat(),
|
|
||||||
right,
|
|
||||||
(parent.height - parent.paddingBottom).toFloat()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
left = 0f
|
|
||||||
right = parent.width.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
var previous: RecyclerView.ViewHolder? = null
|
|
||||||
for (child in parent.children) {
|
|
||||||
val holder = parent.getChildViewHolder(child)
|
|
||||||
if (previous != null && shouldDrawDivider(previous, holder)) {
|
|
||||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
|
||||||
val top: Float = bounds.top + child.translationY
|
|
||||||
val bottom: Float = top + thickness
|
|
||||||
canvas.drawRect(left, top, right, bottom, paint)
|
|
||||||
}
|
|
||||||
previous = holder
|
|
||||||
}
|
|
||||||
canvas.restore()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun shouldDrawDivider(
|
|
||||||
above: RecyclerView.ViewHolder,
|
|
||||||
below: RecyclerView.ViewHolder,
|
|
||||||
): Boolean
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list.decor
|
|
||||||
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.util.SparseIntArray
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.util.getOrDefault
|
|
||||||
import androidx.core.util.set
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class TypedSpacingItemDecoration(
|
|
||||||
vararg spacingMapping: Pair<Int, Int>,
|
|
||||||
private val fallbackSpacing: Int = 0,
|
|
||||||
) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
private val mapping = SparseIntArray(spacingMapping.size)
|
|
||||||
|
|
||||||
init {
|
|
||||||
spacingMapping.forEach { (k, v) -> mapping[k] = v }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State
|
|
||||||
) {
|
|
||||||
val itemType = parent.getChildViewHolder(view)?.itemViewType
|
|
||||||
val spacing = if (itemType == null) {
|
|
||||||
fallbackSpacing
|
|
||||||
} else {
|
|
||||||
mapping.getOrDefault(itemType, fallbackSpacing)
|
|
||||||
}
|
|
||||||
outRect.set(spacing, spacing, spacing, spacing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
|
||||||
|
|
||||||
import androidx.annotation.AnyThread
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
class CountedBooleanLiveData : LiveData<Boolean>(false) {
|
|
||||||
|
|
||||||
private val counter = AtomicInteger(0)
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun increment() {
|
|
||||||
if (counter.getAndIncrement() == 0) {
|
|
||||||
postValue(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun decrement() {
|
|
||||||
if (counter.decrementAndGet() == 0) {
|
|
||||||
postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun reset() {
|
|
||||||
if (counter.getAndSet(0) != 0) {
|
|
||||||
postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.animation.LayoutTransition
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.WindowInsets
|
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.annotation.MenuRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import androidx.core.content.withStyledAttributes
|
|
||||||
import androidx.core.view.*
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.databinding.LayoutSheetHeaderBinding
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeDrawable
|
|
||||||
import org.koitharu.kotatsu.utils.ext.parents
|
|
||||||
import java.util.*
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
private const val THROTTLE_DELAY = 200L
|
|
||||||
|
|
||||||
class BottomSheetHeaderBar @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
@AttrRes defStyleAttr: Int = materialR.attr.appBarLayoutStyle,
|
|
||||||
) : AppBarLayout(context, attrs, defStyleAttr), MenuHost {
|
|
||||||
|
|
||||||
private val binding = LayoutSheetHeaderBinding.inflate(LayoutInflater.from(context), this)
|
|
||||||
private val closeDrawable = context.getThemeDrawable(materialR.attr.actionModeCloseDrawable)
|
|
||||||
private val bottomSheetCallback = Callback()
|
|
||||||
private val adjustStateRunnable = Runnable { adjustState() }
|
|
||||||
private var bottomSheetBehavior: BottomSheetBehavior<*>? = null
|
|
||||||
private val locationBuffer = IntArray(2)
|
|
||||||
private val expansionListeners = LinkedList<OnExpansionChangeListener>()
|
|
||||||
private var fitStatusBar = false
|
|
||||||
private val minHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_min)
|
|
||||||
private val maxHandleHeight = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_handle_size_max)
|
|
||||||
private var isLayoutSuppressedCompat = false
|
|
||||||
private var isLayoutCalledWhileSuppressed = false
|
|
||||||
private var isBsExpanded = false
|
|
||||||
private var stateAdjustedAt = 0L
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
val toolbar: MaterialToolbar
|
|
||||||
get() = binding.toolbar
|
|
||||||
|
|
||||||
val menu: Menu
|
|
||||||
get() = binding.toolbar.menu
|
|
||||||
|
|
||||||
var title: CharSequence?
|
|
||||||
get() = binding.toolbar.title
|
|
||||||
set(value) {
|
|
||||||
binding.toolbar.title = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var subtitle: CharSequence?
|
|
||||||
get() = binding.toolbar.subtitle
|
|
||||||
set(value) {
|
|
||||||
binding.toolbar.subtitle = value
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
setBackgroundResource(R.drawable.sheet_toolbar_background)
|
|
||||||
layoutTransition = LayoutTransition().apply {
|
|
||||||
setDuration(context.getAnimationDuration(R.integer.config_tinyAnimTime))
|
|
||||||
}
|
|
||||||
context.withStyledAttributes(attrs, R.styleable.BottomSheetHeaderBar, defStyleAttr) {
|
|
||||||
binding.toolbar.title = getString(R.styleable.BottomSheetHeaderBar_title)
|
|
||||||
fitStatusBar = getBoolean(R.styleable.BottomSheetHeaderBar_fitStatusBar, fitStatusBar)
|
|
||||||
val menuResId = getResourceId(R.styleable.BottomSheetHeaderBar_menu, 0)
|
|
||||||
if (menuResId != 0) {
|
|
||||||
binding.toolbar.inflateMenu(menuResId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.toolbar.setNavigationOnClickListener(bottomSheetCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
|
||||||
super.onAttachedToWindow()
|
|
||||||
setBottomSheetBehavior(findParentBottomSheetBehavior())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
|
||||||
setBottomSheetBehavior(null)
|
|
||||||
super.onDetachedFromWindow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addView(child: View?, index: Int) {
|
|
||||||
if (shouldAddView(child)) {
|
|
||||||
super.addView(child, index)
|
|
||||||
} else {
|
|
||||||
binding.toolbar.addView(child, index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addView(child: View?, width: Int, height: Int) {
|
|
||||||
if (shouldAddView(child)) {
|
|
||||||
super.addView(child, width, height)
|
|
||||||
} else {
|
|
||||||
binding.toolbar.addView(child, width, height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
|
||||||
if (shouldAddView(child)) {
|
|
||||||
super.addView(child, index, params)
|
|
||||||
} else {
|
|
||||||
binding.toolbar.addView(child, index, convertLayoutParams(params))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets {
|
|
||||||
dispatchInsets(if (insets != null) WindowInsetsCompat.toWindowInsetsCompat(insets) else null)
|
|
||||||
return super.onApplyWindowInsets(insets)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addMenuProvider(provider: MenuProvider) {
|
|
||||||
binding.toolbar.addMenuProvider(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addMenuProvider(provider: MenuProvider, owner: LifecycleOwner) {
|
|
||||||
binding.toolbar.addMenuProvider(provider, owner)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addMenuProvider(provider: MenuProvider, owner: LifecycleOwner, state: Lifecycle.State) {
|
|
||||||
binding.toolbar.addMenuProvider(provider, owner, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeMenuProvider(provider: MenuProvider) {
|
|
||||||
binding.toolbar.removeMenuProvider(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun invalidateMenu() {
|
|
||||||
binding.toolbar.invalidateMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun inflateMenu(@MenuRes resId: Int) {
|
|
||||||
binding.toolbar.inflateMenu(resId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNavigationOnClickListener(onClickListener: OnClickListener) {
|
|
||||||
binding.toolbar.setNavigationOnClickListener(onClickListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addOnExpansionChangeListener(listener: OnExpansionChangeListener) {
|
|
||||||
expansionListeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeOnExpansionChangeListener(listener: OnExpansionChangeListener) {
|
|
||||||
expansionListeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTitle(@StringRes resId: Int) {
|
|
||||||
binding.toolbar.setTitle(resId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSubtitle(@StringRes resId: Int) {
|
|
||||||
binding.toolbar.setSubtitle(resId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
|
||||||
if (isLayoutSuppressedCompat) {
|
|
||||||
isLayoutCalledWhileSuppressed = true
|
|
||||||
} else {
|
|
||||||
super.onLayout(changed, l, t, r, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setBottomSheetBehavior(behavior: BottomSheetBehavior<*>?) {
|
|
||||||
bottomSheetBehavior?.removeBottomSheetCallback(bottomSheetCallback)
|
|
||||||
bottomSheetBehavior = behavior
|
|
||||||
if (behavior != null) {
|
|
||||||
onBottomSheetStateChanged(behavior.state)
|
|
||||||
behavior.addBottomSheetCallback(bottomSheetCallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onBottomSheetStateChanged(newState: Int) {
|
|
||||||
val expanded = newState == BottomSheetBehavior.STATE_EXPANDED && isOnTopOfScreen()
|
|
||||||
if (isBsExpanded != expanded) {
|
|
||||||
isBsExpanded = expanded
|
|
||||||
postAdjustState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun suppressLayoutCompat(suppress: Boolean) {
|
|
||||||
if (suppress == isLayoutSuppressedCompat) return
|
|
||||||
isLayoutSuppressedCompat = suppress
|
|
||||||
if (!suppress && isLayoutCalledWhileSuppressed) {
|
|
||||||
requestLayout()
|
|
||||||
}
|
|
||||||
isLayoutCalledWhileSuppressed = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dispatchInsets(insets: WindowInsetsCompat?) {
|
|
||||||
if (!fitStatusBar) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val isExpanded = binding.dragHandle.isGone
|
|
||||||
val topInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
|
|
||||||
if (isExpanded) {
|
|
||||||
updatePadding(top = topInset)
|
|
||||||
} else {
|
|
||||||
updatePadding(top = 0)
|
|
||||||
binding.dragHandle.updateLayoutParams {
|
|
||||||
height = topInset.coerceIn(minHandleHeight, maxHandleHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findParentBottomSheetBehavior(): BottomSheetBehavior<*>? {
|
|
||||||
for (p in parents) {
|
|
||||||
val layoutParams = (p as? View)?.layoutParams
|
|
||||||
if (layoutParams is CoordinatorLayout.LayoutParams) {
|
|
||||||
val behavior = layoutParams.behavior
|
|
||||||
if (behavior is BottomSheetBehavior<*>) {
|
|
||||||
return behavior
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isOnTopOfScreen(): Boolean {
|
|
||||||
getLocationInWindow(locationBuffer)
|
|
||||||
val topInset = ViewCompat.getRootWindowInsets(this)
|
|
||||||
?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
|
|
||||||
val zeroTop = (layoutParams as? MarginLayoutParams)?.topMargin ?: 0
|
|
||||||
return (locationBuffer[1] - topInset) <= zeroTop
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dismissBottomSheet() {
|
|
||||||
val behavior = bottomSheetBehavior ?: return
|
|
||||||
if (behavior.isHideable) {
|
|
||||||
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
||||||
} else {
|
|
||||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldAddView(child: View?): Boolean {
|
|
||||||
if (child == null) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val viewId = child.id
|
|
||||||
return viewId == R.id.dragHandle || viewId == R.id.toolbar
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun convertLayoutParams(params: ViewGroup.LayoutParams?): Toolbar.LayoutParams? {
|
|
||||||
return when (params) {
|
|
||||||
null -> null
|
|
||||||
is MarginLayoutParams -> {
|
|
||||||
val lp = Toolbar.LayoutParams(params)
|
|
||||||
if (params is LayoutParams) {
|
|
||||||
lp.gravity = params.gravity
|
|
||||||
}
|
|
||||||
lp
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> Toolbar.LayoutParams(params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun postAdjustState() {
|
|
||||||
removeCallbacks(adjustStateRunnable)
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
if (stateAdjustedAt + THROTTLE_DELAY < now) {
|
|
||||||
adjustState()
|
|
||||||
} else {
|
|
||||||
postDelayed(adjustStateRunnable, THROTTLE_DELAY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustState() {
|
|
||||||
suppressLayoutCompat(true)
|
|
||||||
binding.toolbar.navigationIcon = (if (isBsExpanded) closeDrawable else null)
|
|
||||||
binding.dragHandle.isGone = isBsExpanded
|
|
||||||
expansionListeners.forEach { it.onExpansionStateChanged(this, isBsExpanded) }
|
|
||||||
dispatchInsets(ViewCompat.getRootWindowInsets(this))
|
|
||||||
stateAdjustedAt = System.currentTimeMillis()
|
|
||||||
suppressLayoutCompat(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), OnClickListener {
|
|
||||||
|
|
||||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
|
||||||
onBottomSheetStateChanged(newState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
|
||||||
dismissBottomSheet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface OnExpansionChangeListener {
|
|
||||||
|
|
||||||
fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.data
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
abstract class BookmarksDao {
|
|
||||||
|
|
||||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
|
||||||
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
|
|
||||||
abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow<BookmarkEntity?>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC")
|
|
||||||
abstract fun observe(mangaId: Long): Flow<List<BookmarkEntity>>
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query(
|
|
||||||
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at"
|
|
||||||
)
|
|
||||||
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
abstract suspend fun insert(entity: BookmarkEntity)
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
abstract suspend fun delete(entity: BookmarkEntity)
|
|
||||||
|
|
||||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
|
||||||
abstract suspend fun delete(mangaId: Long, pageId: Long)
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.domain
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Bookmark(
|
|
||||||
val manga: Manga,
|
|
||||||
val pageId: Long,
|
|
||||||
val chapterId: Long,
|
|
||||||
val page: Int,
|
|
||||||
val scroll: Int,
|
|
||||||
val imageUrl: String,
|
|
||||||
val createdAt: Date,
|
|
||||||
val percent: Float,
|
|
||||||
) {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Bookmark
|
|
||||||
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (pageId != other.pageId) return false
|
|
||||||
if (chapterId != other.chapterId) return false
|
|
||||||
if (page != other.page) return false
|
|
||||||
if (scroll != other.scroll) return false
|
|
||||||
if (imageUrl != other.imageUrl) return false
|
|
||||||
if (createdAt != other.createdAt) return false
|
|
||||||
if (percent != other.percent) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = manga.hashCode()
|
|
||||||
result = 31 * result + pageId.hashCode()
|
|
||||||
result = 31 * result + chapterId.hashCode()
|
|
||||||
result = 31 * result + page
|
|
||||||
result = 31 * result + scroll
|
|
||||||
result = 31 * result + imageUrl.hashCode()
|
|
||||||
result = 31 * result + createdAt.hashCode()
|
|
||||||
result = 31 * result + percent.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
|
||||||
import org.koitharu.kotatsu.bookmarks.data.ids
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
|
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
|
||||||
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
|
||||||
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
|
||||||
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class BookmarksFragment :
|
|
||||||
BaseFragment<FragmentListSimpleBinding>(),
|
|
||||||
ListStateHolderListener,
|
|
||||||
OnListItemClickListener<Bookmark>,
|
|
||||||
SectionedSelectionController.Callback<Manga>,
|
|
||||||
FastScroller.FastScrollListener {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var coil: ImageLoader
|
|
||||||
|
|
||||||
private val viewModel by viewModels<BookmarksViewModel>()
|
|
||||||
private var adapter: BookmarksGroupAdapter? = null
|
|
||||||
private var selectionController: SectionedSelectionController<Manga>? = null
|
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
|
|
||||||
return FragmentListSimpleBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
selectionController = SectionedSelectionController(
|
|
||||||
activity = requireActivity(),
|
|
||||||
owner = this,
|
|
||||||
callback = this,
|
|
||||||
)
|
|
||||||
adapter = BookmarksGroupAdapter(
|
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
|
||||||
coil = coil,
|
|
||||||
listener = this,
|
|
||||||
selectionController = checkNotNull(selectionController),
|
|
||||||
bookmarkClickListener = this,
|
|
||||||
groupClickListener = OnGroupClickListener(),
|
|
||||||
)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
|
||||||
val spacingDecoration = SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
|
||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
|
||||||
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
|
||||||
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
adapter = null
|
|
||||||
selectionController = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: Bookmark, view: View) {
|
|
||||||
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
|
|
||||||
val intent = ReaderActivity.newIntent(view.context, item)
|
|
||||||
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
|
|
||||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
|
||||||
return selectionController?.onItemLongClick(item.manga, item.pageId) ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRetryClick(error: Throwable) = Unit
|
|
||||||
|
|
||||||
override fun onEmptyActionClick() = Unit
|
|
||||||
|
|
||||||
override fun onFastScrollStart(fastScroller: FastScroller) {
|
|
||||||
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFastScrollStop(fastScroller: FastScroller) = Unit
|
|
||||||
|
|
||||||
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
|
|
||||||
binding.recyclerView.invalidateNestedItemDecorations()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateActionMode(
|
|
||||||
controller: SectionedSelectionController<Manga>,
|
|
||||||
mode: ActionMode,
|
|
||||||
menu: Menu,
|
|
||||||
): Boolean {
|
|
||||||
mode.menuInflater.inflate(R.menu.mode_bookmarks, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionItemClicked(
|
|
||||||
controller: SectionedSelectionController<Manga>,
|
|
||||||
mode: ActionMode,
|
|
||||||
item: MenuItem,
|
|
||||||
): Boolean {
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.action_remove -> {
|
|
||||||
val ids = selectionController?.snapshot() ?: return false
|
|
||||||
viewModel.removeBookmarks(ids)
|
|
||||||
mode.finish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateItemDecoration(
|
|
||||||
controller: SectionedSelectionController<Manga>,
|
|
||||||
section: Manga,
|
|
||||||
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
binding.recyclerView.updatePadding(
|
|
||||||
bottom = insets.bottom,
|
|
||||||
)
|
|
||||||
binding.recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = insets.bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onListChanged(list: List<ListModel>) {
|
|
||||||
adapter?.items = list
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onActionDone(action: ReversibleAction) {
|
|
||||||
val handle = action.handle
|
|
||||||
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
|
|
||||||
val snackbar = Snackbar.make((activity as SnackbarOwner).snackbarHost, action.stringResId, length)
|
|
||||||
if (handle != null) {
|
|
||||||
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
|
|
||||||
}
|
|
||||||
snackbar.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class OnGroupClickListener : OnListItemClickListener<BookmarksGroup> {
|
|
||||||
|
|
||||||
override fun onItemClick(item: BookmarksGroup, view: View) {
|
|
||||||
val controller = selectionController
|
|
||||||
if (controller != null && controller.count > 0) {
|
|
||||||
if (controller.getSectionCount(item.manga) == item.bookmarks.size) {
|
|
||||||
controller.clearSelection(item.manga)
|
|
||||||
} else {
|
|
||||||
controller.addToSelection(item.manga, item.bookmarks.ids())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val intent = DetailsActivity.newIntent(view.context, item.manga)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemLongClick(item: BookmarksGroup, view: View): Boolean {
|
|
||||||
return selectionController?.addToSelection(item.manga, item.bookmarks.ids()) ?: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun newInstance() = BookmarksFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
|
|
||||||
class BookmarksAdapter(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
clickListener: OnListItemClickListener<Bookmark>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<Bookmark>(
|
|
||||||
DiffCallback(),
|
|
||||||
bookmarkListAD(coil, lifecycleOwner, clickListener)
|
|
||||||
) {
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
|
||||||
return oldItem.manga.id == newItem.manga.id && oldItem.pageId == newItem.pageId
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
|
||||||
return oldItem.imageUrl == newItem.imageUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
|
|
||||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
|
||||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
|
||||||
import org.koitharu.kotatsu.utils.ext.source
|
|
||||||
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
|
||||||
|
|
||||||
fun bookmarksGroupAD(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
sharedPool: RecyclerView.RecycledViewPool,
|
|
||||||
selectionController: SectionedSelectionController<Manga>,
|
|
||||||
bookmarkClickListener: OnListItemClickListener<Bookmark>,
|
|
||||||
groupClickListener: OnListItemClickListener<BookmarksGroup>,
|
|
||||||
) = adapterDelegateViewBinding<BookmarksGroup, ListModel, ItemBookmarksGroupBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemBookmarksGroupBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
val viewListenerAdapter = object : View.OnClickListener, View.OnLongClickListener {
|
|
||||||
override fun onClick(v: View) = groupClickListener.onItemClick(item, v)
|
|
||||||
override fun onLongClick(v: View) = groupClickListener.onItemLongClick(item, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
val adapter = BookmarksAdapter(coil, lifecycleOwner, bookmarkClickListener)
|
|
||||||
binding.recyclerView.setRecycledViewPool(sharedPool)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
|
||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
|
||||||
binding.root.setOnClickListener(viewListenerAdapter)
|
|
||||||
binding.root.setOnLongClickListener(viewListenerAdapter)
|
|
||||||
|
|
||||||
bind { payloads ->
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
binding.recyclerView.clearItemDecorations()
|
|
||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
|
||||||
selectionController.attachToRecyclerView(item.manga, binding.recyclerView)
|
|
||||||
}
|
|
||||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
|
|
||||||
placeholder(R.drawable.ic_placeholder)
|
|
||||||
fallback(R.drawable.ic_placeholder)
|
|
||||||
error(R.drawable.ic_error_placeholder)
|
|
||||||
allowRgb565(true)
|
|
||||||
size(CoverSizeResolver(binding.imageViewCover))
|
|
||||||
source(item.manga.source)
|
|
||||||
enqueueWith(coil)
|
|
||||||
}
|
|
||||||
binding.textViewTitle.text = item.manga.title
|
|
||||||
adapter.items = item.bookmarks
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewRecycled {
|
|
||||||
binding.imageViewCover.disposeImageRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import kotlin.jvm.internal.Intrinsics
|
|
||||||
|
|
||||||
class BookmarksGroupAdapter(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
selectionController: SectionedSelectionController<Manga>,
|
|
||||||
listener: ListStateHolderListener,
|
|
||||||
bookmarkClickListener: OnListItemClickListener<Bookmark>,
|
|
||||||
groupClickListener: OnListItemClickListener<BookmarksGroup>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
val pool = RecyclerView.RecycledViewPool()
|
|
||||||
delegatesManager
|
|
||||||
.addDelegate(
|
|
||||||
bookmarksGroupAD(
|
|
||||||
coil = coil,
|
|
||||||
lifecycleOwner = lifecycleOwner,
|
|
||||||
sharedPool = pool,
|
|
||||||
selectionController = selectionController,
|
|
||||||
bookmarkClickListener = bookmarkClickListener,
|
|
||||||
groupClickListener = groupClickListener,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addDelegate(loadingStateAD())
|
|
||||||
.addDelegate(loadingFooterAD())
|
|
||||||
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener))
|
|
||||||
.addDelegate(errorStateListAD(listener))
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem is BookmarksGroup && newItem is BookmarksGroup -> {
|
|
||||||
oldItem.manga.id == newItem.manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
|
||||||
return Intrinsics.areEqual(oldItem, newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
|
||||||
return when {
|
|
||||||
oldItem is BookmarksGroup && newItem is BookmarksGroup -> Unit
|
|
||||||
else -> super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.model
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.util.areItemsEquals
|
|
||||||
|
|
||||||
class BookmarksGroup(
|
|
||||||
val manga: Manga,
|
|
||||||
val bookmarks: List<Bookmark>,
|
|
||||||
) : ListModel {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as BookmarksGroup
|
|
||||||
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
|
|
||||||
return bookmarks.areItemsEquals(other.bookmarks) { a, b ->
|
|
||||||
a.imageUrl == b.imageUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = manga.hashCode()
|
|
||||||
result = 31 * result + bookmarks.sumOf { it.imageUrl.hashCode() }
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser.cloudflare
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.CookieManager
|
|
||||||
import android.webkit.WebSettings
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.fragment.app.setFragmentResult
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import okhttp3.Headers
|
|
||||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
|
||||||
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
|
||||||
|
|
||||||
private lateinit var url: String
|
|
||||||
private val pendingResult = Bundle(1)
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var cookieJar: MutableCookieJar
|
|
||||||
|
|
||||||
private var onBackPressedCallback: WebViewBackPressedCallback? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
url = requireArguments().getString(ARG_URL).orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
) = FragmentCloudflareBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
with(binding.webView.settings) {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
cacheMode = WebSettings.LOAD_DEFAULT
|
|
||||||
domStorageEnabled = true
|
|
||||||
databaseEnabled = true
|
|
||||||
userAgentString = arguments?.getString(ARG_UA) ?: CommonHeadersInterceptor.userAgentChrome
|
|
||||||
}
|
|
||||||
binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
|
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
|
||||||
if (url.isEmpty()) {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
} else {
|
|
||||||
binding.webView.loadUrl(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
binding.webView.stopLoading()
|
|
||||||
binding.webView.destroy()
|
|
||||||
onBackPressedCallback = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
|
||||||
return super.onBuildDialog(builder).setNegativeButton(android.R.string.cancel, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogCreated(dialog: AlertDialog) {
|
|
||||||
super.onDialogCreated(dialog)
|
|
||||||
onBackPressedCallback = WebViewBackPressedCallback(binding.webView).also {
|
|
||||||
dialog.onBackPressedDispatcher.addCallback(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
binding.webView.onResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
binding.webView.onPause()
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
setFragmentResult(TAG, pendingResult)
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageLoaded() {
|
|
||||||
bindingOrNull()?.progressBar?.isInvisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCheckPassed() {
|
|
||||||
pendingResult.putBoolean(EXTRA_RESULT, true)
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onHistoryChanged() {
|
|
||||||
onBackPressedCallback?.onHistoryChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val TAG = "CloudFlareDialog"
|
|
||||||
const val EXTRA_RESULT = "result"
|
|
||||||
private const val ARG_URL = "url"
|
|
||||||
private const val ARG_UA = "ua"
|
|
||||||
|
|
||||||
fun newInstance(url: String, headers: Headers?) = CloudFlareDialog().withArgs(2) {
|
|
||||||
putString(ARG_URL, url)
|
|
||||||
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
|
||||||
putString(ARG_UA, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db.migrations
|
|
||||||
|
|
||||||
import androidx.room.migration.Migration
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
|
|
||||||
class Migration14To15 : Migration(14, 15) {
|
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
|
||||||
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_brightness` REAL NOT NULL DEFAULT 0")
|
|
||||||
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_contrast` REAL NOT NULL DEFAULT 0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class FavouriteCategory(
|
|
||||||
val id: Long,
|
|
||||||
val title: String,
|
|
||||||
val sortKey: Int,
|
|
||||||
val order: SortOrder,
|
|
||||||
val createdAt: Date,
|
|
||||||
val isTrackingEnabled: Boolean,
|
|
||||||
val isVisibleInLibrary: Boolean,
|
|
||||||
) : Parcelable
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import androidx.core.os.ParcelCompat
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.utils.ext.readParcelableCompat
|
|
||||||
import org.koitharu.kotatsu.utils.ext.readSerializableCompat
|
|
||||||
|
|
||||||
fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) {
|
|
||||||
out.writeLong(id)
|
|
||||||
out.writeString(title)
|
|
||||||
out.writeString(altTitle)
|
|
||||||
out.writeString(url)
|
|
||||||
out.writeString(publicUrl)
|
|
||||||
out.writeFloat(rating)
|
|
||||||
ParcelCompat.writeBoolean(out, isNsfw)
|
|
||||||
out.writeString(coverUrl)
|
|
||||||
out.writeString(largeCoverUrl)
|
|
||||||
out.writeString(description)
|
|
||||||
out.writeParcelable(ParcelableMangaTags(tags), flags)
|
|
||||||
out.writeSerializable(state)
|
|
||||||
out.writeString(author)
|
|
||||||
if (withChapters) {
|
|
||||||
out.writeParcelable(chapters?.let(::ParcelableMangaChapters), flags)
|
|
||||||
} else {
|
|
||||||
out.writeString(null)
|
|
||||||
}
|
|
||||||
out.writeSerializable(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcel.readManga() = Manga(
|
|
||||||
id = readLong(),
|
|
||||||
title = requireNotNull(readString()),
|
|
||||||
altTitle = readString(),
|
|
||||||
url = requireNotNull(readString()),
|
|
||||||
publicUrl = requireNotNull(readString()),
|
|
||||||
rating = readFloat(),
|
|
||||||
isNsfw = ParcelCompat.readBoolean(this),
|
|
||||||
coverUrl = requireNotNull(readString()),
|
|
||||||
largeCoverUrl = readString(),
|
|
||||||
description = readString(),
|
|
||||||
tags = requireNotNull(readParcelableCompat<ParcelableMangaTags>()).tags,
|
|
||||||
state = readSerializableCompat(),
|
|
||||||
author = readString(),
|
|
||||||
chapters = readParcelableCompat<ParcelableMangaChapters>()?.chapters,
|
|
||||||
source = checkNotNull(readSerializableCompat()),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MangaPage.writeToParcel(out: Parcel) {
|
|
||||||
out.writeLong(id)
|
|
||||||
out.writeString(url)
|
|
||||||
out.writeString(preview)
|
|
||||||
out.writeSerializable(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcel.readMangaPage() = MangaPage(
|
|
||||||
id = readLong(),
|
|
||||||
url = requireNotNull(readString()),
|
|
||||||
preview = readString(),
|
|
||||||
source = checkNotNull(readSerializableCompat()),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MangaChapter.writeToParcel(out: Parcel) {
|
|
||||||
out.writeLong(id)
|
|
||||||
out.writeString(name)
|
|
||||||
out.writeInt(number)
|
|
||||||
out.writeString(url)
|
|
||||||
out.writeString(scanlator)
|
|
||||||
out.writeLong(uploadDate)
|
|
||||||
out.writeString(branch)
|
|
||||||
out.writeSerializable(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcel.readMangaChapter() = MangaChapter(
|
|
||||||
id = readLong(),
|
|
||||||
name = requireNotNull(readString()),
|
|
||||||
number = readInt(),
|
|
||||||
url = requireNotNull(readString()),
|
|
||||||
scanlator = readString(),
|
|
||||||
uploadDate = readLong(),
|
|
||||||
branch = readString(),
|
|
||||||
source = checkNotNull(readSerializableCompat()),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MangaTag.writeToParcel(out: Parcel) {
|
|
||||||
out.writeString(title)
|
|
||||||
out.writeString(key)
|
|
||||||
out.writeSerializable(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcel.readMangaTag() = MangaTag(
|
|
||||||
title = requireNotNull(readString()),
|
|
||||||
key = requireNotNull(readString()),
|
|
||||||
source = checkNotNull(readSerializableCompat()),
|
|
||||||
)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
|
|
||||||
// Limits to avoid TransactionTooLargeException
|
|
||||||
private const val MAX_SAFE_SIZE = 1024 * 100 // Assume that 100 kb is safe parcel size
|
|
||||||
private const val MAX_SAFE_CHAPTERS_COUNT = 24 // this is 100% safe
|
|
||||||
|
|
||||||
class ParcelableManga(
|
|
||||||
val manga: Manga,
|
|
||||||
private val withChapters: Boolean,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(parcel.readManga(), true)
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
val chapters = manga.chapters
|
|
||||||
if (!withChapters || chapters == null) {
|
|
||||||
manga.writeToParcel(parcel, flags, withChapters = false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (chapters.size <= MAX_SAFE_CHAPTERS_COUNT) {
|
|
||||||
// fast path
|
|
||||||
manga.writeToParcel(parcel, flags, withChapters = true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val tempParcel = Parcel.obtain()
|
|
||||||
manga.writeToParcel(tempParcel, flags, withChapters = true)
|
|
||||||
val size = tempParcel.dataSize()
|
|
||||||
if (size < MAX_SAFE_SIZE) {
|
|
||||||
parcel.appendFrom(tempParcel, 0, size)
|
|
||||||
} else {
|
|
||||||
manga.writeToParcel(parcel, flags, withChapters = false)
|
|
||||||
}
|
|
||||||
tempParcel.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ParcelableManga> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): ParcelableManga {
|
|
||||||
return ParcelableManga(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ParcelableManga?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
|
|
||||||
class ParcelableMangaChapters(
|
|
||||||
val chapters: List<MangaChapter>,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
|
||||||
List(parcel.readInt()) { parcel.readMangaChapter() }
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeInt(chapters.size)
|
|
||||||
for (chapter in chapters) {
|
|
||||||
chapter.writeToParcel(parcel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaChapters> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaChapters {
|
|
||||||
return ParcelableMangaChapters(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ParcelableMangaChapters?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
|
|
||||||
class ParcelableMangaPages(
|
|
||||||
val pages: List<MangaPage>,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
|
||||||
List(parcel.readInt()) { parcel.readMangaPage() }
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeInt(pages.size)
|
|
||||||
for (page in pages) {
|
|
||||||
page.writeToParcel(parcel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaPages> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaPages {
|
|
||||||
return ParcelableMangaPages(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ParcelableMangaPages?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.utils.ext.Set
|
|
||||||
|
|
||||||
class ParcelableMangaTags(
|
|
||||||
val tags: Set<MangaTag>,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
|
||||||
Set(parcel.readInt()) { parcel.readMangaTag() }
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeInt(tags.size)
|
|
||||||
for (tag in tags) {
|
|
||||||
tag.writeToParcel(parcel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaTags> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaTags {
|
|
||||||
return ParcelableMangaTags(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ParcelableMangaTags?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.transition.Slide
|
|
||||||
import android.transition.TransitionManager
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
|
||||||
import org.koitharu.kotatsu.base.ui.dialog.RecyclerViewAlertDialog
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|
||||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
|
||||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
|
||||||
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
|
||||||
import org.koitharu.kotatsu.details.ui.adapter.branchAD
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
|
||||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
|
||||||
import org.koitharu.kotatsu.utils.ViewBadge
|
|
||||||
import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat
|
|
||||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class DetailsActivity :
|
|
||||||
BaseActivity<ActivityDetailsBinding>(),
|
|
||||||
View.OnClickListener,
|
|
||||||
BottomSheetHeaderBar.OnExpansionChangeListener,
|
|
||||||
NoModalBottomSheetOwner,
|
|
||||||
View.OnLongClickListener,
|
|
||||||
PopupMenu.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
override val bsHeader: BottomSheetHeaderBar?
|
|
||||||
get() = binding.headerChapters
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
|
||||||
|
|
||||||
private lateinit var viewBadge: ViewBadge
|
|
||||||
|
|
||||||
private val viewModel: DetailsViewModel by viewModels()
|
|
||||||
private lateinit var chaptersMenuProvider: ChaptersMenuProvider
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(ActivityDetailsBinding.inflate(layoutInflater))
|
|
||||||
supportActionBar?.run {
|
|
||||||
setDisplayHomeAsUpEnabled(true)
|
|
||||||
setDisplayShowTitleEnabled(false)
|
|
||||||
}
|
|
||||||
binding.buttonRead.setOnClickListener(this)
|
|
||||||
binding.buttonRead.setOnLongClickListener(this)
|
|
||||||
binding.buttonDropdown.setOnClickListener(this)
|
|
||||||
viewBadge = ViewBadge(binding.buttonRead, this)
|
|
||||||
|
|
||||||
chaptersMenuProvider = if (binding.layoutBottom != null) {
|
|
||||||
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom))
|
|
||||||
actionModeDelegate.addListener(bsMediator)
|
|
||||||
checkNotNull(binding.headerChapters).addOnExpansionChangeListener(bsMediator)
|
|
||||||
checkNotNull(binding.headerChapters).addOnLayoutChangeListener(bsMediator)
|
|
||||||
onBackPressedDispatcher.addCallback(bsMediator)
|
|
||||||
ChaptersMenuProvider(viewModel, bsMediator)
|
|
||||||
} else {
|
|
||||||
ChaptersMenuProvider(viewModel, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.manga.observe(this, ::onMangaUpdated)
|
|
||||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
|
||||||
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
|
|
||||||
viewModel.onError.observe(
|
|
||||||
this,
|
|
||||||
SnackbarErrorObserver(
|
|
||||||
host = binding.containerDetails,
|
|
||||||
fragment = null,
|
|
||||||
resolver = exceptionResolver,
|
|
||||||
onResolved = { isResolved ->
|
|
||||||
if (isResolved) {
|
|
||||||
viewModel.reload()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
viewModel.onShowToast.observe(this) {
|
|
||||||
makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
|
||||||
viewModel.selectedBranchName.observe(this) {
|
|
||||||
binding.headerChapters?.subtitle = it
|
|
||||||
binding.textViewSubtitle?.textAndVisible = it
|
|
||||||
}
|
|
||||||
viewModel.isChaptersReversed.observe(this) {
|
|
||||||
binding.headerChapters?.invalidateMenu() ?: invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
viewModel.favouriteCategories.observe(this) {
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
viewModel.branches.observe(this) {
|
|
||||||
binding.buttonDropdown.isVisible = it.size > 1
|
|
||||||
}
|
|
||||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
|
||||||
viewModel.onDownloadStarted.observe(this, DownloadStartedObserver(binding.containerDetails))
|
|
||||||
|
|
||||||
addMenuProvider(
|
|
||||||
DetailsMenuProvider(
|
|
||||||
activity = this,
|
|
||||||
viewModel = viewModel,
|
|
||||||
snackbarHost = binding.containerChapters,
|
|
||||||
shortcutsUpdater = shortcutsUpdater,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
binding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
when (v.id) {
|
|
||||||
R.id.button_read -> openReader(isIncognitoMode = false)
|
|
||||||
R.id.button_dropdown -> showBranchPopupMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(v: View): Boolean = when (v.id) {
|
|
||||||
R.id.button_read -> {
|
|
||||||
val menu = PopupMenu(v.context, v)
|
|
||||||
menu.inflate(R.menu.popup_read)
|
|
||||||
menu.setOnMenuItemClickListener(this)
|
|
||||||
menu.setForceShowIcon(true)
|
|
||||||
menu.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
|
|
||||||
R.id.action_incognito -> {
|
|
||||||
openReader(isIncognitoMode = true)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
|
||||||
if (isExpanded) {
|
|
||||||
headerBar.addMenuProvider(chaptersMenuProvider)
|
|
||||||
} else {
|
|
||||||
headerBar.removeMenuProvider(chaptersMenuProvider)
|
|
||||||
}
|
|
||||||
binding.buttonRead.isGone = isExpanded
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMangaUpdated(manga: Manga) {
|
|
||||||
title = manga.title
|
|
||||||
val hasChapters = !manga.chapters.isNullOrEmpty()
|
|
||||||
binding.buttonRead.isEnabled = hasChapters
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
showBottomSheet(manga.chapters != null)
|
|
||||||
binding.groupHeader?.isVisible = hasChapters
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMangaRemoved(manga: Manga) {
|
|
||||||
Toast.makeText(
|
|
||||||
this,
|
|
||||||
getString(R.string._s_deleted_from_local_storage, manga.title),
|
|
||||||
Toast.LENGTH_SHORT,
|
|
||||||
).show()
|
|
||||||
finishAfterTransition()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
binding.root.updatePadding(
|
|
||||||
left = insets.left,
|
|
||||||
right = insets.right,
|
|
||||||
)
|
|
||||||
if (insets.bottom > 0) {
|
|
||||||
window.setNavigationBarTransparentCompat(this, binding.layoutBottom?.elevation ?: 0f, 0.9f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onHistoryChanged(info: HistoryInfo) {
|
|
||||||
with(binding.buttonRead) {
|
|
||||||
if (info.history != null) {
|
|
||||||
setText(R.string._continue)
|
|
||||||
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
|
|
||||||
} else {
|
|
||||||
setText(R.string.read)
|
|
||||||
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val text = when {
|
|
||||||
!info.isValid -> getString(R.string.loading_)
|
|
||||||
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters)
|
|
||||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
|
||||||
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
|
||||||
}
|
|
||||||
binding.headerChapters?.title = text
|
|
||||||
binding.textViewTitle?.text = text
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onNewChaptersChanged(newChapters: Int) {
|
|
||||||
viewBadge.counter = newChapters
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showChapterMissingDialog(chapterId: Long) {
|
|
||||||
val remoteManga = viewModel.getRemoteManga()
|
|
||||||
if (remoteManga == null) {
|
|
||||||
val snackbar = makeSnackbar(getString(R.string.chapter_is_missing), Snackbar.LENGTH_SHORT)
|
|
||||||
snackbar.show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
MaterialAlertDialogBuilder(this).apply {
|
|
||||||
setMessage(R.string.chapter_is_missing_text)
|
|
||||||
setTitle(R.string.chapter_is_missing)
|
|
||||||
setNegativeButton(android.R.string.cancel, null)
|
|
||||||
setPositiveButton(R.string.read) { _, _ ->
|
|
||||||
startActivity(
|
|
||||||
ReaderActivity.newIntent(
|
|
||||||
context = this@DetailsActivity,
|
|
||||||
manga = remoteManga,
|
|
||||||
state = ReaderState(chapterId, 0, 0),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setNeutralButton(R.string.download) { _, _ ->
|
|
||||||
viewModel.download(setOf(chapterId))
|
|
||||||
}
|
|
||||||
setCancelable(true)
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBranchPopupMenu() {
|
|
||||||
var dialog: DialogInterface? = null
|
|
||||||
val listener = OnListItemClickListener<MangaBranch> { item, _ ->
|
|
||||||
viewModel.setSelectedBranch(item.name)
|
|
||||||
dialog?.dismiss()
|
|
||||||
}
|
|
||||||
dialog = RecyclerViewAlertDialog.Builder<MangaBranch>(this)
|
|
||||||
.addAdapterDelegate(branchAD(listener))
|
|
||||||
.setCancelable(true)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setTitle(R.string.translations)
|
|
||||||
.setItems(viewModel.branches.value.orEmpty())
|
|
||||||
.create()
|
|
||||||
.also { it.show() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openReader(isIncognitoMode: Boolean) {
|
|
||||||
val manga = viewModel.manga.value ?: return
|
|
||||||
val chapterId = viewModel.historyInfo.value?.history?.chapterId
|
|
||||||
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
|
|
||||||
showChapterMissingDialog(chapterId)
|
|
||||||
} else {
|
|
||||||
startActivity(
|
|
||||||
ReaderActivity.newIntent(
|
|
||||||
context = this,
|
|
||||||
manga = manga,
|
|
||||||
branch = viewModel.selectedBranchValue,
|
|
||||||
isIncognitoMode = isIncognitoMode,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if (isIncognitoMode) {
|
|
||||||
Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isTabletLayout() = binding.layoutBottom == null
|
|
||||||
|
|
||||||
private fun showBottomSheet(isVisible: Boolean) {
|
|
||||||
val view = binding.layoutBottom ?: return
|
|
||||||
if (view.isVisible == isVisible) return
|
|
||||||
val transition = Slide(Gravity.BOTTOM)
|
|
||||||
transition.addTarget(view)
|
|
||||||
transition.interpolator = AccelerateDecelerateInterpolator()
|
|
||||||
TransitionManager.beginDelayedTransition(binding.root as ViewGroup, transition)
|
|
||||||
view.isVisible = isVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar {
|
|
||||||
val sb = Snackbar.make(binding.containerDetails, text, duration)
|
|
||||||
if (binding.layoutBottom?.isVisible == true) {
|
|
||||||
sb.anchorView = binding.headerChapters
|
|
||||||
}
|
|
||||||
return sb
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PrefetchObserver(
|
|
||||||
private val context: Context,
|
|
||||||
) : Observer<List<ChapterListItem>?> {
|
|
||||||
|
|
||||||
private var isCalled = false
|
|
||||||
|
|
||||||
override fun onChanged(value: List<ChapterListItem>?) {
|
|
||||||
if (value.isNullOrEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!isCalled) {
|
|
||||||
isCalled = true
|
|
||||||
val item = value.find { it.hasFlag(ChapterListItem.FLAG_CURRENT) } ?: value.first()
|
|
||||||
MangaPrefetchService.prefetchPages(context, item.chapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun newIntent(context: Context, manga: Manga): Intent {
|
|
||||||
return Intent(context, DetailsActivity::class.java)
|
|
||||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun newIntent(context: Context, mangaId: Long): Intent {
|
|
||||||
return Intent(context, DetailsActivity::class.java)
|
|
||||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import android.text.Html
|
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.style.ForegroundColorSpan
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.text.getSpans
|
|
||||||
import androidx.core.text.parseAsHtml
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
|
||||||
import org.koitharu.kotatsu.details.domain.BranchComparator
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalManga
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
|
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
|
||||||
import org.koitharu.kotatsu.utils.ext.computeSize
|
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
|
||||||
import org.koitharu.kotatsu.utils.ext.toFileOrNull
|
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class DetailsViewModel @Inject constructor(
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
favouritesRepository: FavouritesRepository,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
trackingRepository: TrackingRepository,
|
|
||||||
private val bookmarksRepository: BookmarksRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
|
||||||
private val imageGetter: Html.ImageGetter,
|
|
||||||
private val delegate: MangaDetailsDelegate,
|
|
||||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
|
||||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
private var loadingJob: Job
|
|
||||||
|
|
||||||
val onShowToast = SingleLiveEvent<Int>()
|
|
||||||
val onDownloadStarted = SingleLiveEvent<Unit>()
|
|
||||||
|
|
||||||
private val history = historyRepository.observeOne(delegate.mangaId)
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
|
||||||
|
|
||||||
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
private val newChapters = settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
|
|
||||||
.flatMapLatest { isEnabled ->
|
|
||||||
if (isEnabled) {
|
|
||||||
trackingRepository.observeNewChaptersCount(delegate.mangaId)
|
|
||||||
} else {
|
|
||||||
flowOf(0)
|
|
||||||
}
|
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
|
||||||
|
|
||||||
private val chaptersQuery = MutableStateFlow("")
|
|
||||||
|
|
||||||
private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
|
|
||||||
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
|
|
||||||
val historyInfo: LiveData<HistoryInfo> = combine(
|
|
||||||
delegate.manga,
|
|
||||||
delegate.selectedBranch,
|
|
||||||
history,
|
|
||||||
historyRepository.observeShouldSkip(delegate.manga),
|
|
||||||
) { m, b, h, im ->
|
|
||||||
HistoryInfo(m, b, h, im)
|
|
||||||
}.asFlowLiveData(
|
|
||||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
|
||||||
defaultValue = HistoryInfo(null, null, null, false),
|
|
||||||
)
|
|
||||||
|
|
||||||
val bookmarks = delegate.manga.flatMapLatest {
|
|
||||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
|
||||||
|
|
||||||
val localSize = combine(
|
|
||||||
delegate.manga,
|
|
||||||
delegate.relatedManga,
|
|
||||||
) { m1, m2 ->
|
|
||||||
val url = when {
|
|
||||||
m1?.source == MangaSource.LOCAL -> m1.url
|
|
||||||
m2?.source == MangaSource.LOCAL -> m2.url
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if (url != null) {
|
|
||||||
val file = url.toUri().toFileOrNull()
|
|
||||||
file?.computeSize() ?: 0L
|
|
||||||
} else {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, 0)
|
|
||||||
|
|
||||||
val description = delegate.manga
|
|
||||||
.distinctUntilChangedBy { it?.description.orEmpty() }
|
|
||||||
.transformLatest {
|
|
||||||
val description = it?.description
|
|
||||||
if (description.isNullOrEmpty()) {
|
|
||||||
emit(null)
|
|
||||||
} else {
|
|
||||||
emit(description.parseAsHtml().filterSpans())
|
|
||||||
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
|
|
||||||
}
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null)
|
|
||||||
|
|
||||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
|
||||||
val isScrobblingAvailable: Boolean
|
|
||||||
get() = scrobblers.any { it.isAvailable }
|
|
||||||
|
|
||||||
val scrobblingInfo: LiveData<List<ScrobblingInfo>> = combine(
|
|
||||||
scrobblers.map { it.observeScrobblingInfo(delegate.mangaId) },
|
|
||||||
) { scrobblingInfo ->
|
|
||||||
scrobblingInfo.filterNotNull()
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
|
||||||
|
|
||||||
val branches: LiveData<List<MangaBranch>> = combine(
|
|
||||||
delegate.manga,
|
|
||||||
delegate.selectedBranch,
|
|
||||||
) { m, b ->
|
|
||||||
val chapters = m?.chapters ?: return@combine emptyList()
|
|
||||||
chapters.groupBy { x -> x.branch }
|
|
||||||
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
|
|
||||||
.sortedWith(BranchComparator())
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
|
||||||
|
|
||||||
val selectedBranchName = delegate.selectedBranch
|
|
||||||
.asFlowLiveData(viewModelScope.coroutineContext, null)
|
|
||||||
|
|
||||||
val isChaptersEmpty: LiveData<Boolean> = combine(
|
|
||||||
delegate.manga,
|
|
||||||
isLoading.asFlow(),
|
|
||||||
) { m, loading ->
|
|
||||||
m != null && m.chapters.isNullOrEmpty() && !loading
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext, false)
|
|
||||||
|
|
||||||
val chapters = combine(
|
|
||||||
combine(
|
|
||||||
delegate.manga,
|
|
||||||
delegate.relatedManga,
|
|
||||||
history,
|
|
||||||
delegate.selectedBranch,
|
|
||||||
newChapters,
|
|
||||||
) { manga, related, history, branch, news ->
|
|
||||||
delegate.mapChapters(manga, related, history, news, branch)
|
|
||||||
},
|
|
||||||
chaptersReversed,
|
|
||||||
chaptersQuery,
|
|
||||||
) { list, reversed, query ->
|
|
||||||
(if (reversed) list.asReversed() else list).filterSearch(query)
|
|
||||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
||||||
|
|
||||||
val selectedBranchValue: String?
|
|
||||||
get() = delegate.selectedBranch.value
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadingJob = doLoad()
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
localStorageChanges
|
|
||||||
.collect { onDownloadComplete(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reload() {
|
|
||||||
loadingJob.cancel()
|
|
||||||
loadingJob = doLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteLocal() {
|
|
||||||
val m = delegate.manga.value
|
|
||||||
if (m == null) {
|
|
||||||
onShowToast.call(R.string.file_not_found)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)?.manga
|
|
||||||
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
|
|
||||||
val original = localMangaRepository.getRemoteManga(manga)
|
|
||||||
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
|
||||||
runCatchingCancellable {
|
|
||||||
historyRepository.deleteOrSwap(manga, original)
|
|
||||||
}
|
|
||||||
onMangaRemoved.emitCall(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeBookmark(bookmark: Bookmark) {
|
|
||||||
launchJob {
|
|
||||||
bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId)
|
|
||||||
onShowToast.call(R.string.bookmark_removed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setChaptersReversed(newValue: Boolean) {
|
|
||||||
settings.chaptersReverse = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedBranch(branch: String?) {
|
|
||||||
delegate.selectedBranch.value = branch
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRemoteManga(): Manga? {
|
|
||||||
return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun performChapterSearch(query: String?) {
|
|
||||||
chaptersQuery.value = query?.trim().orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) {
|
|
||||||
val scrobbler = getScrobbler(index) ?: return
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
scrobbler.updateScrobblingInfo(
|
|
||||||
mangaId = delegate.mangaId,
|
|
||||||
rating = rating,
|
|
||||||
status = status,
|
|
||||||
comment = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unregisterScrobbling(index: Int) {
|
|
||||||
val scrobbler = getScrobbler(index) ?: return
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
scrobbler.unregisterScrobbling(
|
|
||||||
mangaId = delegate.mangaId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markChapterAsCurrent(chapterId: Long) {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
val manga = checkNotNull(delegate.manga.value)
|
|
||||||
val chapters = checkNotNull(manga.getChapters(selectedBranchValue))
|
|
||||||
val chapterIndex = chapters.indexOfFirst { it.id == chapterId }
|
|
||||||
check(chapterIndex in chapters.indices) { "Chapter not found" }
|
|
||||||
val percent = chapterIndex / chapters.size.toFloat()
|
|
||||||
historyRepository.addOrUpdate(manga = manga, chapterId = chapterId, page = 0, scroll = 0, percent = percent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun download(chaptersIds: Set<Long>?) {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
downloadScheduler.schedule(
|
|
||||||
getRemoteManga() ?: checkNotNull(manga.value),
|
|
||||||
chaptersIds,
|
|
||||||
)
|
|
||||||
onDownloadStarted.emitCall(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
|
||||||
delegate.doLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
|
|
||||||
if (query.isEmpty() || this.isEmpty()) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
return filter {
|
|
||||||
it.chapter.name.contains(query, ignoreCase = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
|
|
||||||
downloadedManga ?: return
|
|
||||||
val currentManga = delegate.manga.value ?: return
|
|
||||||
if (currentManga.id != downloadedManga.manga.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (currentManga.source == MangaSource.LOCAL) {
|
|
||||||
reload()
|
|
||||||
} else {
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
|
||||||
runCatchingCancellable {
|
|
||||||
localMangaRepository.getDetails(downloadedManga.manga)
|
|
||||||
}.onSuccess {
|
|
||||||
delegate.relatedManga.value = it
|
|
||||||
}.onFailure {
|
|
||||||
it.printStackTraceDebug()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Spanned.filterSpans(): CharSequence {
|
|
||||||
val spannable = SpannableString.valueOf(this)
|
|
||||||
val spans = spannable.getSpans<ForegroundColorSpan>()
|
|
||||||
for (span in spans) {
|
|
||||||
spannable.removeSpan(span)
|
|
||||||
}
|
|
||||||
return spannable.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getScrobbler(index: Int): Scrobbler? {
|
|
||||||
val info = scrobblingInfo.value?.getOrNull(index)
|
|
||||||
val scrobbler = if (info != null) {
|
|
||||||
scrobblers.find { it.scrobblerService == info.scrobbler && it.isAvailable }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (scrobbler == null) {
|
|
||||||
errorEvent.call(IllegalStateException("Scrobbler [$index] is not available"))
|
|
||||||
}
|
|
||||||
return scrobbler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
|
|
||||||
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
|
|
||||||
|
|
||||||
override fun getItemCount() = 2
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = when (position) {
|
|
||||||
0 -> DetailsFragment()
|
|
||||||
1 -> ChaptersFragment()
|
|
||||||
else -> throw IndexOutOfBoundsException("No fragment for position $position")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
|
||||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ViewModelScoped
|
|
||||||
class MangaDetailsDelegate @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
||||||
) {
|
|
||||||
private val intent = MangaIntent(savedStateHandle)
|
|
||||||
private val mangaData = MutableStateFlow(intent.manga)
|
|
||||||
|
|
||||||
val selectedBranch = MutableStateFlow<String?>(null)
|
|
||||||
|
|
||||||
// Remote manga for saved and saved for remote
|
|
||||||
val relatedManga = MutableStateFlow<Manga?>(null)
|
|
||||||
val manga: StateFlow<Manga?>
|
|
||||||
get() = mangaData
|
|
||||||
val mangaId = intent.manga?.id ?: intent.mangaId
|
|
||||||
|
|
||||||
suspend fun doLoad() {
|
|
||||||
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
|
|
||||||
mangaData.value = manga
|
|
||||||
manga = mangaRepositoryFactory.create(manga.source).getDetails(manga)
|
|
||||||
// find default branch
|
|
||||||
val hist = historyRepository.getOne(manga)
|
|
||||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
|
||||||
mangaData.value = manga
|
|
||||||
relatedManga.value = runCatchingCancellable {
|
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
|
||||||
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
|
|
||||||
mangaRepositoryFactory.create(m.source).getDetails(m)
|
|
||||||
} else {
|
|
||||||
localMangaRepository.findSavedManga(manga)?.manga
|
|
||||||
}
|
|
||||||
}.onFailure { error ->
|
|
||||||
error.printStackTraceDebug()
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapChapters(
|
|
||||||
manga: Manga?,
|
|
||||||
related: Manga?,
|
|
||||||
history: MangaHistory?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val chapters = manga?.chapters ?: return emptyList()
|
|
||||||
val relatedChapters = related?.chapters
|
|
||||||
return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
|
|
||||||
mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
|
||||||
} else {
|
|
||||||
mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapChapters(
|
|
||||||
chapters: List<MangaChapter>,
|
|
||||||
downloadedChapters: List<MangaChapter>?,
|
|
||||||
currentId: Long?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val result = ArrayList<ChapterListItem>(chapters.size)
|
|
||||||
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
|
||||||
val firstNewIndex = chapters.size - newCount
|
|
||||||
val downloadedIds = downloadedChapters?.mapTo(HashSet(downloadedChapters.size)) { it.id }
|
|
||||||
for (i in chapters.indices) {
|
|
||||||
val chapter = chapters[i]
|
|
||||||
if (chapter.branch != branch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += chapter.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = downloadedIds?.contains(chapter.id) == true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (result.size < chapters.size / 2) {
|
|
||||||
result.trimToSize()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapChaptersWithSource(
|
|
||||||
chapters: List<MangaChapter>,
|
|
||||||
sourceChapters: List<MangaChapter>,
|
|
||||||
currentId: Long?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
|
|
||||||
val result = ArrayList<ChapterListItem>(sourceChapters.size)
|
|
||||||
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
|
|
||||||
val firstNewIndex = sourceChapters.size - newCount
|
|
||||||
for (i in sourceChapters.indices) {
|
|
||||||
val chapter = sourceChapters[i]
|
|
||||||
val localChapter = chaptersMap.remove(chapter.id)
|
|
||||||
if (chapter.branch != branch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += localChapter?.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = false,
|
|
||||||
) ?: chapter.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = true,
|
|
||||||
isDownloaded = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
|
|
||||||
result.ensureCapacity(result.size + chaptersMap.size)
|
|
||||||
chaptersMap.values.mapNotNullTo(result) {
|
|
||||||
if (it.branch == branch) {
|
|
||||||
it.toListItem(
|
|
||||||
isCurrent = false,
|
|
||||||
isUnread = true,
|
|
||||||
isNew = false,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = false,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.sortBy { it.chapter.number }
|
|
||||||
}
|
|
||||||
if (result.size < sourceChapters.size / 2) {
|
|
||||||
result.trimToSize()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.adapter
|
|
||||||
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
|
||||||
|
|
||||||
class BranchesAdapter(
|
|
||||||
list: List<MangaBranch>,
|
|
||||||
listener: OnListItemClickListener<MangaBranch>,
|
|
||||||
) : ListDelegationAdapter<List<MangaBranch>>() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(branchAD(listener))
|
|
||||||
items = list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.adapter
|
|
||||||
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
|
||||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
|
||||||
|
|
||||||
fun chapterListItemAD(
|
|
||||||
clickListener: OnListItemClickListener<ChapterListItem>,
|
|
||||||
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
|
|
||||||
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener)
|
|
||||||
itemView.setOnClickListener(eventListener)
|
|
||||||
itemView.setOnLongClickListener(eventListener)
|
|
||||||
|
|
||||||
bind { payloads ->
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
binding.textViewTitle.text = item.chapter.name
|
|
||||||
binding.textViewNumber.text = item.chapter.number.toString()
|
|
||||||
binding.textViewDescription.textAndVisible = item.description()
|
|
||||||
}
|
|
||||||
when (item.status) {
|
|
||||||
FLAG_UNREAD -> {
|
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
|
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(com.google.android.material.R.attr.colorOnTertiary))
|
|
||||||
}
|
|
||||||
FLAG_CURRENT -> {
|
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
|
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
|
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val isMissing = item.hasFlag(FLAG_MISSING)
|
|
||||||
binding.textViewTitle.alpha = if (isMissing) 0.3f else 1f
|
|
||||||
binding.textViewDescription.alpha = if (isMissing) 0.3f else 1f
|
|
||||||
binding.textViewNumber.alpha = if (isMissing) 0.3f else 1f
|
|
||||||
|
|
||||||
binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
|
|
||||||
binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.adapter
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import kotlin.jvm.internal.Intrinsics
|
|
||||||
|
|
||||||
class ChaptersAdapter(
|
|
||||||
onItemClickListener: OnListItemClickListener<ChapterListItem>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()), FastScroller.SectionIndexer {
|
|
||||||
|
|
||||||
init {
|
|
||||||
setHasStableIds(true)
|
|
||||||
delegatesManager.addDelegate(chapterListItemAD(onItemClickListener))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return items[position].chapter.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ChapterListItem>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
|
|
||||||
return oldItem.chapter.id == newItem.chapter.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: ChapterListItem,
|
|
||||||
newItem: ChapterListItem
|
|
||||||
): Boolean {
|
|
||||||
return Intrinsics.areEqual(oldItem, newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
|
|
||||||
if (oldItem.flags != newItem.flags && oldItem.chapter == newItem.chapter) {
|
|
||||||
return newItem.flags
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
|
||||||
val item = items.getOrNull(position) ?: return null
|
|
||||||
return item.chapter.number.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.scrobbling
|
|
||||||
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
|
||||||
|
|
||||||
class ScrollingInfoAdapter(
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
coil: ImageLoader,
|
|
||||||
fragmentManager: FragmentManager,
|
|
||||||
) : AsyncListDifferDelegationAdapter<ScrobblingInfo>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(scrobblingInfoAD(lifecycleOwner, coil, fragmentManager))
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ScrobblingInfo>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Boolean {
|
|
||||||
return oldItem.scrobbler == newItem.scrobbler
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ScrobblingInfo, newItem: ScrobblingInfo): Any {
|
|
||||||
return Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.download.ui.list
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.relatedDateItemAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import kotlin.jvm.internal.Intrinsics
|
|
||||||
|
|
||||||
class DownloadsAdapter(
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
coil: ImageLoader,
|
|
||||||
listener: DownloadItemListener,
|
|
||||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(ITEM_TYPE_DOWNLOAD, downloadItemAD(lifecycleOwner, coil, listener))
|
|
||||||
.addDelegate(loadingStateAD())
|
|
||||||
.addDelegate(emptyStateListAD(coil, lifecycleOwner, null))
|
|
||||||
.addDelegate(relatedDateItemAD())
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
|
|
||||||
|
|
||||||
oldItem is DownloadItemModel && newItem is DownloadItemModel -> {
|
|
||||||
oldItem.id == newItem.id
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
|
|
||||||
oldItem == newItem
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
|
||||||
return Intrinsics.areEqual(oldItem, newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
|
||||||
return when (newItem) {
|
|
||||||
is DownloadItemModel -> {
|
|
||||||
oldItem as DownloadItemModel
|
|
||||||
if (oldItem.workState == newItem.workState) {
|
|
||||||
Unit
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ITEM_TYPE_DOWNLOAD = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.explore.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
|
||||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
|
||||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private const val TIP_SUGGESTIONS = "suggestions"
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ExploreViewModel @Inject constructor(
|
|
||||||
private val settings: AppSettings,
|
|
||||||
private val exploreRepository: ExploreRepository,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
private val gridMode = settings.observeAsStateFlow(
|
|
||||||
key = AppSettings.KEY_SOURCES_GRID,
|
|
||||||
scope = viewModelScope + Dispatchers.IO,
|
|
||||||
valueProducer = { isSourcesGridMode },
|
|
||||||
)
|
|
||||||
|
|
||||||
val onOpenManga = SingleLiveEvent<Manga>()
|
|
||||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
|
||||||
val onShowSuggestionsTip = SingleLiveEvent<Unit>()
|
|
||||||
val isGrid = gridMode.asFlowLiveData(viewModelScope.coroutineContext)
|
|
||||||
|
|
||||||
val content: LiveData<List<ExploreItem>> = isLoading.asFlow().flatMapLatest { loading ->
|
|
||||||
if (loading) {
|
|
||||||
flowOf(listOf(ExploreItem.Loading))
|
|
||||||
} else {
|
|
||||||
createContentFlow()
|
|
||||||
}
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(ExploreItem.Loading))
|
|
||||||
|
|
||||||
init {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
if (!settings.isSuggestionsEnabled && settings.isTipEnabled(TIP_SUGGESTIONS)) {
|
|
||||||
onShowSuggestionsTip.emitCall(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openRandom() {
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
val manga = exploreRepository.findRandomManga(tagsLimit = 8)
|
|
||||||
onOpenManga.emitCall(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideSource(source: MangaSource) {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
settings.hiddenSources += source.name
|
|
||||||
val rollback = ReversibleHandle {
|
|
||||||
settings.hiddenSources -= source.name
|
|
||||||
}
|
|
||||||
onActionDone.emitCall(ReversibleAction(R.string.source_disabled, rollback))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setGridMode(value: Boolean) {
|
|
||||||
settings.isSourcesGridMode = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun respondSuggestionTip(isAccepted: Boolean) {
|
|
||||||
settings.isSuggestionsEnabled = isAccepted
|
|
||||||
settings.closeTip(TIP_SUGGESTIONS)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createContentFlow() = settings.observe()
|
|
||||||
.filter {
|
|
||||||
it == AppSettings.KEY_SOURCES_HIDDEN ||
|
|
||||||
it == AppSettings.KEY_SOURCES_ORDER ||
|
|
||||||
it == AppSettings.KEY_SUGGESTIONS
|
|
||||||
}
|
|
||||||
.onStart { emit("") }
|
|
||||||
.map { settings.getMangaSources(includeHidden = false) }
|
|
||||||
.combine(gridMode) { content, grid -> buildList(content, grid) }
|
|
||||||
|
|
||||||
private fun buildList(sources: List<MangaSource>, isGrid: Boolean): List<ExploreItem> {
|
|
||||||
val result = ArrayList<ExploreItem>(sources.size + 3)
|
|
||||||
result += ExploreItem.Buttons(
|
|
||||||
isSuggestionsEnabled = settings.isSuggestionsEnabled,
|
|
||||||
)
|
|
||||||
result += ExploreItem.Header(R.string.remote_sources, sources.isNotEmpty())
|
|
||||||
if (sources.isNotEmpty()) {
|
|
||||||
sources.mapTo(result) { ExploreItem.Source(it, isGrid) }
|
|
||||||
} else {
|
|
||||||
result += ExploreItem.EmptyHint(
|
|
||||||
icon = R.drawable.ic_empty_common,
|
|
||||||
textPrimary = R.string.no_manga_sources,
|
|
||||||
textSecondary = R.string.no_manga_sources_text,
|
|
||||||
actionStringRes = R.string.manage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.explore.ui.adapter
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
|
||||||
|
|
||||||
class ExploreAdapter(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
listener: ExploreListEventListener,
|
|
||||||
clickListener: OnListItemClickListener<ExploreItem.Source>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<ExploreItem>(ExploreDiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager
|
|
||||||
.addDelegate(ITEM_TYPE_BUTTONS, exploreButtonsAD(listener))
|
|
||||||
.addDelegate(ITEM_TYPE_HEADER, exploreSourcesHeaderAD(listener))
|
|
||||||
.addDelegate(ITEM_TYPE_SOURCE_LIST, exploreSourceListItemAD(coil, clickListener, lifecycleOwner))
|
|
||||||
.addDelegate(ITEM_TYPE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
|
|
||||||
.addDelegate(ITEM_TYPE_HINT, exploreEmptyHintListAD(listener))
|
|
||||||
.addDelegate(ITEM_TYPE_LOADING, exploreLoadingAD())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val ITEM_TYPE_BUTTONS = 0
|
|
||||||
const val ITEM_TYPE_HEADER = 1
|
|
||||||
const val ITEM_TYPE_SOURCE_LIST = 2
|
|
||||||
const val ITEM_TYPE_SOURCE_GRID = 3
|
|
||||||
const val ITEM_TYPE_HINT = 4
|
|
||||||
const val ITEM_TYPE_LOADING = 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.explore.ui.adapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemExploreSourceListBinding
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
|
|
||||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
|
||||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
|
||||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
|
||||||
import org.koitharu.kotatsu.utils.ext.source
|
|
||||||
import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable
|
|
||||||
|
|
||||||
fun exploreButtonsAD(
|
|
||||||
clickListener: View.OnClickListener,
|
|
||||||
) = adapterDelegateViewBinding<ExploreItem.Buttons, ExploreItem, ItemExploreButtonsBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemExploreButtonsBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
|
|
||||||
binding.buttonBookmarks.setOnClickListener(clickListener)
|
|
||||||
binding.buttonHistory.setOnClickListener(clickListener)
|
|
||||||
binding.buttonLocal.setOnClickListener(clickListener)
|
|
||||||
binding.buttonSuggestions.setOnClickListener(clickListener)
|
|
||||||
binding.buttonFavourites.setOnClickListener(clickListener)
|
|
||||||
binding.buttonRandom.setOnClickListener(clickListener)
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.buttonSuggestions.isVisible = item.isSuggestionsEnabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exploreSourcesHeaderAD(
|
|
||||||
listener: ExploreListEventListener,
|
|
||||||
) = adapterDelegateViewBinding<ExploreItem.Header, ExploreItem, ItemHeaderButtonBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemHeaderButtonBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
|
|
||||||
val listenerAdapter = View.OnClickListener {
|
|
||||||
listener.onManageClick(itemView)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.buttonMore.setOnClickListener(listenerAdapter)
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.textViewTitle.setText(item.titleResId)
|
|
||||||
binding.buttonMore.isVisible = item.isButtonVisible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exploreSourceListItemAD(
|
|
||||||
coil: ImageLoader,
|
|
||||||
listener: OnListItemClickListener<ExploreItem.Source>,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceListBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemExploreSourceListBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
on = { item, _, _ -> item is ExploreItem.Source && !item.isGrid },
|
|
||||||
) {
|
|
||||||
|
|
||||||
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
|
|
||||||
|
|
||||||
binding.root.setOnClickListener(eventListener)
|
|
||||||
binding.root.setOnLongClickListener(eventListener)
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.textViewTitle.text = item.source.title
|
|
||||||
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
|
|
||||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
|
||||||
fallback(fallbackIcon)
|
|
||||||
placeholder(fallbackIcon)
|
|
||||||
error(fallbackIcon)
|
|
||||||
source(item.source)
|
|
||||||
enqueueWith(coil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewRecycled {
|
|
||||||
binding.imageViewIcon.disposeImageRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exploreSourceGridItemAD(
|
|
||||||
coil: ImageLoader,
|
|
||||||
listener: OnListItemClickListener<ExploreItem.Source>,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceGridBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemExploreSourceGridBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
on = { item, _, _ -> item is ExploreItem.Source && item.isGrid },
|
|
||||||
) {
|
|
||||||
|
|
||||||
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
|
|
||||||
|
|
||||||
binding.root.setOnClickListener(eventListener)
|
|
||||||
binding.root.setOnLongClickListener(eventListener)
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.textViewTitle.text = item.source.title
|
|
||||||
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
|
|
||||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
|
||||||
fallback(fallbackIcon)
|
|
||||||
placeholder(fallbackIcon)
|
|
||||||
error(fallbackIcon)
|
|
||||||
source(item.source)
|
|
||||||
enqueueWith(coil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewRecycled {
|
|
||||||
binding.imageViewIcon.disposeImageRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exploreEmptyHintListAD(
|
|
||||||
listener: ListStateHolderListener,
|
|
||||||
) = adapterDelegateViewBinding<ExploreItem.EmptyHint, ExploreItem, ItemEmptyCardBinding>(
|
|
||||||
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
|
|
||||||
) {
|
|
||||||
|
|
||||||
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.icon.setImageResource(item.icon)
|
|
||||||
binding.textPrimary.setText(item.textPrimary)
|
|
||||||
binding.textSecondary.setTextAndVisible(item.textSecondary)
|
|
||||||
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exploreLoadingAD() = adapterDelegate<ExploreItem.Loading, ExploreItem>(R.layout.item_loading_state) {}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.explore.ui.adapter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
|
||||||
|
|
||||||
class ExploreDiffCallback : DiffUtil.ItemCallback<ExploreItem>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem.javaClass != newItem.javaClass -> false
|
|
||||||
oldItem is ExploreItem.Buttons && newItem is ExploreItem.Buttons -> true
|
|
||||||
oldItem is ExploreItem.Loading && newItem is ExploreItem.Loading -> true
|
|
||||||
oldItem is ExploreItem.EmptyHint && newItem is ExploreItem.EmptyHint -> true
|
|
||||||
oldItem is ExploreItem.Source && newItem is ExploreItem.Source -> {
|
|
||||||
oldItem.source == newItem.source && oldItem.isGrid == newItem.isGrid
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is ExploreItem.Header && newItem is ExploreItem.Header -> {
|
|
||||||
oldItem.titleResId == newItem.titleResId
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.explore.ui.model
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
sealed interface ExploreItem : ListModel {
|
|
||||||
|
|
||||||
class Buttons(
|
|
||||||
val isSuggestionsEnabled: Boolean
|
|
||||||
) : ExploreItem {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Buttons
|
|
||||||
|
|
||||||
if (isSuggestionsEnabled != other.isSuggestionsEnabled) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return isSuggestionsEnabled.hashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Header(
|
|
||||||
@StringRes val titleResId: Int,
|
|
||||||
val isButtonVisible: Boolean,
|
|
||||||
) : ExploreItem {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Header
|
|
||||||
|
|
||||||
if (titleResId != other.titleResId) return false
|
|
||||||
if (isButtonVisible != other.isButtonVisible) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = titleResId
|
|
||||||
result = 31 * result + isButtonVisible.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Source(
|
|
||||||
val source: MangaSource,
|
|
||||||
val isGrid: Boolean,
|
|
||||||
) : ExploreItem {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Source
|
|
||||||
|
|
||||||
if (source != other.source) return false
|
|
||||||
if (isGrid != other.isGrid) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = source.hashCode()
|
|
||||||
result = 31 * result + isGrid.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
class EmptyHint(
|
|
||||||
@DrawableRes icon: Int,
|
|
||||||
@StringRes textPrimary: Int,
|
|
||||||
@StringRes textSecondary: Int,
|
|
||||||
@StringRes actionStringRes: Int,
|
|
||||||
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
|
|
||||||
|
|
||||||
object Loading : ExploreItem {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean = other === Loading
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories
|
|
||||||
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
|
||||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
|
||||||
import org.koitharu.kotatsu.utils.ext.requireValue
|
|
||||||
import java.util.Collections
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class FavouritesCategoriesViewModel @Inject constructor(
|
|
||||||
private val repository: FavouritesRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
private var reorderJob: Job? = null
|
|
||||||
private val isReorder = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val isInReorderMode = isReorder.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
|
|
||||||
val allCategories = repository.observeCategories()
|
|
||||||
.mapItems {
|
|
||||||
CategoryListModel(
|
|
||||||
mangaCount = 0,
|
|
||||||
covers = listOf(),
|
|
||||||
category = it,
|
|
||||||
isReorderMode = false,
|
|
||||||
)
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
|
||||||
|
|
||||||
val detalizedCategories = combine(
|
|
||||||
repository.observeCategoriesWithCovers(),
|
|
||||||
isReorder,
|
|
||||||
) { list, reordering ->
|
|
||||||
list.map { (category, covers) ->
|
|
||||||
CategoryListModel(
|
|
||||||
mangaCount = covers.size,
|
|
||||||
covers = covers.take(3),
|
|
||||||
category = category,
|
|
||||||
isReorderMode = reordering,
|
|
||||||
)
|
|
||||||
}.ifEmpty {
|
|
||||||
listOf(
|
|
||||||
EmptyState(
|
|
||||||
icon = R.drawable.ic_empty_favourites,
|
|
||||||
textPrimary = R.string.text_empty_holder_primary,
|
|
||||||
textSecondary = R.string.empty_favourite_categories,
|
|
||||||
actionStringRes = 0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
|
||||||
|
|
||||||
fun deleteCategory(id: Long) {
|
|
||||||
launchJob {
|
|
||||||
repository.removeCategory(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteCategories(ids: Set<Long>) {
|
|
||||||
launchJob {
|
|
||||||
repository.removeCategories(ids)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAllCategoriesVisible(isVisible: Boolean) {
|
|
||||||
settings.isAllFavouritesVisible = isVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isInReorderMode(): Boolean = isReorder.value
|
|
||||||
|
|
||||||
fun isEmpty(): Boolean = detalizedCategories.value?.none { it is CategoryListModel } ?: true
|
|
||||||
|
|
||||||
fun setReorderMode(isReorderMode: Boolean) {
|
|
||||||
isReorder.value = isReorderMode
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reorderCategories(oldPos: Int, newPos: Int) {
|
|
||||||
val prevJob = reorderJob
|
|
||||||
reorderJob = launchJob(Dispatchers.Default) {
|
|
||||||
prevJob?.join()
|
|
||||||
val items = detalizedCategories.requireValue()
|
|
||||||
val ids = items.mapNotNullTo(ArrayList(items.size)) {
|
|
||||||
(it as? CategoryListModel)?.category?.id
|
|
||||||
}
|
|
||||||
Collections.swap(ids, oldPos, newPos)
|
|
||||||
ids.remove(0L)
|
|
||||||
repository.reorderCategories(ids)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.adapter
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import kotlin.jvm.internal.Intrinsics
|
|
||||||
|
|
||||||
class CategoriesAdapter(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
onItemClickListener: FavouriteCategoriesListListener,
|
|
||||||
listListener: ListStateHolderListener,
|
|
||||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(categoryAD(coil, lifecycleOwner, onItemClickListener))
|
|
||||||
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listListener))
|
|
||||||
.addDelegate(loadingStateAD())
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem is CategoryListModel && newItem is CategoryListModel -> {
|
|
||||||
oldItem.category.id == newItem.category.id
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
|
||||||
return Intrinsics.areEqual(oldItem, newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
|
||||||
return when {
|
|
||||||
oldItem is CategoryListModel && newItem is CategoryListModel -> {
|
|
||||||
if (oldItem.category == newItem.category &&
|
|
||||||
oldItem.mangaCount == newItem.mangaCount &&
|
|
||||||
oldItem.covers == newItem.covers &&
|
|
||||||
oldItem.isReorderMode != newItem.isReorderMode
|
|
||||||
) {
|
|
||||||
Unit
|
|
||||||
} else {
|
|
||||||
super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
|
||||||
|
|
||||||
class MangaCategoriesAdapter(
|
|
||||||
clickListener: OnListItemClickListener<MangaCategoryItem>
|
|
||||||
) : AsyncListDifferDelegationAdapter<MangaCategoryItem>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(mangaCategoryAD(clickListener))
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<MangaCategoryItem>() {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: MangaCategoryItem,
|
|
||||||
newItem: MangaCategoryItem
|
|
||||||
): Boolean = oldItem.id == newItem.id
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: MangaCategoryItem,
|
|
||||||
newItem: MangaCategoryItem
|
|
||||||
): Boolean = oldItem == newItem
|
|
||||||
|
|
||||||
override fun getChangePayload(
|
|
||||||
oldItem: MangaCategoryItem,
|
|
||||||
newItem: MangaCategoryItem
|
|
||||||
): Any? {
|
|
||||||
if (oldItem.isChecked != newItem.isChecked) {
|
|
||||||
return newItem.isChecked
|
|
||||||
}
|
|
||||||
return super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
|
|
||||||
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
|
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
|
||||||
|
|
||||||
fun mangaCategoryAD(
|
|
||||||
clickListener: OnListItemClickListener<MangaCategoryItem>
|
|
||||||
) = adapterDelegateViewBinding<MangaCategoryItem, MangaCategoryItem, ItemCheckableNewBinding>(
|
|
||||||
{ inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
clickListener.onItemClick(item, itemView)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
with(binding.root) {
|
|
||||||
text = item.name
|
|
||||||
isChecked = item.isChecked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.select.model
|
|
||||||
|
|
||||||
data class MangaCategoryItem(
|
|
||||||
val id: Long,
|
|
||||||
val name: String,
|
|
||||||
val isChecked: Boolean
|
|
||||||
)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.history.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.core.view.MenuProvider
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
|
|
||||||
class HistoryListMenuProvider(
|
|
||||||
private val context: Context,
|
|
||||||
private val viewModel: HistoryListViewModel,
|
|
||||||
) : MenuProvider {
|
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
|
||||||
menuInflater.inflate(R.menu.opt_history, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
|
||||||
R.id.action_clear_history -> {
|
|
||||||
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
|
|
||||||
.setTitle(R.string.clear_history)
|
|
||||||
.setMessage(R.string.text_clear_history_prompt)
|
|
||||||
.setIcon(R.drawable.ic_delete)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setPositiveButton(R.string.clear) { _, _ ->
|
|
||||||
viewModel.clearHistory()
|
|
||||||
}.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_history_grouping -> {
|
|
||||||
viewModel.setGrouping(!menuItem.isChecked)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareMenu(menu: Menu) {
|
|
||||||
menu.findItem(R.id.action_history_grouping)?.isChecked = viewModel.isGroupingEnabled.value == true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.history.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
|
||||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
|
||||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
|
||||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
|
||||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class HistoryListViewModel @Inject constructor(
|
|
||||||
private val repository: HistoryRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
private val trackingRepository: TrackingRepository,
|
|
||||||
private val tagHighlighter: MangaTagHighlighter,
|
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
|
||||||
) : MangaListViewModel(settings, downloadScheduler) {
|
|
||||||
|
|
||||||
val isGroupingEnabled = MutableLiveData<Boolean>()
|
|
||||||
|
|
||||||
private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { isHistoryGroupingEnabled }
|
|
||||||
.onEach { isGroupingEnabled.emitValue(it) }
|
|
||||||
|
|
||||||
override val content = combine(
|
|
||||||
repository.observeAllWithHistory(),
|
|
||||||
historyGrouping,
|
|
||||||
listModeFlow,
|
|
||||||
) { list, grouped, mode ->
|
|
||||||
when {
|
|
||||||
list.isEmpty() -> listOf(
|
|
||||||
EmptyState(
|
|
||||||
icon = R.drawable.ic_empty_history,
|
|
||||||
textPrimary = R.string.text_history_holder_primary,
|
|
||||||
textSecondary = R.string.text_history_holder_secondary,
|
|
||||||
actionStringRes = 0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> mapList(list, grouped, mode)
|
|
||||||
}
|
|
||||||
}.onStart {
|
|
||||||
loadingCounter.increment()
|
|
||||||
}.onFirst {
|
|
||||||
loadingCounter.decrement()
|
|
||||||
}.catch {
|
|
||||||
emit(listOf(it.toErrorState(canRetry = false)))
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
|
||||||
|
|
||||||
override fun onRefresh() = Unit
|
|
||||||
|
|
||||||
override fun onRetry() = Unit
|
|
||||||
|
|
||||||
fun clearHistory() {
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
repository.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeFromHistory(ids: Set<Long>) {
|
|
||||||
if (ids.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
val handle = repository.delete(ids)
|
|
||||||
onActionDone.emitCall(ReversibleAction(R.string.removed_from_history, handle))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setGrouping(isGroupingEnabled: Boolean) {
|
|
||||||
settings.isHistoryGroupingEnabled = isGroupingEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun mapList(
|
|
||||||
list: List<MangaWithHistory>,
|
|
||||||
grouped: Boolean,
|
|
||||||
mode: ListMode,
|
|
||||||
): List<ListModel> {
|
|
||||||
val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 1)
|
|
||||||
val showPercent = settings.isReadingIndicatorsEnabled
|
|
||||||
var prevDate: DateTimeAgo? = null
|
|
||||||
for ((manga, history) in list) {
|
|
||||||
if (grouped) {
|
|
||||||
val date = timeAgo(history.updatedAt)
|
|
||||||
if (prevDate != date) {
|
|
||||||
result += 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) {
|
|
||||||
ListMode.LIST -> manga.toListModel(counter, percent)
|
|
||||||
ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter, percent, tagHighlighter)
|
|
||||||
ListMode.GRID -> manga.toGridModel(counter, percent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun timeAgo(date: Date): DateTimeAgo {
|
|
||||||
val diff = (System.currentTimeMillis() - date.time).coerceAtLeast(0L)
|
|
||||||
val diffMinutes = TimeUnit.MILLISECONDS.toMinutes(diff).toInt()
|
|
||||||
val diffDays = -date.daysDiff(System.currentTimeMillis())
|
|
||||||
return when {
|
|
||||||
diffMinutes < 3 -> DateTimeAgo.JustNow
|
|
||||||
diffDays < 1 -> DateTimeAgo.Today
|
|
||||||
diffDays == 1 -> DateTimeAgo.Yesterday
|
|
||||||
diffDays < 6 -> DateTimeAgo.DaysAgo(diffDays)
|
|
||||||
else -> DateTimeAgo.LongAgo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.domain
|
|
||||||
|
|
||||||
interface ListExtraProvider {
|
|
||||||
|
|
||||||
suspend fun getCounter(mangaId: Long): Int
|
|
||||||
|
|
||||||
suspend fun getProgress(mangaId: Long): Float
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.adapter
|
|
||||||
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.core.ui.titleRes
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemHeader2Binding
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled
|
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
|
||||||
|
|
||||||
fun listHeader2AD(
|
|
||||||
listener: MangaListListener,
|
|
||||||
) = adapterDelegateViewBinding<ListHeader2, ListModel, ItemHeader2Binding>(
|
|
||||||
{ layoutInflater, parent -> ItemHeader2Binding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
|
|
||||||
var ignoreChecking = false
|
|
||||||
binding.textViewFilter.setOnClickListener {
|
|
||||||
listener.onFilterClick(it)
|
|
||||||
}
|
|
||||||
binding.chipsTags.setOnCheckedStateChangeListener { _, _ ->
|
|
||||||
if (!ignoreChecking) {
|
|
||||||
listener.onUpdateFilter(binding.chipsTags.getCheckedData(MangaTag::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bind { payloads ->
|
|
||||||
if (payloads.isNotEmpty()) {
|
|
||||||
if (context.isAnimationsEnabled) {
|
|
||||||
binding.scrollView.smoothScrollTo(0, 0)
|
|
||||||
} else {
|
|
||||||
binding.scrollView.scrollTo(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ignoreChecking = true
|
|
||||||
binding.chipsTags.setChips(item.chips) // TODO use recyclerview
|
|
||||||
ignoreChecking = false
|
|
||||||
binding.textViewFilter.setTextAndVisible(item.sortOrder?.titleRes ?: 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.adapter
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
|
||||||
import kotlin.jvm.internal.Intrinsics
|
|
||||||
|
|
||||||
open class MangaListAdapter(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
listener: MangaListListener,
|
|
||||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager
|
|
||||||
.addDelegate(ITEM_TYPE_MANGA_LIST, mangaListItemAD(coil, lifecycleOwner, listener))
|
|
||||||
.addDelegate(ITEM_TYPE_MANGA_LIST_DETAILED, mangaListDetailedItemAD(coil, lifecycleOwner, listener))
|
|
||||||
.addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener, null))
|
|
||||||
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
|
|
||||||
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
|
|
||||||
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
|
|
||||||
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
|
|
||||||
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
|
|
||||||
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
|
||||||
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD(listener))
|
|
||||||
.addDelegate(ITEM_TYPE_HEADER_2, listHeader2AD(listener))
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
|
|
||||||
oldItem is MangaListModel && newItem is MangaListModel -> {
|
|
||||||
oldItem.id == newItem.id
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is MangaListDetailedModel && newItem is MangaListDetailedModel -> {
|
|
||||||
oldItem.id == newItem.id
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is MangaGridModel && newItem is MangaGridModel -> {
|
|
||||||
oldItem.id == newItem.id
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
|
|
||||||
oldItem == newItem
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is ListHeader && newItem is ListHeader -> {
|
|
||||||
oldItem.textRes == newItem.textRes &&
|
|
||||||
oldItem.text == newItem.text &&
|
|
||||||
oldItem.dateTimeAgo == newItem.dateTimeAgo
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
|
||||||
return Intrinsics.areEqual(oldItem, newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
|
||||||
return when (newItem) {
|
|
||||||
is MangaItemModel -> {
|
|
||||||
oldItem as MangaItemModel
|
|
||||||
if (oldItem.progress != newItem.progress) {
|
|
||||||
PAYLOAD_PROGRESS
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is ListHeader2 -> Unit
|
|
||||||
else -> super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val ITEM_TYPE_MANGA_LIST = 0
|
|
||||||
const val ITEM_TYPE_MANGA_LIST_DETAILED = 1
|
|
||||||
const val ITEM_TYPE_MANGA_GRID = 2
|
|
||||||
const val ITEM_TYPE_LOADING_FOOTER = 3
|
|
||||||
const val ITEM_TYPE_LOADING_STATE = 4
|
|
||||||
const val ITEM_TYPE_DATE = 5
|
|
||||||
const val ITEM_TYPE_ERROR_STATE = 6
|
|
||||||
const val ITEM_TYPE_ERROR_FOOTER = 7
|
|
||||||
const val ITEM_TYPE_EMPTY = 8
|
|
||||||
const val ITEM_TYPE_HEADER = 9
|
|
||||||
const val ITEM_TYPE_HEADER_2 = 10
|
|
||||||
|
|
||||||
val PAYLOAD_PROGRESS = Any()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.adapter
|
|
||||||
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
|
|
||||||
fun relatedDateItemAD() = adapterDelegate<DateTimeAgo, ListModel>(R.layout.item_header) {
|
|
||||||
|
|
||||||
bind {
|
|
||||||
(itemView as TextView).text = item.format(context.resources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
|
|
||||||
class FilterAdapter(
|
|
||||||
listener: OnFilterChangedListener,
|
|
||||||
listListener: ListListener<FilterItem>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<FilterItem>(
|
|
||||||
FilterDiffCallback(),
|
|
||||||
filterSortDelegate(listener),
|
|
||||||
filterTagDelegate(listener),
|
|
||||||
filterHeaderDelegate(),
|
|
||||||
filterLoadingDelegate(),
|
|
||||||
filterErrorDelegate(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
differ.addListListener(listListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.ui.titleRes
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
|
|
||||||
|
|
||||||
fun filterSortDelegate(
|
|
||||||
listener: OnFilterChangedListener,
|
|
||||||
) = adapterDelegateViewBinding<FilterItem.Sort, FilterItem, ItemCheckableNewBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
listener.onSortItemClick(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.root.setText(item.order.titleRes)
|
|
||||||
binding.root.isChecked = item.isSelected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterTagDelegate(
|
|
||||||
listener: OnFilterChangedListener,
|
|
||||||
) = adapterDelegateViewBinding<FilterItem.Tag, FilterItem, ItemCheckableNewBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
listener.onTagItemClick(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.root.text = item.tag.title
|
|
||||||
binding.root.isChecked = item.isChecked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterHeaderDelegate() = adapterDelegateViewBinding<FilterItem.Header, FilterItem, ItemFilterHeaderBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.textViewTitle.setText(item.titleResId)
|
|
||||||
binding.badge.isVisible = if (item.counter == 0) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
binding.badge.text = item.counter.toString()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterLoadingDelegate() = adapterDelegate<FilterItem.Loading, FilterItem>(R.layout.item_loading_footer) {}
|
|
||||||
|
|
||||||
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, FilterItem>(R.layout.item_sources_empty) {
|
|
||||||
|
|
||||||
bind {
|
|
||||||
(itemView as TextView).setText(item.textResId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.CollapseActionViewCallback
|
|
||||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
|
||||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
|
||||||
import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels
|
|
||||||
|
|
||||||
class FilterBottomSheet :
|
|
||||||
BaseBottomSheet<SheetFilterBinding>(),
|
|
||||||
MenuItem.OnActionExpandListener,
|
|
||||||
SearchView.OnQueryTextListener,
|
|
||||||
AsyncListDiffer.ListListener<FilterItem> {
|
|
||||||
|
|
||||||
private val viewModel by parentFragmentViewModels<RemoteListViewModel>()
|
|
||||||
private var collapsibleActionViewCallback: CollapseActionViewCallback? = null
|
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
|
||||||
return SheetFilterBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
val adapter = FilterAdapter(viewModel, this)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems)
|
|
||||||
initOptionsMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
collapsibleActionViewCallback = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
|
||||||
setExpanded(isExpanded = true, isLocked = true)
|
|
||||||
collapsibleActionViewCallback?.onMenuItemActionExpand(item)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
|
||||||
val searchView = (item.actionView as? SearchView) ?: return false
|
|
||||||
searchView.setQuery("", false)
|
|
||||||
searchView.post { setExpanded(isExpanded = false, isLocked = false) }
|
|
||||||
collapsibleActionViewCallback?.onMenuItemActionCollapse(item)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
viewModel.filterSearch(newText?.trim().orEmpty())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCurrentListChanged(previousList: MutableList<FilterItem>, currentList: MutableList<FilterItem>) {
|
|
||||||
if (currentList.size > previousList.size && view != null) {
|
|
||||||
(binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initOptionsMenu() {
|
|
||||||
binding.headerBar.inflateMenu(R.menu.opt_filter)
|
|
||||||
val searchMenuItem = binding.headerBar.menu.findItem(R.id.action_search)
|
|
||||||
searchMenuItem.setOnActionExpandListener(this)
|
|
||||||
val searchView = searchMenuItem.actionView as SearchView
|
|
||||||
searchView.setOnQueryTextListener(this)
|
|
||||||
searchView.setIconifiedByDefault(false)
|
|
||||||
searchView.queryHint = searchMenuItem.title
|
|
||||||
collapsibleActionViewCallback = CollapseActionViewCallback(searchMenuItem).also {
|
|
||||||
onBackPressedDispatcher.addCallback(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "FilterBottomSheet"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = FilterBottomSheet().show(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
|
|
||||||
class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem === newItem -> true
|
|
||||||
oldItem.javaClass != newItem.javaClass -> false
|
|
||||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
|
||||||
oldItem.titleResId == newItem.titleResId
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
|
||||||
oldItem.tag == newItem.tag
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
|
||||||
oldItem.order == newItem.order
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Error && newItem is FilterItem.Error -> {
|
|
||||||
oldItem.textResId == newItem.textResId
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true
|
|
||||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
|
||||||
oldItem.counter == newItem.counter
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Error && newItem is FilterItem.Error -> true
|
|
||||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
|
||||||
oldItem.isChecked == newItem.isChecked
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
|
||||||
oldItem.isSelected == newItem.isSelected
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: FilterItem, newItem: FilterItem): Any? {
|
|
||||||
val hasPayload = when {
|
|
||||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
|
||||||
oldItem.isChecked != newItem.isChecked
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
|
||||||
oldItem.isSelected != newItem.isSelected
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
|
||||||
oldItem.counter != newItem.counter
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
|
|
||||||
sealed interface FilterItem {
|
|
||||||
|
|
||||||
class Header(
|
|
||||||
@StringRes val titleResId: Int,
|
|
||||||
val counter: Int,
|
|
||||||
) : FilterItem
|
|
||||||
|
|
||||||
class Sort(
|
|
||||||
val order: SortOrder,
|
|
||||||
val isSelected: Boolean,
|
|
||||||
) : FilterItem
|
|
||||||
|
|
||||||
class Tag(
|
|
||||||
val tag: MangaTag,
|
|
||||||
val isChecked: Boolean,
|
|
||||||
) : FilterItem
|
|
||||||
|
|
||||||
object Loading : FilterItem
|
|
||||||
|
|
||||||
class Error(
|
|
||||||
@StringRes val textResId: Int,
|
|
||||||
) : FilterItem
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
interface OnFilterChangedListener {
|
|
||||||
|
|
||||||
fun onSortItemClick(item: FilterItem.Sort)
|
|
||||||
|
|
||||||
fun onTagItemClick(item: FilterItem.Tag)
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
|
|
||||||
class EmptyHint(
|
|
||||||
@DrawableRes icon: Int,
|
|
||||||
@StringRes textPrimary: Int,
|
|
||||||
@StringRes textSecondary: Int,
|
|
||||||
@StringRes actionStringRes: Int,
|
|
||||||
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes) {
|
|
||||||
|
|
||||||
fun toState() = EmptyState(icon, textPrimary, textSecondary, actionStringRes)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
|
|
||||||
open class EmptyState(
|
|
||||||
@DrawableRes val icon: Int,
|
|
||||||
@StringRes val textPrimary: Int,
|
|
||||||
@StringRes val textSecondary: Int,
|
|
||||||
@StringRes val actionStringRes: Int,
|
|
||||||
) : ListModel {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as EmptyState
|
|
||||||
|
|
||||||
if (icon != other.icon) return false
|
|
||||||
if (textPrimary != other.textPrimary) return false
|
|
||||||
if (textSecondary != other.textSecondary) return false
|
|
||||||
if (actionStringRes != other.actionStringRes) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = icon
|
|
||||||
result = 31 * result + textPrimary
|
|
||||||
result = 31 * result + textSecondary
|
|
||||||
result = 31 * result + actionStringRes
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
|
||||||
|
|
||||||
class ListHeader private constructor(
|
|
||||||
val text: CharSequence?,
|
|
||||||
@StringRes val textRes: Int,
|
|
||||||
val dateTimeAgo: DateTimeAgo?,
|
|
||||||
@StringRes val buttonTextRes: Int,
|
|
||||||
val payload: Any?,
|
|
||||||
) : ListModel {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
text: CharSequence,
|
|
||||||
@StringRes buttonTextRes: Int,
|
|
||||||
payload: Any?,
|
|
||||||
) : this(text, 0, null, buttonTextRes, payload)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@StringRes textRes: Int,
|
|
||||||
@StringRes buttonTextRes: Int,
|
|
||||||
payload: Any?,
|
|
||||||
) : this(null, textRes, null, buttonTextRes, payload)
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
dateTimeAgo: DateTimeAgo,
|
|
||||||
@StringRes buttonTextRes: Int,
|
|
||||||
payload: Any?,
|
|
||||||
) : this(null, 0, dateTimeAgo, buttonTextRes, payload)
|
|
||||||
|
|
||||||
fun getText(context: Context): CharSequence? = when {
|
|
||||||
text != null -> text
|
|
||||||
textRes != 0 -> context.getString(textRes)
|
|
||||||
else -> dateTimeAgo?.format(context.resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ListHeader
|
|
||||||
|
|
||||||
if (text != other.text) return false
|
|
||||||
if (textRes != other.textRes) return false
|
|
||||||
if (dateTimeAgo != other.dateTimeAgo) return false
|
|
||||||
if (buttonTextRes != other.buttonTextRes) return false
|
|
||||||
if (payload != other.payload) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = text?.hashCode() ?: 0
|
|
||||||
result = 31 * result + textRes
|
|
||||||
result = 31 * result + (dateTimeAgo?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + buttonTextRes
|
|
||||||
result = 31 * result + (payload?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
|
||||||
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.history.domain.PROGRESS_NONE
|
|
||||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.utils.ext.ifZero
|
|
||||||
import java.net.SocketTimeoutException
|
|
||||||
import java.net.UnknownHostException
|
|
||||||
|
|
||||||
fun Manga.toListModel(
|
|
||||||
counter: Int,
|
|
||||||
progress: Float,
|
|
||||||
) = MangaListModel(
|
|
||||||
id = id,
|
|
||||||
title = title,
|
|
||||||
subtitle = tags.joinToString(", ") { it.title },
|
|
||||||
coverUrl = coverUrl,
|
|
||||||
manga = this,
|
|
||||||
counter = counter,
|
|
||||||
progress = progress,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Manga.toListDetailedModel(
|
|
||||||
counter: Int,
|
|
||||||
progress: Float,
|
|
||||||
tagHighlighter: MangaTagHighlighter?,
|
|
||||||
) = MangaListDetailedModel(
|
|
||||||
id = id,
|
|
||||||
title = title,
|
|
||||||
subtitle = altTitle,
|
|
||||||
coverUrl = coverUrl,
|
|
||||||
manga = this,
|
|
||||||
counter = counter,
|
|
||||||
progress = progress,
|
|
||||||
tags = tags.map {
|
|
||||||
ChipsView.ChipModel(
|
|
||||||
tint = tagHighlighter?.getTint(it) ?: 0,
|
|
||||||
title = it.title,
|
|
||||||
isCheckable = false,
|
|
||||||
isChecked = false,
|
|
||||||
data = it,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Manga.toGridModel(counter: Int, progress: Float) = MangaGridModel(
|
|
||||||
id = id,
|
|
||||||
title = title,
|
|
||||||
coverUrl = coverUrl,
|
|
||||||
manga = this,
|
|
||||||
counter = counter,
|
|
||||||
progress = progress,
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun List<Manga>.toUi(
|
|
||||||
mode: ListMode,
|
|
||||||
extraProvider: ListExtraProvider,
|
|
||||||
tagHighlighter: MangaTagHighlighter?,
|
|
||||||
): 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(
|
|
||||||
destination: C,
|
|
||||||
mode: ListMode,
|
|
||||||
extraProvider: ListExtraProvider,
|
|
||||||
tagHighlighter: MangaTagHighlighter?,
|
|
||||||
): C = when (mode) {
|
|
||||||
ListMode.LIST -> mapTo(destination) {
|
|
||||||
it.toListModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
exception = this,
|
|
||||||
icon = getErrorIcon(this),
|
|
||||||
canRetry = canRetry,
|
|
||||||
buttonText = ExceptionResolver.getResolveStringId(this).ifZero { R.string.try_again },
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Throwable.toErrorFooter() = ErrorFooter(
|
|
||||||
exception = this,
|
|
||||||
icon = R.drawable.ic_alert_outline,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getErrorIcon(error: Throwable) = when (error) {
|
|
||||||
is AuthRequiredException -> R.drawable.ic_auth_key_large
|
|
||||||
is CloudFlareProtectedException -> R.drawable.ic_bot_large
|
|
||||||
is UnknownHostException,
|
|
||||||
is SocketTimeoutException,
|
|
||||||
-> R.drawable.ic_plug_large
|
|
||||||
|
|
||||||
else -> R.drawable.ic_error_large
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
|
||||||
|
|
||||||
object LoadingFooter : ListModel {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean = other === LoadingFooter
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
sealed interface MangaItemModel : ListModel {
|
|
||||||
|
|
||||||
val id: Long
|
|
||||||
val manga: Manga
|
|
||||||
val title: String
|
|
||||||
val coverUrl: String
|
|
||||||
val counter: Int
|
|
||||||
val progress: Float
|
|
||||||
|
|
||||||
val source: MangaSource
|
|
||||||
get() = manga.source
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.local.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
|
||||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalManga
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.LinkedList
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class LocalListViewModel @Inject constructor(
|
|
||||||
private val repository: LocalMangaRepository,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val trackingRepository: TrackingRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
private val tagHighlighter: MangaTagHighlighter,
|
|
||||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
|
||||||
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider {
|
|
||||||
|
|
||||||
val onMangaRemoved = SingleLiveEvent<Unit>()
|
|
||||||
val sortOrder = MutableLiveData(settings.localListOrder)
|
|
||||||
private val listError = MutableStateFlow<Throwable?>(null)
|
|
||||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
|
||||||
private val selectedTags = MutableStateFlow<Set<MangaTag>>(emptySet())
|
|
||||||
private var refreshJob: Job? = null
|
|
||||||
|
|
||||||
override val content = combine(
|
|
||||||
mangaList,
|
|
||||||
listModeFlow,
|
|
||||||
sortOrder.asFlow(),
|
|
||||||
selectedTags,
|
|
||||||
listError,
|
|
||||||
) { list, mode, order, tags, error ->
|
|
||||||
when {
|
|
||||||
error != null -> listOf(error.toErrorState(canRetry = true))
|
|
||||||
list == null -> listOf(LoadingState)
|
|
||||||
list.isEmpty() -> listOf(
|
|
||||||
EmptyState(
|
|
||||||
icon = R.drawable.ic_empty_local,
|
|
||||||
textPrimary = R.string.text_local_holder_primary,
|
|
||||||
textSecondary = R.string.text_local_holder_secondary,
|
|
||||||
actionStringRes = R.string._import,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> buildList(list.size + 1) {
|
|
||||||
add(createHeader(list, tags, order))
|
|
||||||
list.toUi(this, mode, this@LocalListViewModel, tagHighlighter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
|
||||||
|
|
||||||
init {
|
|
||||||
onRefresh()
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
localStorageChanges
|
|
||||||
.collectLatest {
|
|
||||||
if (refreshJob?.isActive != true) {
|
|
||||||
doRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpdateFilter(tags: Set<MangaTag>) {
|
|
||||||
selectedTags.value = tags
|
|
||||||
onRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRefresh() {
|
|
||||||
val prevJob = refreshJob
|
|
||||||
refreshJob = launchLoadingJob(Dispatchers.Default) {
|
|
||||||
prevJob?.cancelAndJoin()
|
|
||||||
doRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRetry() = onRefresh()
|
|
||||||
|
|
||||||
fun setSortOrder(value: SortOrder) {
|
|
||||||
sortOrder.value = value
|
|
||||||
settings.localListOrder = value
|
|
||||||
onRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun delete(ids: Set<Long>) {
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
|
|
||||||
for (manga in itemsToRemove) {
|
|
||||||
val original = repository.getRemoteManga(manga)
|
|
||||||
repository.delete(manga) || throw IOException("Unable to delete file")
|
|
||||||
runCatchingCancellable {
|
|
||||||
historyRepository.deleteOrSwap(manga, original)
|
|
||||||
}
|
|
||||||
mangaList.update { list ->
|
|
||||||
list?.filterNot { it.id == manga.id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onMangaRemoved.emitCall(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun doRefresh() {
|
|
||||||
try {
|
|
||||||
listError.value = null
|
|
||||||
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
listError.value = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createHeader(mangaList: List<Manga>, selectedTags: Set<MangaTag>, order: SortOrder): ListHeader2 {
|
|
||||||
val tags = HashMap<MangaTag, Int>()
|
|
||||||
for (item in mangaList) {
|
|
||||||
for (tag in item.tags) {
|
|
||||||
tags[tag] = tags[tag]?.plus(1) ?: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val topTags = tags.entries.sortedByDescending { it.value }.take(6)
|
|
||||||
val chips = LinkedList<ChipsView.ChipModel>()
|
|
||||||
for ((tag, _) in topTags) {
|
|
||||||
val model = ChipsView.ChipModel(
|
|
||||||
tint = 0,
|
|
||||||
title = tag.title,
|
|
||||||
isCheckable = true,
|
|
||||||
isChecked = tag in selectedTags,
|
|
||||||
data = tag,
|
|
||||||
)
|
|
||||||
if (model.isChecked) {
|
|
||||||
chips.addFirst(model)
|
|
||||||
} else {
|
|
||||||
chips.addLast(model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ListHeader2(
|
|
||||||
chips = chips,
|
|
||||||
sortOrder = order,
|
|
||||||
hasSelectedTags = selectedTags.isNotEmpty(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getCounter(mangaId: Long): Int {
|
|
||||||
return if (settings.isTrackerEnabled) {
|
|
||||||
trackingRepository.getNewChaptersCount(mangaId)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getProgress(mangaId: Long): Float {
|
|
||||||
return if (settings.isReadingIndicatorsEnabled) {
|
|
||||||
historyRepository.getProgress(mangaId)
|
|
||||||
} else {
|
|
||||||
PROGRESS_NONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.main.ui
|
|
||||||
|
|
||||||
import android.util.SparseIntArray
|
|
||||||
import androidx.core.util.set
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
|
||||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class MainViewModel @Inject constructor(
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val appUpdateRepository: AppUpdateRepository,
|
|
||||||
private val trackingRepository: TrackingRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
val onOpenReader = SingleLiveEvent<Manga>()
|
|
||||||
|
|
||||||
val isResumeEnabled = combine(
|
|
||||||
historyRepository.observeHasItems(),
|
|
||||||
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
|
|
||||||
) { hasItems, incognito ->
|
|
||||||
hasItems && !incognito
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
|
||||||
|
|
||||||
val isFeedAvailable = settings.observeAsLiveData(
|
|
||||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
|
||||||
key = AppSettings.KEY_TRACKER_ENABLED,
|
|
||||||
valueProducer = { isTrackerEnabled },
|
|
||||||
)
|
|
||||||
|
|
||||||
val counters = combine(
|
|
||||||
appUpdateRepository.observeAvailableUpdate(),
|
|
||||||
trackingRepository.observeUpdatedMangaCount(),
|
|
||||||
) { appUpdate, tracks ->
|
|
||||||
val a = SparseIntArray(2)
|
|
||||||
a[R.id.nav_tools] = if (appUpdate != null) 1 else 0
|
|
||||||
a[R.id.nav_feed] = tracks
|
|
||||||
a
|
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, SparseIntArray(0))
|
|
||||||
|
|
||||||
init {
|
|
||||||
launchJob {
|
|
||||||
appUpdateRepository.fetchUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openLastReader() {
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
val manga = historyRepository.getLastOrNull() ?: throw EmptyHistoryException()
|
|
||||||
onOpenReader.emitCall(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.main.ui.owners
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
|
|
||||||
|
|
||||||
interface NoModalBottomSheetOwner {
|
|
||||||
|
|
||||||
val bsHeader: BottomSheetHeaderBar?
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.colorfilter
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.ext.emitValue
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ColorFilterConfigViewModel @Inject constructor(
|
|
||||||
savedStateHandle: SavedStateHandle,
|
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
private val manga = checkNotNull(savedStateHandle.get<ParcelableManga>(EXTRA_MANGA)?.manga)
|
|
||||||
|
|
||||||
private var initialColorFilter: ReaderColorFilter? = null
|
|
||||||
val colorFilter = MutableLiveData<ReaderColorFilter?>(null)
|
|
||||||
val onDismiss = SingleLiveEvent<Unit>()
|
|
||||||
val preview = MutableLiveData<MangaPage?>(null)
|
|
||||||
|
|
||||||
val isChanged: Boolean
|
|
||||||
get() = colorFilter.value != initialColorFilter
|
|
||||||
|
|
||||||
init {
|
|
||||||
val page = checkNotNull(
|
|
||||||
savedStateHandle.get<ParcelableMangaPages>(ColorFilterConfigActivity.EXTRA_PAGES)?.pages?.firstOrNull(),
|
|
||||||
)
|
|
||||||
launchLoadingJob {
|
|
||||||
initialColorFilter = mangaDataRepository.getColorFilter(manga.id)
|
|
||||||
colorFilter.value = initialColorFilter
|
|
||||||
}
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
val repository = mangaRepositoryFactory.create(page.source)
|
|
||||||
val url = repository.getPageUrl(page)
|
|
||||||
preview.emitValue(
|
|
||||||
MangaPage(
|
|
||||||
id = page.id,
|
|
||||||
url = url,
|
|
||||||
preview = page.preview,
|
|
||||||
source = page.source,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setBrightness(brightness: Float) {
|
|
||||||
val cf = colorFilter.value
|
|
||||||
colorFilter.value = ReaderColorFilter(brightness, cf?.contrast ?: 0f).takeUnless { it.isEmpty }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setContrast(contrast: Float) {
|
|
||||||
val cf = colorFilter.value
|
|
||||||
colorFilter.value = ReaderColorFilter(cf?.brightness ?: 0f, contrast).takeUnless { it.isEmpty }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
colorFilter.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save() {
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
|
|
||||||
onDismiss.emitCall(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.children
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
|
||||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
|
||||||
import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled
|
|
||||||
import org.koitharu.kotatsu.utils.ext.recyclerView
|
|
||||||
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var networkState: NetworkState
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var pageLoader: PageLoader
|
|
||||||
|
|
||||||
private var pagerAdapter: ReversedPagesAdapter? = null
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
pagerAdapter = ReversedPagesAdapter(
|
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
|
||||||
loader = pageLoader,
|
|
||||||
settings = viewModel.readerSettings,
|
|
||||||
networkState = networkState,
|
|
||||||
exceptionResolver = exceptionResolver,
|
|
||||||
)
|
|
||||||
with(binding.pager) {
|
|
||||||
adapter = pagerAdapter
|
|
||||||
offscreenPageLimit = 2
|
|
||||||
doOnPageChanged(::notifyPageChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.readerAnimation.observe(viewLifecycleOwner) {
|
|
||||||
val transformer = if (it) ReversedPageAnimTransformer() else null
|
|
||||||
binding.pager.setPageTransformer(transformer)
|
|
||||||
if (transformer == null) {
|
|
||||||
binding.pager.recyclerView?.children?.forEach { v ->
|
|
||||||
v.resetTransformations()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
pagerAdapter = null
|
|
||||||
binding.pager.adapter = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun switchPageBy(delta: Int) {
|
|
||||||
with(binding.pager) {
|
|
||||||
setCurrentItem(currentItem - delta, context.isAnimationsEnabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun switchPageTo(position: Int, smooth: Boolean) {
|
|
||||||
with(binding.pager) {
|
|
||||||
setCurrentItem(
|
|
||||||
reversed(position),
|
|
||||||
smooth && context.isAnimationsEnabled && (currentItem - position).absoluteValue < PagerReaderFragment.SMOOTH_SCROLL_LIMIT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
|
|
||||||
val reversedPages = pages.asReversed()
|
|
||||||
viewLifecycleScope.launch {
|
|
||||||
val items = async {
|
|
||||||
pagerAdapter?.setItems(reversedPages)
|
|
||||||
}
|
|
||||||
if (pendingState != null) {
|
|
||||||
val position = reversedPages.indexOfLast {
|
|
||||||
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
|
||||||
}
|
|
||||||
items.await() ?: return@launch
|
|
||||||
if (position != -1) {
|
|
||||||
binding.pager.setCurrentItem(position, false)
|
|
||||||
notifyPageChanged(position)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items.await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentState(): ReaderState? = bindingOrNull()?.run {
|
|
||||||
val adapter = pager.adapter as? BaseReaderAdapter<*>
|
|
||||||
val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null
|
|
||||||
ReaderState(
|
|
||||||
chapterId = page.chapterId,
|
|
||||||
page = page.index,
|
|
||||||
scroll = 0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyPageChanged(page: Int) {
|
|
||||||
viewModel.onCurrentPageChanged(reversed(page))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reversed(position: Int): Int {
|
|
||||||
return ((pagerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager.standard
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.children
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
|
||||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
|
||||||
import org.koitharu.kotatsu.utils.ext.isAnimationsEnabled
|
|
||||||
import org.koitharu.kotatsu.utils.ext.recyclerView
|
|
||||||
import org.koitharu.kotatsu.utils.ext.resetTransformations
|
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>() {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var networkState: NetworkState
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var pageLoader: PageLoader
|
|
||||||
|
|
||||||
private var pagesAdapter: PagesAdapter? = null
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
pagesAdapter = PagesAdapter(
|
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
|
||||||
loader = pageLoader,
|
|
||||||
settings = viewModel.readerSettings,
|
|
||||||
networkState = networkState,
|
|
||||||
exceptionResolver = exceptionResolver,
|
|
||||||
)
|
|
||||||
with(binding.pager) {
|
|
||||||
adapter = pagesAdapter
|
|
||||||
offscreenPageLimit = 2
|
|
||||||
doOnPageChanged(::notifyPageChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.readerAnimation.observe(viewLifecycleOwner) {
|
|
||||||
val transformer = if (it) PageAnimTransformer() else null
|
|
||||||
binding.pager.setPageTransformer(transformer)
|
|
||||||
if (transformer == null) {
|
|
||||||
binding.pager.recyclerView?.children?.forEach { view ->
|
|
||||||
view.resetTransformations()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
pagesAdapter = null
|
|
||||||
binding.pager.adapter = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
|
|
||||||
viewLifecycleScope.launch {
|
|
||||||
val items = async {
|
|
||||||
pagesAdapter?.setItems(pages)
|
|
||||||
}
|
|
||||||
if (pendingState != null) {
|
|
||||||
val position = pages.indexOfFirst {
|
|
||||||
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
|
||||||
}
|
|
||||||
items.await() ?: return@launch
|
|
||||||
if (position != -1) {
|
|
||||||
binding.pager.setCurrentItem(position, false)
|
|
||||||
notifyPageChanged(position)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items.await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun switchPageBy(delta: Int) {
|
|
||||||
with(binding.pager) {
|
|
||||||
setCurrentItem(currentItem + delta, context.isAnimationsEnabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun switchPageTo(position: Int, smooth: Boolean) {
|
|
||||||
with(binding.pager) {
|
|
||||||
setCurrentItem(
|
|
||||||
position,
|
|
||||||
smooth && context.isAnimationsEnabled && (currentItem - position).absoluteValue < SMOOTH_SCROLL_LIMIT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentState(): ReaderState? = bindingOrNull()?.run {
|
|
||||||
val adapter = pager.adapter as? BaseReaderAdapter<*>
|
|
||||||
val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null
|
|
||||||
ReaderState(
|
|
||||||
chapterId = page.chapterId,
|
|
||||||
page = page.index,
|
|
||||||
scroll = 0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyPageChanged(page: Int) {
|
|
||||||
viewModel.onCurrentPageChanged(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val SMOOTH_SCROLL_LIMIT = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
|
|
||||||
fun interface OnPageSelectListener {
|
|
||||||
|
|
||||||
fun onPageSelected(page: MangaPage)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
|
|
||||||
data class PageThumbnail(
|
|
||||||
val number: Int,
|
|
||||||
val isCurrent: Boolean,
|
|
||||||
val repository: MangaRepository,
|
|
||||||
val page: MangaPage
|
|
||||||
)
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import coil.ImageLoader
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPages
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getParcelableCompat
|
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class PagesThumbnailsSheet :
|
|
||||||
BaseBottomSheet<SheetPagesBinding>(),
|
|
||||||
OnListItemClickListener<MangaPage>,
|
|
||||||
BottomSheetHeaderBar.OnExpansionChangeListener {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var pageLoader: PageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var coil: ImageLoader
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var settings: AppSettings
|
|
||||||
|
|
||||||
private lateinit var thumbnails: List<PageThumbnail>
|
|
||||||
private var spanResolver: MangaListSpanResolver? = null
|
|
||||||
private var currentPageIndex = -1
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pages = arguments?.getParcelableCompat<ParcelableMangaPages>(ARG_PAGES)?.pages
|
|
||||||
if (pages.isNullOrEmpty()) {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
currentPageIndex = requireArguments().getInt(ARG_CURRENT, currentPageIndex)
|
|
||||||
val repository = mangaRepositoryFactory.create(pages.first().source)
|
|
||||||
thumbnails = pages.mapIndexed { i, x ->
|
|
||||||
PageThumbnail(
|
|
||||||
number = i + 1,
|
|
||||||
isCurrent = i == currentPageIndex,
|
|
||||||
repository = repository,
|
|
||||||
page = x,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
|
|
||||||
return SheetPagesBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
spanResolver = MangaListSpanResolver(view.resources)
|
|
||||||
with(binding.headerBar) {
|
|
||||||
title = arguments?.getString(ARG_TITLE)
|
|
||||||
subtitle = null
|
|
||||||
addOnExpansionChangeListener(this@PagesThumbnailsSheet)
|
|
||||||
}
|
|
||||||
|
|
||||||
with(binding.recyclerView) {
|
|
||||||
addItemDecoration(
|
|
||||||
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
|
|
||||||
)
|
|
||||||
adapter = PageThumbnailAdapter(
|
|
||||||
dataSet = thumbnails,
|
|
||||||
coil = coil,
|
|
||||||
scope = viewLifecycleScope,
|
|
||||||
loader = pageLoader,
|
|
||||||
clickListener = this@PagesThumbnailsSheet,
|
|
||||||
)
|
|
||||||
addOnLayoutChangeListener(spanResolver)
|
|
||||||
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
|
||||||
if (currentPageIndex > 0) {
|
|
||||||
val offset = resources.getDimensionPixelOffset(R.dimen.preferred_grid_width)
|
|
||||||
(layoutManager as GridLayoutManager).scrollToPositionWithOffset(currentPageIndex, offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
spanResolver = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: MangaPage, view: View) {
|
|
||||||
(
|
|
||||||
(parentFragment as? OnPageSelectListener)
|
|
||||||
?: (activity as? OnPageSelectListener)
|
|
||||||
)?.run {
|
|
||||||
onPageSelected(item)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
|
||||||
if (isExpanded) {
|
|
||||||
headerBar.subtitle = resources.getQuantityString(
|
|
||||||
R.plurals.pages,
|
|
||||||
thumbnails.size,
|
|
||||||
thumbnails.size,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
headerBar.subtitle = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val ARG_PAGES = "pages"
|
|
||||||
private const val ARG_TITLE = "title"
|
|
||||||
private const val ARG_CURRENT = "current"
|
|
||||||
|
|
||||||
private const val TAG = "PagesThumbnailsSheet"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager, pages: List<MangaPage>, title: String, currentPage: Int) =
|
|
||||||
PagesThumbnailsSheet().withArgs(3) {
|
|
||||||
putParcelable(ARG_PAGES, ParcelableMangaPages(pages))
|
|
||||||
putString(ARG_TITLE, title)
|
|
||||||
putInt(ARG_CURRENT, currentPage)
|
|
||||||
}.show(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.Scale
|
|
||||||
import coil.size.Size
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
|
||||||
import org.koitharu.kotatsu.utils.ext.decodeRegion
|
|
||||||
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
|
||||||
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
fun pageThumbnailAD(
|
|
||||||
coil: ImageLoader,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
loader: PageLoader,
|
|
||||||
clickListener: OnListItemClickListener<MangaPage>,
|
|
||||||
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
|
|
||||||
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) },
|
|
||||||
) {
|
|
||||||
var job: Job? = null
|
|
||||||
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
|
|
||||||
val thumbSize = Size(
|
|
||||||
width = gridWidth,
|
|
||||||
height = (gridWidth / 13f * 18f).toInt(),
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) {
|
|
||||||
item.page.preview?.let { url ->
|
|
||||||
coil.execute(
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(url)
|
|
||||||
.tag(item.page.source)
|
|
||||||
.size(thumbSize)
|
|
||||||
.scale(Scale.FILL)
|
|
||||||
.allowRgb565(true)
|
|
||||||
.build(),
|
|
||||||
).drawable
|
|
||||||
}?.let { drawable ->
|
|
||||||
return@withContext drawable
|
|
||||||
}
|
|
||||||
val file = loader.loadPage(item.page, force = false)
|
|
||||||
coil.execute(
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(file)
|
|
||||||
.size(thumbSize)
|
|
||||||
.decodeRegion(0)
|
|
||||||
.allowRgb565(isLowRamDevice(context))
|
|
||||||
.build(),
|
|
||||||
).drawable
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.setOnClickListener {
|
|
||||||
clickListener.onItemClick(item.page, itemView)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
job?.cancel()
|
|
||||||
binding.imageViewThumb.setImageDrawable(null)
|
|
||||||
with(binding.textViewNumber) {
|
|
||||||
setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty)
|
|
||||||
setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary)
|
|
||||||
text = (item.number).toString()
|
|
||||||
}
|
|
||||||
job = scope.launch {
|
|
||||||
val drawable = runCatchingCancellable {
|
|
||||||
loadPageThumbnail(item)
|
|
||||||
}.getOrNull()
|
|
||||||
binding.imageViewThumb.setImageDrawable(drawable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewRecycled {
|
|
||||||
job?.cancel()
|
|
||||||
job = null
|
|
||||||
binding.imageViewThumb.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
|
|
||||||
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
|
||||||
|
|
||||||
class PageThumbnailAdapter(
|
|
||||||
dataSet: List<PageThumbnail>,
|
|
||||||
coil: ImageLoader,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
loader: PageLoader,
|
|
||||||
clickListener: OnListItemClickListener<MangaPage>
|
|
||||||
) : ListDelegationAdapter<List<PageThumbnail>>() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(pageThumbnailAD(coil, scope, loader, clickListener))
|
|
||||||
setItems(dataSet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
|
|
||||||
enum class ScrobblingStatus : ListModel {
|
|
||||||
|
|
||||||
PLANNED,
|
|
||||||
READING,
|
|
||||||
RE_READING,
|
|
||||||
COMPLETED,
|
|
||||||
ON_HOLD,
|
|
||||||
DROPPED,
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
|
|
||||||
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
|
||||||
|
|
||||||
fun scrobblingHeaderAD() = adapterDelegate<ScrobblingStatus, ListModel>(R.layout.item_header) {
|
|
||||||
|
|
||||||
bind {
|
|
||||||
(itemView as TextView).text = context.resources
|
|
||||||
.getStringArray(R.array.scrobbling_statuses)
|
|
||||||
.getOrNull(item.ordinal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user