Compare commits
191 Commits
v4.0-beta2
...
v4.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 155af8889b | |||
| 61b7117b97 | |||
| 0f4de329e5 | |||
| 9b290bea40 | |||
| 7ffa15d2d7 | |||
| e11e890818 | |||
| 3e7a48d27a | |||
| eeba959ba5 | |||
| e7fa1036be | |||
| 542a7e1141 | |||
| 5951f4438a | |||
| 1fbae6bd7b | |||
|
|
b73924aea8 | ||
|
|
005443f4ae | ||
|
|
abb55d4424 | ||
|
|
e0538da079 | ||
|
|
665bf5a034 | ||
|
|
dc7e1282c6 | ||
|
|
3a877d4f4a | ||
|
|
8a23c9a327 | ||
| 452c0edfc7 | |||
| 2b9307aa17 | |||
| f91d5e1c29 | |||
| 2fbfd14252 | |||
| c09dd92cff | |||
| 6b08074a70 | |||
| 9cb5971182 | |||
| 6f37d95c24 | |||
| d290ba24b7 | |||
| f57d23026b | |||
| 1a70ccff55 | |||
| bd6a51e58d | |||
| a9c122b144 | |||
| ed56170809 | |||
| a36e5fce29 | |||
|
|
760bfaf4d7 | ||
| 24463720b1 | |||
| 516470e8ae | |||
| 7f530d0476 | |||
| 8a2706d70b | |||
| 27f09480a0 | |||
| c03dcf6d2e | |||
| bdb2ae9c2f | |||
| 3413fe6943 | |||
| e6ae9e8bd6 | |||
| 8d9426f257 | |||
| 30247e3def | |||
| 084dc32d2d | |||
| 3393f1397b | |||
| 61784bcfc4 | |||
| bd692fc60c | |||
| 738299e8d3 | |||
| c8b6dc27b2 | |||
| 1493aa39a3 | |||
| f115031846 | |||
| 571b85dfd8 | |||
|
|
75cc9e9030 | ||
| 656a707b4c | |||
| 04afe7a934 | |||
| 689670b3ff | |||
| 6273a9decb | |||
|
|
72336d4f71 | ||
|
|
731e998eb2 | ||
|
|
9bf53114de | ||
| 0e1b8db688 | |||
| 3a62e2e6c0 | |||
| 08764cb3cb | |||
|
|
9c52545f63 | ||
|
|
a6c30d33d4 | ||
|
|
25974af229 | ||
|
|
607dfc9be3 | ||
|
|
560e669700 | ||
|
|
ba403c9360 | ||
|
|
0f1c9ff05d | ||
|
|
662f08e115 | ||
|
|
d647a32e9f | ||
| 375e72cb98 | |||
| 34c7cafdfe | |||
|
|
03e0eefe4d | ||
| f41425f03d | |||
| 400b91278f | |||
| 9088f77ae5 | |||
| 86da3217d1 | |||
| 24908e52af | |||
| 1261a6790d | |||
| 59fa61864a | |||
| 1cbfe017ea | |||
| f469369b14 | |||
| 1ddcaed483 | |||
|
|
7bb7736f18 | ||
|
|
d1e7e7a2a6 | ||
| 0c4b7b0586 | |||
| f320f22863 | |||
| d224cd99bb | |||
|
|
b955d31770 | ||
| b4eb8d56a6 | |||
| c896ac72e8 | |||
| b599cb33ff | |||
| b3eab1a2a0 | |||
| 79d9dc7b24 | |||
| 7b573f8e6b | |||
|
|
7bd769e294 | ||
|
|
fde5f86313 | ||
| 3c23bf7ec9 | |||
| 4665f8b74e | |||
| 5a43e677c5 | |||
| 38d4274ece | |||
| 0e5221fa6e | |||
| b458bde8a1 | |||
| c663d10515 | |||
| cec19c3db3 | |||
|
|
ff58539e2e | ||
| d8e7689a94 | |||
| 32cfbb327c | |||
| 245e87256b | |||
| ed8c69037f | |||
|
|
3f76d22d67 | ||
| 980988e684 | |||
|
|
347811abb6 | ||
|
|
ccb8b0c8e7 | ||
|
|
18137ab48e | ||
|
|
a9f435ae3d | ||
| 0758cfef64 | |||
| 06d1d56448 | |||
| 07c70eaccc | |||
| 5ad6413952 | |||
|
|
0b9d9ac7f2 | ||
| 3ab87027ab | |||
|
|
7545a774ba | ||
| db16eb8e29 | |||
|
|
e5a27a7c6f | ||
|
|
d26bc102d1 | ||
|
|
fc6a8afd93 | ||
| 5a9d401446 | |||
| 77ac40b445 | |||
| a29454f672 | |||
| 80ee7c8e54 | |||
| fb202f80a5 | |||
| 45dbd5aa44 | |||
| ee65251bf5 | |||
| eaeb11f9ce | |||
| 2f74633abb | |||
| 0f346dc725 | |||
| 1b92848964 | |||
| 3d91583585 | |||
| f76d9fa3e4 | |||
| b00b2e406e | |||
| 74717e2b93 | |||
| 9b54ed6bc7 | |||
| 7b36c64b34 | |||
| da09884136 | |||
| 64aaf37556 | |||
| 11104223eb | |||
| 0c119bc137 | |||
| 5c058e626b | |||
| 2005ae2bf3 | |||
| d0650c7cf4 | |||
|
|
2df4e6480a | ||
|
|
017a1686dc | ||
|
|
279dc03695 | ||
|
|
c8c482f692 | ||
|
|
02a0e3ebcd | ||
|
|
fc0b3f3b38 | ||
|
|
2925900214 | ||
|
|
eae370e41c | ||
|
|
d0338a604a | ||
|
|
e22b98b476 | ||
| 4d838d290d | |||
| 048efdf59f | |||
| 65dbc6b8e5 | |||
| 627a00beb4 | |||
| e00ed13ad1 | |||
| af2adeba13 | |||
| 893fa6bd90 | |||
| 512188c8dd | |||
| aae6761809 | |||
| c3f055d0c4 | |||
| 93c6bec452 | |||
|
|
04d5df20d1 | ||
|
|
665eca0699 | ||
|
|
9a1534464f | ||
|
|
f856fc6fac | ||
| 4af8e73303 | |||
| 23239f1fec | |||
| 853e4d6fde | |||
| 14a37ad16e | |||
| c944044465 | |||
| 8a63ca2310 | |||
| 12e5e3b35e | |||
| 553a85ef86 | |||
| de7012cabf |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
|||||||
|
ko_fi: xtimms
|
||||||
custom: ["https://yoomoney.ru/to/410012543938752"]
|
custom: ["https://yoomoney.ru/to/410012543938752"]
|
||||||
ko_fi: koitharu
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
|||||||
/.idea/modules.xml
|
/.idea/modules.xml
|
||||||
/.idea/misc.xml
|
/.idea/misc.xml
|
||||||
/.idea/discord.xml
|
/.idea/discord.xml
|
||||||
|
/.idea/compiler.xml
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
|
|||||||
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel target="11" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@@ -7,7 +7,7 @@
|
|||||||
<option name="testRunner" value="GRADLE" />
|
<option name="testRunner" value="GRADLE" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="Embedded JDK" />
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
|||||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -4,6 +4,6 @@
|
|||||||
<option name="jvmTarget" value="1.8" />
|
<option name="jvmTarget" value="1.8" />
|
||||||
</component>
|
</component>
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.7.10" />
|
<option name="version" value="1.8.0" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -26,7 +26,7 @@ Download APK directly from GitHub:
|
|||||||
* Notifications about new chapters with updates feed
|
* Notifications about new chapters with updates feed
|
||||||
* Shikimori integration (manga tracking)
|
* Shikimori integration (manga tracking)
|
||||||
* Password/fingerprint protect access to the app
|
* Password/fingerprint protect access to the app
|
||||||
* History and favourites synchronization across devices (coming soon)
|
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdk = 33
|
||||||
buildToolsVersion '33.0.0'
|
buildToolsVersion = '33.0.1'
|
||||||
namespace 'org.koitharu.kotatsu'
|
namespace = 'org.koitharu.kotatsu'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode 498
|
versionCode 514
|
||||||
versionName '4.0-beta2'
|
versionName '4.3.3'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -56,10 +56,12 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
freeCompilerArgs += [
|
freeCompilerArgs += [
|
||||||
|
'-opt-in=kotlin.ExperimentalStdlibApi',
|
||||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
'-opt-in=kotlinx.coroutines.FlowPreview',
|
||||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||||
'-opt-in=coil.annotation.ExperimentalCoilApi',
|
'-opt-in=coil.annotation.ExperimentalCoilApi',
|
||||||
|
'-opt-in=com.google.android.material.badge.ExperimentalBadgeUtils',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
@@ -82,15 +84,17 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:b3a9c5fcda') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:00abaea324') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||||
|
|
||||||
|
implementation "androidx.appcompat:appcompat:1.6.0"
|
||||||
implementation 'androidx.core:core-ktx:1.9.0'
|
implementation 'androidx.core:core-ktx:1.9.0'
|
||||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
implementation 'androidx.activity:activity-ktx:1.6.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.5.2'
|
implementation 'androidx.fragment:fragment-ktx:1.5.5'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
|
||||||
@@ -101,51 +105,52 @@ dependencies {
|
|||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.7.0-rc01'
|
implementation 'com.google.android.material:material:1.8.0'
|
||||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
|
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
|
||||||
|
|
||||||
implementation 'androidx.room:room-runtime:2.4.3'
|
implementation 'androidx.room:room-runtime:2.5.0'
|
||||||
implementation 'androidx.room:room-ktx:2.4.3'
|
implementation 'androidx.room:room-ktx:2.5.0'
|
||||||
kapt 'androidx.room:room-compiler:2.4.3'
|
kapt 'androidx.room:room-compiler:2.5.0'
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
||||||
implementation 'com.squareup.okio:okio:3.2.0'
|
implementation 'com.squareup.okio:okio:3.3.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.43.2"
|
implementation 'com.google.dagger:hilt-android:2.44.2'
|
||||||
kapt "com.google.dagger:hilt-compiler:2.43.2"
|
kapt 'com.google.dagger:hilt-compiler:2.44.2'
|
||||||
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.2.1'
|
implementation 'io.coil-kt:coil-base:2.2.2'
|
||||||
implementation 'io.coil-kt:coil-svg:2.2.1'
|
implementation 'io.coil-kt:coil-svg:2.2.2'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:0ff0278f0f'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
||||||
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 'ch.acra:acra-http:5.9.6'
|
implementation 'ch.acra:acra-http:5.9.7'
|
||||||
implementation 'ch.acra:acra-dialog:5.9.6'
|
implementation 'ch.acra:acra-dialog:5.9.7'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.json:json:20220320'
|
testImplementation 'org.json:json:20220924'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
|
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||||
|
|
||||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.room:room-testing:2.4.3'
|
androidTestImplementation 'androidx.room:room-testing:2.5.0'
|
||||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
|
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
|
||||||
|
|
||||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.43.2'
|
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44.2'
|
||||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.43.2'
|
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44.2'
|
||||||
}
|
}
|
||||||
|
|||||||
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@@ -14,3 +14,4 @@
|
|||||||
|
|
||||||
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
||||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||||
|
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
android:fullBackupOnly="true"
|
android:fullBackupOnly="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:largeHeap="true"
|
||||||
|
android:localeConfig="@xml/locales"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
@@ -68,6 +70,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
|
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
|
||||||
android:label="@string/history" />
|
android:label="@string/history" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity"
|
||||||
|
android:label="@string/updates" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
|
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
|
||||||
android:label="@string/favourites" />
|
android:label="@string/favourites" />
|
||||||
@@ -105,8 +110,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/manga_shelf"
|
android:label="@string/manga_shelf">
|
||||||
android:theme="@style/Theme.Kotatsu.DialogWhenLarge">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -124,18 +128,18 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
|
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
|
||||||
android:label="@string/downloads"
|
android:label="@string/downloads"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop" />
|
||||||
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
|
|
||||||
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
|
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
|
||||||
<activity
|
<activity android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity" />
|
||||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity"
|
|
||||||
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
|
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
|
||||||
android:label="@string/sync" />
|
android:label="@string/sync" />
|
||||||
<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" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
||||||
@@ -184,6 +188,9 @@
|
|||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
android:resource="@xml/sync_history" />
|
android:resource="@xml/sync_history" />
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.details.service.MangaPrefetchService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
||||||
@@ -244,6 +251,9 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.webkit.WebView.MetricsOptOut"
|
android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.icon_container.has_icon_container"
|
||||||
|
android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.hilt.work.HiltWorkerFactory
|
|||||||
import androidx.room.InvalidationTracker
|
import androidx.room.InvalidationTracker
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.acra.ReportField
|
import org.acra.ReportField
|
||||||
@@ -25,6 +24,7 @@ import org.koitharu.kotatsu.local.data.PagesCache
|
|||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class KotatsuApp : Application(), Configuration.Provider {
|
class KotatsuApp : Application(), Configuration.Provider {
|
||||||
@@ -50,6 +50,7 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
enableStrictMode()
|
enableStrictMode()
|
||||||
}
|
}
|
||||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||||
|
AppCompatDelegate.setApplicationLocales(settings.appLocales)
|
||||||
setupActivityLifecycleCallbacks()
|
setupActivityLifecycleCallbacks()
|
||||||
processLifecycleScope.launch(Dispatchers.Default) {
|
processLifecycleScope.launch(Dispatchers.Default) {
|
||||||
setupDatabaseObservers()
|
setupDatabaseObservers()
|
||||||
@@ -72,6 +73,7 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
}
|
}
|
||||||
reportContent = listOf(
|
reportContent = listOf(
|
||||||
ReportField.PACKAGE_NAME,
|
ReportField.PACKAGE_NAME,
|
||||||
|
ReportField.INSTALLATION_ID,
|
||||||
ReportField.APP_VERSION_CODE,
|
ReportField.APP_VERSION_CODE,
|
||||||
ReportField.APP_VERSION_NAME,
|
ReportField.APP_VERSION_NAME,
|
||||||
ReportField.ANDROID_VERSION,
|
ReportField.ANDROID_VERSION,
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
|
EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
|
||||||
val isAmoled = settings.isAmoledTheme
|
val isAmoled = settings.isAmoledTheme
|
||||||
val isDynamic = settings.isDynamicTheme
|
val isDynamic = settings.isDynamicTheme
|
||||||
// TODO support DialogWhenLarge theme
|
|
||||||
when {
|
when {
|
||||||
isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
|
isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
|
||||||
isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)
|
isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import android.app.Service
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
|
||||||
abstract class CoroutineIntentService : BaseService() {
|
abstract class CoroutineIntentService : BaseService() {
|
||||||
|
|
||||||
@@ -21,17 +23,29 @@ abstract class CoroutineIntentService : BaseService() {
|
|||||||
return Service.START_REDELIVER_INTENT
|
return Service.START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch {
|
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch(errorHandler(startId)) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
try {
|
try {
|
||||||
|
if (intent != null) {
|
||||||
withContext(dispatcher) {
|
withContext(dispatcher) {
|
||||||
processIntent(intent)
|
processIntent(startId, intent)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
onError(startId, e)
|
||||||
} finally {
|
} finally {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract suspend fun processIntent(intent: Intent?)
|
protected abstract suspend fun processIntent(startId: Int, intent: Intent)
|
||||||
|
|
||||||
|
protected abstract fun onError(startId: Int, error: Throwable)
|
||||||
|
|
||||||
|
private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
throwable.printStackTraceDebug()
|
||||||
|
onError(startId, throwable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application.ActivityLifecycleCallbacks
|
||||||
|
import android.os.Bundle
|
||||||
|
|
||||||
|
interface DefaultActivityLifecycleCallbacks : ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivityResumed(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivityPaused(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivityStopped(activity: Activity) = Unit
|
||||||
|
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
||||||
|
|
||||||
|
override fun onActivityDestroyed(activity: Activity) = Unit
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import android.view.View.OnLongClickListener
|
|||||||
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
|
||||||
|
|
||||||
class AdapterDelegateClickListenerAdapter<I>(
|
class AdapterDelegateClickListenerAdapter<I>(
|
||||||
private val adapterDelegate: AdapterDelegateViewBindingViewHolder<I, *>,
|
private val adapterDelegate: AdapterDelegateViewBindingViewHolder<out I, *>,
|
||||||
private val clickListener: OnListItemClickListener<I>,
|
private val clickListener: OnListItemClickListener<I>,
|
||||||
) : OnClickListener, OnLongClickListener {
|
) : OnClickListener, OnLongClickListener {
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,20 @@ package org.koitharu.kotatsu.base.ui.list
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.ArrayMap
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.collection.ArrayMap
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.savedstate.SavedStateRegistry
|
import androidx.savedstate.SavedStateRegistry
|
||||||
import androidx.savedstate.SavedStateRegistryOwner
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
private const val PROVIDER_NAME = "selection_decoration_sectioned"
|
private const val PROVIDER_NAME = "selection_decoration_sectioned"
|
||||||
|
|
||||||
|
|||||||
@@ -516,6 +516,6 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
|
|
||||||
interface SectionIndexer {
|
interface SectionIndexer {
|
||||||
|
|
||||||
fun getSectionText(context: Context, position: Int): CharSequence
|
fun getSectionText(context: Context, position: Int): CharSequence?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
package org.koitharu.kotatsu.base.ui.util
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application.ActivityLifecycleCallbacks
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import java.util.*
|
import org.koitharu.kotatsu.base.ui.DefaultActivityLifecycleCallbacks
|
||||||
|
import java.util.WeakHashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ActivityRecreationHandle @Inject constructor() : ActivityLifecycleCallbacks {
|
class ActivityRecreationHandle @Inject constructor() : DefaultActivityLifecycleCallbacks {
|
||||||
|
|
||||||
private val activities = WeakHashMap<Activity, Unit>()
|
private val activities = WeakHashMap<Activity, Unit>()
|
||||||
|
|
||||||
@@ -16,16 +16,6 @@ class ActivityRecreationHandle @Inject constructor() : ActivityLifecycleCallback
|
|||||||
activities[activity] = Unit
|
activities[activity] = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityStarted(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivityPaused(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivityStopped(activity: Activity) = Unit
|
|
||||||
|
|
||||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
|
||||||
|
|
||||||
override fun onActivityDestroyed(activity: Activity) {
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
activities.remove(activity)
|
activities.remove(activity)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior
|
|||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
|
||||||
class ShrinkOnScrollBehavior : Behavior<ExtendedFloatingActionButton> {
|
open class ShrinkOnScrollBehavior : Behavior<ExtendedFloatingActionButton> {
|
||||||
|
|
||||||
@Suppress("unused") constructor() : super()
|
@Suppress("unused")
|
||||||
@Suppress("unused") constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
|
constructor() : super()
|
||||||
|
@Suppress("unused")
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
|
||||||
|
|
||||||
override fun onStartNestedScroll(
|
override fun onStartNestedScroll(
|
||||||
coordinatorLayout: CoordinatorLayout,
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ fun bookmarkListAD(
|
|||||||
binding.root.setOnLongClickListener(listener)
|
binding.root.setOnLongClickListener(listener)
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.imageViewThumb.newImageRequest(item.imageUrl)?.run {
|
binding.imageViewThumb.newImageRequest(item.imageUrl, item.manga.source)?.run {
|
||||||
referer(item.manga.publicUrl)
|
referer(item.manga.publicUrl)
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
fallback(R.drawable.ic_placeholder)
|
fallback(R.drawable.ic_placeholder)
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
|||||||
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
|
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
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.referer
|
||||||
|
|
||||||
fun bookmarksGroupAD(
|
fun bookmarksGroupAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
@@ -45,7 +49,7 @@ fun bookmarksGroupAD(
|
|||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
binding.recyclerView.addItemDecoration(spacingDecoration)
|
||||||
selectionController.attachToRecyclerView(item.manga, binding.recyclerView)
|
selectionController.attachToRecyclerView(item.manga, binding.recyclerView)
|
||||||
}
|
}
|
||||||
binding.imageViewCover.newImageRequest(item.manga.coverUrl)?.run {
|
binding.imageViewCover.newImageRequest(item.manga.coverUrl, item.manga.source)?.run {
|
||||||
referer(item.manga.publicUrl)
|
referer(item.manga.publicUrl)
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
fallback(R.drawable.ic_placeholder)
|
fallback(R.drawable.ic_placeholder)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
}
|
}
|
||||||
with(binding.webView.settings) {
|
with(binding.webView.settings) {
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
userAgentString = UserAgentInterceptor.userAgent
|
userAgentString = UserAgentInterceptor.userAgentChrome
|
||||||
}
|
}
|
||||||
binding.webView.webViewClient = BrowserClient(this)
|
binding.webView.webViewClient = BrowserClient(this)
|
||||||
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
|
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import android.graphics.Bitmap
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
|
|
||||||
private const val CF_CLEARANCE = "cf_clearance"
|
private const val CF_CLEARANCE = "cf_clearance"
|
||||||
|
|
||||||
class CloudFlareClient(
|
class CloudFlareClient(
|
||||||
private val cookieJar: AndroidCookieJar,
|
private val cookieJar: MutableCookieJar,
|
||||||
private val callback: CloudFlareCallback,
|
private val callback: CloudFlareCallback,
|
||||||
private val targetUrl: String,
|
private val targetUrl: String,
|
||||||
) : WebViewClient() {
|
) : WebViewClient() {
|
||||||
|
|||||||
@@ -12,13 +12,15 @@ import androidx.core.view.isInvisible
|
|||||||
import androidx.fragment.app.setFragmentResult
|
import androidx.fragment.app.setFragmentResult
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import okhttp3.Headers
|
||||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
||||||
import org.koitharu.kotatsu.utils.ext.stringArgument
|
import org.koitharu.kotatsu.utils.ext.stringArgument
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
||||||
@@ -27,7 +29,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
|
|||||||
private val pendingResult = Bundle(1)
|
private val pendingResult = Bundle(1)
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var cookieJar: AndroidCookieJar
|
lateinit var cookieJar: MutableCookieJar
|
||||||
|
|
||||||
override fun onInflateView(
|
override fun onInflateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -42,7 +44,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
|
|||||||
cacheMode = WebSettings.LOAD_DEFAULT
|
cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
domStorageEnabled = true
|
domStorageEnabled = true
|
||||||
databaseEnabled = true
|
databaseEnabled = true
|
||||||
userAgentString = UserAgentInterceptor.userAgent
|
userAgentString = arguments?.getString(ARG_UA) ?: UserAgentInterceptor.userAgentChrome
|
||||||
}
|
}
|
||||||
binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url.orEmpty())
|
binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url.orEmpty())
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
||||||
@@ -92,9 +94,13 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
|
|||||||
const val TAG = "CloudFlareDialog"
|
const val TAG = "CloudFlareDialog"
|
||||||
const val EXTRA_RESULT = "result"
|
const val EXTRA_RESULT = "result"
|
||||||
private const val ARG_URL = "url"
|
private const val ARG_URL = "url"
|
||||||
|
private const val ARG_UA = "ua"
|
||||||
|
|
||||||
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {
|
fun newInstance(url: String, headers: Headers?) = CloudFlareDialog().withArgs(2) {
|
||||||
putString(ARG_URL, url)
|
putString(ARG_URL, url)
|
||||||
|
headers?.get(CommonHeaders.USER_AGENT)?.let {
|
||||||
|
putString(ARG_UA, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.SearchRecentSuggestions
|
import android.provider.SearchRecentSuggestions
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
|
import android.util.AndroidRuntimeException
|
||||||
import androidx.collection.arraySetOf
|
import androidx.collection.arraySetOf
|
||||||
import androidx.room.InvalidationTracker
|
import androidx.room.InvalidationTracker
|
||||||
import coil.ComponentRegistry
|
import coil.ComponentRegistry
|
||||||
@@ -18,15 +19,20 @@ import dagger.hilt.InstallIn
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import dagger.multibindings.ElementsIntoSet
|
import dagger.multibindings.ElementsIntoSet
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
||||||
|
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||||
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
|
import org.koitharu.kotatsu.core.cache.StubContentCache
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.network.*
|
import org.koitharu.kotatsu.core.network.*
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
|
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
@@ -40,16 +46,21 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|||||||
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||||
|
import org.koitharu.kotatsu.utils.IncognitoModeIndicator
|
||||||
|
import org.koitharu.kotatsu.utils.ext.activityManager
|
||||||
|
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||||
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
||||||
import org.koitharu.kotatsu.utils.image.CoilImageGetter
|
import org.koitharu.kotatsu.utils.image.CoilImageGetter
|
||||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface AppModule {
|
interface AppModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindCookieJar(androidCookieJar: AndroidCookieJar): CookieJar
|
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
|
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
|
||||||
@@ -59,10 +70,22 @@ interface AppModule {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideCookieJar(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
): MutableCookieJar = try {
|
||||||
|
AndroidCookieJar()
|
||||||
|
} catch (e: AndroidRuntimeException) {
|
||||||
|
// WebView is not available
|
||||||
|
PreferencesCookieJar(context)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideOkHttpClient(
|
fun provideOkHttpClient(
|
||||||
localStorageManager: LocalStorageManager,
|
localStorageManager: LocalStorageManager,
|
||||||
|
userAgentInterceptor: UserAgentInterceptor,
|
||||||
cookieJar: CookieJar,
|
cookieJar: CookieJar,
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
@@ -75,11 +98,17 @@ interface AppModule {
|
|||||||
dns(DoHManager(cache, settings))
|
dns(DoHManager(cache, settings))
|
||||||
cache(cache)
|
cache(cache)
|
||||||
addInterceptor(GZipInterceptor())
|
addInterceptor(GZipInterceptor())
|
||||||
addInterceptor(UserAgentInterceptor())
|
addInterceptor(userAgentInterceptor)
|
||||||
addInterceptor(CloudFlareInterceptor())
|
addInterceptor(CloudFlareInterceptor())
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideNetworkState(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
) = NetworkState(context.connectivityManager)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideMangaDatabase(
|
fun provideMangaDatabase(
|
||||||
@@ -152,9 +181,23 @@ interface AppModule {
|
|||||||
fun provideActivityLifecycleCallbacks(
|
fun provideActivityLifecycleCallbacks(
|
||||||
appProtectHelper: AppProtectHelper,
|
appProtectHelper: AppProtectHelper,
|
||||||
activityRecreationHandle: ActivityRecreationHandle,
|
activityRecreationHandle: ActivityRecreationHandle,
|
||||||
|
incognitoModeIndicator: IncognitoModeIndicator,
|
||||||
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
|
||||||
appProtectHelper,
|
appProtectHelper,
|
||||||
activityRecreationHandle,
|
activityRecreationHandle,
|
||||||
|
incognitoModeIndicator,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideContentCache(
|
||||||
|
application: Application,
|
||||||
|
): ContentCache {
|
||||||
|
return if (application.activityManager?.isLowRamDevice == true) {
|
||||||
|
StubContentCache()
|
||||||
|
} else {
|
||||||
|
MemoryContentCache(application)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt
vendored
Normal file
23
app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package org.koitharu.kotatsu.core.cache
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
interface ContentCache {
|
||||||
|
|
||||||
|
val isCachingEnabled: Boolean
|
||||||
|
|
||||||
|
suspend fun getDetails(source: MangaSource, url: String): Manga?
|
||||||
|
|
||||||
|
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>)
|
||||||
|
|
||||||
|
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>?
|
||||||
|
|
||||||
|
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
|
||||||
|
|
||||||
|
data class Key(
|
||||||
|
val source: MangaSource,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
5
app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt
vendored
Normal file
5
app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package org.koitharu.kotatsu.core.cache
|
||||||
|
|
||||||
|
import androidx.collection.LruCache
|
||||||
|
|
||||||
|
class DeferredLruCache<T>(maxSize: Int) : LruCache<ContentCache.Key, SafeDeferred<T>>(maxSize)
|
||||||
59
app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt
vendored
Normal file
59
app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package org.koitharu.kotatsu.core.cache
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ComponentCallbacks2
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
|
||||||
|
|
||||||
|
init {
|
||||||
|
application.registerComponentCallbacks(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val detailsCache = DeferredLruCache<Manga>(4)
|
||||||
|
private val pagesCache = DeferredLruCache<List<MangaPage>>(4)
|
||||||
|
|
||||||
|
override val isCachingEnabled: Boolean = true
|
||||||
|
|
||||||
|
override suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||||
|
return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||||
|
detailsCache.put(ContentCache.Key(source, url), details)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||||
|
return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||||
|
pagesCache.put(ContentCache.Key(source, url), pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||||
|
|
||||||
|
override fun onLowMemory() = Unit
|
||||||
|
|
||||||
|
override fun onTrimMemory(level: Int) {
|
||||||
|
trimCache(detailsCache, level)
|
||||||
|
trimCache(pagesCache, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trimCache(cache: DeferredLruCache<*>, level: Int) {
|
||||||
|
when (level) {
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.evictAll()
|
||||||
|
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> cache.trimToSize(1)
|
||||||
|
|
||||||
|
else -> cache.trimToSize(cache.maxSize() / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt
vendored
Normal file
20
app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package org.koitharu.kotatsu.core.cache
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
|
||||||
|
class SafeDeferred<T>(
|
||||||
|
private val delegate: Deferred<Result<T>>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun await(): T {
|
||||||
|
return delegate.await().getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun awaitOrNull(): T? {
|
||||||
|
return delegate.await().getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
delegate.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt
vendored
Normal file
18
app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.core.cache
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class StubContentCache : ContentCache {
|
||||||
|
|
||||||
|
override val isCachingEnabled: Boolean = false
|
||||||
|
|
||||||
|
override suspend fun getDetails(source: MangaSource, url: String): Manga? = null
|
||||||
|
|
||||||
|
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) = Unit
|
||||||
|
|
||||||
|
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
|
||||||
|
|
||||||
|
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
|
||||||
|
}
|
||||||
@@ -14,11 +14,11 @@ abstract class MangaDao {
|
|||||||
abstract suspend fun find(id: Long): MangaWithTags?
|
abstract suspend fun find(id: Long): MangaWithTags?
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM manga WHERE title LIKE :query OR alt_title LIKE :query LIMIT :limit")
|
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source LIMIT :limit")
|
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||||
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
|||||||
@@ -13,16 +13,6 @@ abstract class PreferencesDao {
|
|||||||
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
|
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
|
||||||
abstract fun observe(mangaId: Long): Flow<MangaPrefsEntity?>
|
abstract fun observe(mangaId: Long): Flow<MangaPrefsEntity?>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Upsert
|
||||||
abstract suspend fun insert(pref: MangaPrefsEntity): Long
|
abstract suspend fun upsert(pref: MangaPrefsEntity)
|
||||||
|
|
||||||
@Update
|
|
||||||
abstract suspend fun update(pref: MangaPrefsEntity): Int
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
open suspend fun upsert(pref: MangaPrefsEntity) {
|
|
||||||
if (update(pref) == 0) {
|
|
||||||
insert(pref)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ abstract class TagsDao {
|
|||||||
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
GROUP BY tags.title
|
GROUP BY tags.title
|
||||||
ORDER BY COUNT(manga_id) DESC
|
ORDER BY COUNT(manga_id) DESC
|
||||||
LIMIT :limit"""
|
LIMIT :limit""",
|
||||||
)
|
)
|
||||||
abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
|
abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ abstract class TagsDao {
|
|||||||
WHERE tags.source = :source
|
WHERE tags.source = :source
|
||||||
GROUP BY tags.title
|
GROUP BY tags.title
|
||||||
ORDER BY COUNT(manga_id) DESC
|
ORDER BY COUNT(manga_id) DESC
|
||||||
LIMIT :limit"""
|
LIMIT :limit""",
|
||||||
)
|
)
|
||||||
abstract suspend fun findPopularTags(source: String, limit: Int): List<TagEntity>
|
abstract suspend fun findPopularTags(source: String, limit: Int): List<TagEntity>
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ abstract class TagsDao {
|
|||||||
WHERE tags.source = :source AND title LIKE :query
|
WHERE tags.source = :source AND title LIKE :query
|
||||||
GROUP BY tags.title
|
GROUP BY tags.title
|
||||||
ORDER BY COUNT(manga_id) DESC
|
ORDER BY COUNT(manga_id) DESC
|
||||||
LIMIT :limit"""
|
LIMIT :limit""",
|
||||||
)
|
)
|
||||||
abstract suspend fun findTags(source: String, query: String, limit: Int): List<TagEntity>
|
abstract suspend fun findTags(source: String, query: String, limit: Int): List<TagEntity>
|
||||||
|
|
||||||
@@ -44,22 +44,10 @@ abstract class TagsDao {
|
|||||||
WHERE title LIKE :query
|
WHERE title LIKE :query
|
||||||
GROUP BY tags.title
|
GROUP BY tags.title
|
||||||
ORDER BY COUNT(manga_id) DESC
|
ORDER BY COUNT(manga_id) DESC
|
||||||
LIMIT :limit"""
|
LIMIT :limit""",
|
||||||
)
|
)
|
||||||
abstract suspend fun findTags(query: String, limit: Int): List<TagEntity>
|
abstract suspend fun findTags(query: String, limit: Int): List<TagEntity>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Upsert
|
||||||
abstract suspend fun insert(tag: TagEntity): Long
|
abstract suspend fun upsert(tags: Iterable<TagEntity>)
|
||||||
|
|
||||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
abstract suspend fun update(tag: TagEntity): Int
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
open suspend fun upsert(tags: Iterable<TagEntity>) {
|
|
||||||
tags.forEach { tag ->
|
|
||||||
if (update(tag) <= 0) {
|
|
||||||
insert(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.utils.ext.longHashCode
|
|||||||
fun TagEntity.toMangaTag() = MangaTag(
|
fun TagEntity.toMangaTag() = MangaTag(
|
||||||
key = this.key,
|
key = this.key,
|
||||||
title = this.title.toTitleCase(),
|
title = this.title.toTitleCase(),
|
||||||
source = MangaSource(this.source) ?: MangaSource.DUMMY,
|
source = MangaSource(this.source),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
|
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
|
||||||
@@ -28,7 +28,7 @@ fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
|||||||
coverUrl = this.coverUrl,
|
coverUrl = this.coverUrl,
|
||||||
largeCoverUrl = this.largeCoverUrl,
|
largeCoverUrl = this.largeCoverUrl,
|
||||||
author = this.author,
|
author = this.author,
|
||||||
source = MangaSource(this.source) ?: MangaSource.DUMMY,
|
source = MangaSource(this.source),
|
||||||
tags = tags,
|
tags = tags,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions
|
package org.koitharu.kotatsu.core.exceptions
|
||||||
|
|
||||||
|
import okhttp3.Headers
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
|
|
||||||
class CloudFlareProtectedException(
|
class CloudFlareProtectedException(
|
||||||
val url: String
|
val url: String,
|
||||||
|
val headers: Headers,
|
||||||
) : IOException("Protected by CloudFlare")
|
) : IOException("Protected by CloudFlare")
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions.resolve
|
package org.koitharu.kotatsu.core.exceptions.resolve
|
||||||
|
|
||||||
import android.util.ArrayMap
|
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.collection.ArrayMap
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import okhttp3.Headers
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
||||||
@@ -20,6 +18,9 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
|||||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
||||||
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
||||||
import org.koitharu.kotatsu.utils.isSuccess
|
import org.koitharu.kotatsu.utils.isSuccess
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
class ExceptionResolver private constructor(
|
class ExceptionResolver private constructor(
|
||||||
private val activity: FragmentActivity?,
|
private val activity: FragmentActivity?,
|
||||||
@@ -43,17 +44,18 @@ class ExceptionResolver private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
||||||
is CloudFlareProtectedException -> resolveCF(e.url)
|
is CloudFlareProtectedException -> resolveCF(e.url, e.headers)
|
||||||
is AuthRequiredException -> resolveAuthException(e.source)
|
is AuthRequiredException -> resolveAuthException(e.source)
|
||||||
is NotFoundException -> {
|
is NotFoundException -> {
|
||||||
openInBrowser(e.url)
|
openInBrowser(e.url)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun resolveCF(url: String): Boolean {
|
private suspend fun resolveCF(url: String, headers: Headers): Boolean {
|
||||||
val dialog = CloudFlareDialog.newInstance(url)
|
val dialog = CloudFlareDialog.newInstance(url, headers)
|
||||||
val fm = getFragmentManager()
|
val fm = getFragmentManager()
|
||||||
return suspendCancellableCoroutine { cont ->
|
return suspendCancellableCoroutine { cont ->
|
||||||
fm.clearFragmentResult(CloudFlareDialog.TAG)
|
fm.clearFragmentResult(CloudFlareDialog.TAG)
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ class AppUpdateRepository @Inject constructor(
|
|||||||
return BuildConfig.DEBUG || getCertificateSHA1Fingerprint() == CERT_SHA1
|
return BuildConfig.DEBUG || getCertificateSHA1Fingerprint() == CERT_SHA1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getCurrentVersionChangelog(): String? {
|
||||||
|
val currentVersion = VersionId(BuildConfig.VERSION_NAME)
|
||||||
|
val available = getAvailableVersions()
|
||||||
|
return available.find { x -> x.versionId == currentVersion }?.description
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@SuppressLint("PackageManagerGetSignatures")
|
@SuppressLint("PackageManagerGetSignatures")
|
||||||
private fun getCertificateSHA1Fingerprint(): String? = runCatching {
|
private fun getCertificateSHA1Fingerprint(): String? = runCatching {
|
||||||
|
|||||||
128
app/src/main/java/org/koitharu/kotatsu/core/logs/FileLogger.kt
Normal file
128
app/src/main/java/org/koitharu/kotatsu/core/logs/FileLogger.kt
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package org.koitharu.kotatsu.core.logs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import org.koitharu.kotatsu.utils.ext.subdir
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
private const val DIR = "logs"
|
||||||
|
private const val FLUSH_DELAY = 2_000L
|
||||||
|
private const val MAX_SIZE_BYTES = 1024 * 1024 // 1 MB
|
||||||
|
|
||||||
|
class FileLogger(
|
||||||
|
context: Context,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
name: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val file by lazy {
|
||||||
|
val dir = context.getExternalFilesDir(DIR) ?: context.filesDir.subdir(DIR)
|
||||||
|
File(dir, "$name.log")
|
||||||
|
}
|
||||||
|
val isEnabled: Boolean
|
||||||
|
get() = settings.isLoggingEnabled
|
||||||
|
private val dateFormat = SimpleDateFormat.getDateTimeInstance(
|
||||||
|
SimpleDateFormat.SHORT,
|
||||||
|
SimpleDateFormat.SHORT,
|
||||||
|
Locale.ROOT,
|
||||||
|
)
|
||||||
|
private val buffer = ConcurrentLinkedQueue<String>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private var flushJob: Job? = null
|
||||||
|
|
||||||
|
fun log(message: String, e: Throwable? = null) {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val text = buildString {
|
||||||
|
append(dateFormat.format(Date()))
|
||||||
|
append(": ")
|
||||||
|
if (e != null) {
|
||||||
|
append("E!")
|
||||||
|
}
|
||||||
|
append(message)
|
||||||
|
if (e != null) {
|
||||||
|
append(' ')
|
||||||
|
append(e.stackTraceToString())
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.add(text)
|
||||||
|
postFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun flush() {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flushJob?.cancelAndJoin()
|
||||||
|
flushImpl()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postFlush() {
|
||||||
|
if (flushJob?.isActive == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flushJob = processLifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
delay(FLUSH_DELAY)
|
||||||
|
runCatchingCancellable {
|
||||||
|
flushImpl()
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTraceDebug()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun flushImpl() {
|
||||||
|
mutex.withLock {
|
||||||
|
if (buffer.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runInterruptible(Dispatchers.IO) {
|
||||||
|
if (file.length() > MAX_SIZE_BYTES) {
|
||||||
|
rotate()
|
||||||
|
}
|
||||||
|
FileOutputStream(file, true).use {
|
||||||
|
while (true) {
|
||||||
|
val message = buffer.poll() ?: break
|
||||||
|
it.write(message.toByteArray())
|
||||||
|
it.write('\n'.code)
|
||||||
|
}
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun rotate() {
|
||||||
|
val length = file.length()
|
||||||
|
val bakFile = File(file.parentFile, file.name + ".bak")
|
||||||
|
file.renameTo(bakFile)
|
||||||
|
bakFile.inputStream().use { input ->
|
||||||
|
input.skip(length - MAX_SIZE_BYTES / 2)
|
||||||
|
file.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bakFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.koitharu.kotatsu.core.logs
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class TrackerLogger
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package org.koitharu.kotatsu.core.logs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.collection.arraySetOf
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dagger.multibindings.ElementsIntoSet
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object LoggersModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@TrackerLogger
|
||||||
|
fun provideTrackerLogger(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
settings: AppSettings,
|
||||||
|
) = FileLogger(context, settings, "tracker")
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ElementsIntoSet
|
||||||
|
fun provideAllLoggers(
|
||||||
|
@TrackerLogger trackerLogger: FileLogger,
|
||||||
|
): Set<@JvmSuppressWildcards FileLogger> = arraySetOf(
|
||||||
|
trackerLogger,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
fun MangaSource.getLocaleTitle(): String? {
|
fun MangaSource.getLocaleTitle(): String? {
|
||||||
val lc = Locale(locale ?: return null)
|
val lc = Locale(locale ?: return null)
|
||||||
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
fun MangaSource(name: String): MangaSource {
|
||||||
fun MangaSource(name: String): MangaSource? {
|
|
||||||
MangaSource.values().forEach {
|
MangaSource.values().forEach {
|
||||||
if (it.name == name) return it
|
if (it.name == name) return it
|
||||||
}
|
}
|
||||||
return null
|
return MangaSource.DUMMY
|
||||||
}
|
}
|
||||||
@@ -13,11 +13,15 @@ private const val SERVER_CLOUDFLARE = "cloudflare"
|
|||||||
class CloudFlareInterceptor : Interceptor {
|
class CloudFlareInterceptor : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val response = chain.proceed(chain.request())
|
val request = chain.request()
|
||||||
|
val response = chain.proceed(request)
|
||||||
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
|
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
|
||||||
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
|
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
|
||||||
response.closeQuietly()
|
response.closeQuietly()
|
||||||
throw CloudFlareProtectedException(response.request.url.toString())
|
throw CloudFlareProtectedException(
|
||||||
|
url = response.request.url.toString(),
|
||||||
|
headers = request.headers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import java.util.*
|
import dagger.Lazy
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class UserAgentInterceptor : Interceptor {
|
@Singleton
|
||||||
|
class UserAgentInterceptor @Inject constructor(
|
||||||
|
private val mangaRepositoryFactoryLazy: Lazy<MangaRepository.Factory>,
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
return chain.proceed(
|
return chain.proceed(
|
||||||
if (request.header(CommonHeaders.USER_AGENT) == null) {
|
if (request.header(CommonHeaders.USER_AGENT) == null) {
|
||||||
request.newBuilder()
|
request.newBuilder()
|
||||||
.addHeader(CommonHeaders.USER_AGENT, userAgent)
|
.addHeader(CommonHeaders.USER_AGENT, getUserAgent(request))
|
||||||
.build()
|
.build()
|
||||||
} else request
|
} else request,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUserAgent(request: Request): String {
|
||||||
|
val source = request.tag(MangaSource::class.java) ?: return userAgent
|
||||||
|
val repository = mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository
|
||||||
|
return repository?.userAgent ?: userAgent
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val userAgent
|
val userAgent
|
||||||
@@ -28,8 +44,8 @@ class UserAgentInterceptor : Interceptor {
|
|||||||
Build.MODEL,
|
Build.MODEL,
|
||||||
Build.BRAND,
|
Build.BRAND,
|
||||||
Build.DEVICE,
|
Build.DEVICE,
|
||||||
Locale.getDefault().language
|
Locale.getDefault().language,
|
||||||
)
|
) // TODO Decide what to do with this afterwards
|
||||||
|
|
||||||
val userAgentChrome
|
val userAgentChrome
|
||||||
get() = (
|
get() = (
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
package org.koitharu.kotatsu.core.network.cookies
|
||||||
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import javax.inject.Inject
|
import androidx.annotation.WorkerThread
|
||||||
import javax.inject.Singleton
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
@Singleton
|
class AndroidCookieJar : MutableCookieJar {
|
||||||
class AndroidCookieJar @Inject constructor() : CookieJar {
|
|
||||||
|
|
||||||
private val cookieManager = CookieManager.getInstance()
|
private val cookieManager = CookieManager.getInstance()
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
val rawCookie = cookieManager.getCookie(url.toString()) ?: return emptyList()
|
val rawCookie = cookieManager.getCookie(url.toString()) ?: return emptyList()
|
||||||
return rawCookie.split(';').mapNotNull {
|
return rawCookie.split(';').mapNotNull {
|
||||||
@@ -21,6 +19,7 @@ class AndroidCookieJar @Inject constructor() : CookieJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
if (cookies.isEmpty()) {
|
if (cookies.isEmpty()) {
|
||||||
return
|
return
|
||||||
@@ -31,7 +30,7 @@ class AndroidCookieJar @Inject constructor() : CookieJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
|
override suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
|
||||||
cookieManager.removeAllCookies(continuation::resume)
|
cookieManager.removeAllCookies(continuation::resume)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.cookies
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.ObjectInputStream
|
||||||
|
import java.io.ObjectOutputStream
|
||||||
|
|
||||||
|
|
||||||
|
class CookieWrapper(
|
||||||
|
val cookie: Cookie,
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(encodedString: String) : this(
|
||||||
|
ObjectInputStream(ByteArrayInputStream(Base64.decode(encodedString, Base64.NO_WRAP))).use {
|
||||||
|
val name = it.readUTF()
|
||||||
|
val value = it.readUTF()
|
||||||
|
val expiresAt = it.readLong()
|
||||||
|
val domain = it.readUTF()
|
||||||
|
val path = it.readUTF()
|
||||||
|
val secure = it.readBoolean()
|
||||||
|
val httpOnly = it.readBoolean()
|
||||||
|
val persistent = it.readBoolean()
|
||||||
|
val hostOnly = it.readBoolean()
|
||||||
|
Cookie.Builder().also { c ->
|
||||||
|
c.name(name)
|
||||||
|
c.value(value)
|
||||||
|
if (persistent) {
|
||||||
|
c.expiresAt(expiresAt)
|
||||||
|
}
|
||||||
|
if (hostOnly) {
|
||||||
|
c.hostOnlyDomain(domain)
|
||||||
|
} else {
|
||||||
|
c.domain(domain)
|
||||||
|
}
|
||||||
|
c.path(path)
|
||||||
|
if (secure) {
|
||||||
|
c.secure()
|
||||||
|
}
|
||||||
|
if (httpOnly) {
|
||||||
|
c.httpOnly()
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fun encode(): String {
|
||||||
|
val output = ByteArrayOutputStream()
|
||||||
|
ObjectOutputStream(output).use {
|
||||||
|
it.writeUTF(cookie.name)
|
||||||
|
it.writeUTF(cookie.value)
|
||||||
|
it.writeLong(cookie.expiresAt)
|
||||||
|
it.writeUTF(cookie.domain)
|
||||||
|
it.writeUTF(cookie.path)
|
||||||
|
it.writeBoolean(cookie.secure)
|
||||||
|
it.writeBoolean(cookie.httpOnly)
|
||||||
|
it.writeBoolean(cookie.persistent)
|
||||||
|
it.writeBoolean(cookie.hostOnly)
|
||||||
|
}
|
||||||
|
return Base64.encodeToString(output.toByteArray(), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
|
||||||
|
|
||||||
|
fun key(): String {
|
||||||
|
return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as CookieWrapper
|
||||||
|
|
||||||
|
if (cookie != other.cookie) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return cookie.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.cookies
|
||||||
|
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
interface MutableCookieJar : CookieJar {
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie>
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
|
||||||
|
|
||||||
|
suspend fun clear(): Boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network.cookies
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.collection.ArrayMap
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
|
||||||
|
private const val PREFS_NAME = "cookies"
|
||||||
|
|
||||||
|
class PreferencesCookieJar(
|
||||||
|
context: Context,
|
||||||
|
) : MutableCookieJar {
|
||||||
|
|
||||||
|
private val cache = ArrayMap<String, CookieWrapper>()
|
||||||
|
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
private var isLoaded = false
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
|
loadPersistent()
|
||||||
|
val expired = HashSet<String>()
|
||||||
|
val result = ArrayList<Cookie>()
|
||||||
|
for ((key, cookie) in cache) {
|
||||||
|
if (cookie.isExpired()) {
|
||||||
|
expired += key
|
||||||
|
} else if (cookie.cookie.matches(url)) {
|
||||||
|
result += cookie.cookie
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expired.isNotEmpty()) {
|
||||||
|
cache.removeAll(expired)
|
||||||
|
removePersistent(expired)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
val wrapped = cookies.map { CookieWrapper(it) }
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
for (cookie in wrapped) {
|
||||||
|
val key = cookie.key()
|
||||||
|
cache[key] = cookie
|
||||||
|
if (cookie.cookie.persistent) {
|
||||||
|
putString(key, cookie.encode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear(): Boolean {
|
||||||
|
cache.clear()
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
prefs.edit(commit = true) { clear() }
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun loadPersistent() {
|
||||||
|
if (!isLoaded) {
|
||||||
|
val map = prefs.all
|
||||||
|
cache.ensureCapacity(map.size)
|
||||||
|
for ((k, v) in map) {
|
||||||
|
val cookie = try {
|
||||||
|
CookieWrapper(v as String)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cache[k] = cookie
|
||||||
|
}
|
||||||
|
isLoaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removePersistent(keys: Collection<String>) {
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
for (key in keys) {
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.koitharu.kotatsu.core.os
|
||||||
|
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.ConnectivityManager.NetworkCallback
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.koitharu.kotatsu.utils.MediatorStateFlow
|
||||||
|
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
|
||||||
|
|
||||||
|
class NetworkState(
|
||||||
|
private val connectivityManager: ConnectivityManager,
|
||||||
|
) : MediatorStateFlow<Boolean>(connectivityManager.isNetworkAvailable) {
|
||||||
|
|
||||||
|
private val callback = NetworkCallbackImpl()
|
||||||
|
|
||||||
|
override fun onActive() {
|
||||||
|
invalidate()
|
||||||
|
val request = NetworkRequest.Builder().build()
|
||||||
|
connectivityManager.registerNetworkCallback(request, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInactive() {
|
||||||
|
connectivityManager.unregisterNetworkCallback(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun awaitForConnection() {
|
||||||
|
if (value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
first { it }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidate() {
|
||||||
|
publishValue(connectivityManager.isNetworkAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class NetworkCallbackImpl : NetworkCallback() {
|
||||||
|
|
||||||
|
override fun onAvailable(network: Network) = invalidate()
|
||||||
|
|
||||||
|
override fun onLost(network: Network) = invalidate()
|
||||||
|
|
||||||
|
override fun onUnavailable() = invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,6 +118,7 @@ class ShortcutsUpdater @Inject constructor(
|
|||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(manga.coverUrl)
|
.data(manga.coverUrl)
|
||||||
.size(iconSize.width, iconSize.height)
|
.size(iconSize.width, iconSize.height)
|
||||||
|
.tag(manga.source)
|
||||||
.precision(Precision.EXACT)
|
.precision(Precision.EXACT)
|
||||||
.scale(Scale.FILL)
|
.scale(Scale.FILL)
|
||||||
.build(),
|
.build(),
|
||||||
|
|||||||
@@ -6,25 +6,25 @@ import android.util.Base64
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.ext.toList
|
import org.koitharu.kotatsu.utils.ext.toList
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class MangaLoaderContextImpl @Inject constructor(
|
class MangaLoaderContextImpl @Inject constructor(
|
||||||
override val httpClient: OkHttpClient,
|
override val httpClient: OkHttpClient,
|
||||||
override val cookieJar: AndroidCookieJar,
|
override val cookieJar: MutableCookieJar,
|
||||||
@ApplicationContext private val androidContext: Context,
|
@ApplicationContext private val androidContext: Context,
|
||||||
) : MangaLoaderContext() {
|
) : MangaLoaderContext() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||||
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
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.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.*
|
import java.util.EnumMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
|
|
||||||
interface MangaRepository {
|
interface MangaRepository {
|
||||||
|
|
||||||
@@ -31,6 +37,7 @@ interface MangaRepository {
|
|||||||
class Factory @Inject constructor(
|
class Factory @Inject constructor(
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val loaderContext: MangaLoaderContext,
|
private val loaderContext: MangaLoaderContext,
|
||||||
|
private val contentCache: ContentCache,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val cache = EnumMap<MangaSource, WeakReference<RemoteMangaRepository>>(MangaSource::class.java)
|
private val cache = EnumMap<MangaSource, WeakReference<RemoteMangaRepository>>(MangaSource::class.java)
|
||||||
@@ -42,7 +49,7 @@ interface MangaRepository {
|
|||||||
cache[source]?.get()?.let { return it }
|
cache[source]?.get()?.let { return it }
|
||||||
return synchronized(cache) {
|
return synchronized(cache) {
|
||||||
cache[source]?.get()?.let { return it }
|
cache[source]?.get()?.let { return it }
|
||||||
val repository = RemoteMangaRepository(MangaParser(source, loaderContext))
|
val repository = RemoteMangaRepository(MangaParser(source, loaderContext), contentCache)
|
||||||
cache[source] = WeakReference(repository)
|
cache[source] = WeakReference(repository)
|
||||||
repository
|
repository
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||||
|
import org.koitharu.kotatsu.core.cache.SafeDeferred
|
||||||
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
import org.koitharu.kotatsu.parsers.model.Favicons
|
||||||
|
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.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
|
||||||
class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
class RemoteMangaRepository(
|
||||||
|
private val parser: MangaParser,
|
||||||
|
private val cache: ContentCache,
|
||||||
|
) : MangaRepository {
|
||||||
|
|
||||||
override val source: MangaSource
|
override val source: MangaSource
|
||||||
get() = parser.source
|
get() = parser.source
|
||||||
@@ -20,6 +40,9 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
|||||||
getConfig().defaultSortOrder = value
|
getConfig().defaultSortOrder = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val userAgent: String?
|
||||||
|
get() = parser.headers?.get(CommonHeaders.USER_AGENT)
|
||||||
|
|
||||||
override suspend fun getList(offset: Int, query: String): List<Manga> {
|
override suspend fun getList(offset: Int, query: String): List<Manga> {
|
||||||
return parser.getList(offset, query)
|
return parser.getList(offset, query)
|
||||||
}
|
}
|
||||||
@@ -28,9 +51,23 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
|||||||
return parser.getList(offset, tags, sortOrder)
|
return parser.getList(offset, tags, sortOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = parser.getDetails(manga)
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
cache.getDetails(source, manga.url)?.let { return it }
|
||||||
|
val details = asyncSafe {
|
||||||
|
parser.getDetails(manga)
|
||||||
|
}
|
||||||
|
cache.putDetails(source, manga.url, details)
|
||||||
|
return details.await()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = parser.getPages(chapter)
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
cache.getPages(source, chapter.url)?.let { return it }
|
||||||
|
val pages = asyncSafe {
|
||||||
|
parser.getPages(chapter)
|
||||||
|
}
|
||||||
|
cache.putPages(source, chapter.url, pages)
|
||||||
|
return pages.await()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
|
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
|
||||||
|
|
||||||
@@ -45,4 +82,16 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getConfig() = parser.config as SourceSettings
|
private fun getConfig() = parser.config as SourceSettings
|
||||||
|
|
||||||
|
private suspend fun <T> asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred<T> {
|
||||||
|
var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
|
||||||
|
if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
|
||||||
|
dispatcher = Dispatchers.Default
|
||||||
|
}
|
||||||
|
return SafeDeferred(
|
||||||
|
processLifecycleScope.async(dispatcher) {
|
||||||
|
runCatchingCancellable { block() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,6 @@ import coil.network.HttpException
|
|||||||
import coil.request.Options
|
import coil.request.Options
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import coil.size.pxOrElse
|
import coil.size.pxOrElse
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -27,6 +26,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
|||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
private const val FALLBACK_SIZE = 9999 // largest icon
|
private const val FALLBACK_SIZE = 9999 // largest icon
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class FaviconFetcher(
|
|||||||
options.size.height.pxOrElse { FALLBACK_SIZE },
|
options.size.height.pxOrElse { FALLBACK_SIZE },
|
||||||
)
|
)
|
||||||
val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" }
|
val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" }
|
||||||
val response = loadIcon(icon.url, favicons.referer)
|
val response = loadIcon(icon.url, repo.userAgent, favicons.referer)
|
||||||
val responseBody = response.requireBody()
|
val responseBody = response.requireBody()
|
||||||
val source = writeToDiskCache(responseBody)?.toImageSource() ?: responseBody.toImageSource()
|
val source = writeToDiskCache(responseBody)?.toImageSource() ?: responseBody.toImageSource()
|
||||||
return SourceResult(
|
return SourceResult(
|
||||||
@@ -63,11 +63,14 @@ class FaviconFetcher(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadIcon(url: String, referer: String): Response {
|
private suspend fun loadIcon(url: String, userAgent: String?, referer: String): Response {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.get()
|
.get()
|
||||||
.header(CommonHeaders.REFERER, referer)
|
.header(CommonHeaders.REFERER, referer)
|
||||||
|
if (userAgent != null) {
|
||||||
|
request.header(CommonHeaders.USER_AGENT, userAgent)
|
||||||
|
}
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) }
|
options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) }
|
||||||
val response = okHttpClient.newCall(request.build()).await()
|
val response = okHttpClient.newCall(request.build()).await()
|
||||||
@@ -150,7 +153,7 @@ class FaviconFetcher(
|
|||||||
|
|
||||||
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
|
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
|
||||||
return if (data.scheme == URI_SCHEME_FAVICON) {
|
return if (data.scheme == URI_SCHEME_FAVICON) {
|
||||||
val mangaSource = MangaSource(data.schemeSpecificPart) ?: return null
|
val mangaSource = MangaSource(data.schemeSpecificPart)
|
||||||
FaviconFetcher(okHttpClient, diskCache, mangaSource, options, mangaRepositoryFactory)
|
FaviconFetcher(okHttpClient, diskCache, mangaSource, options, mangaRepositoryFactory)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.prefs
|
|
||||||
|
|
||||||
enum class AppSection {
|
|
||||||
|
|
||||||
LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS
|
|
||||||
}
|
|
||||||
@@ -2,35 +2,40 @@ package org.koitharu.kotatsu.core.prefs
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.collection.arraySetOf
|
import androidx.collection.arraySetOf
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.File
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.shelf.domain.ShelfSection
|
||||||
|
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||||
import org.koitharu.kotatsu.utils.ext.getEnumValue
|
import org.koitharu.kotatsu.utils.ext.getEnumValue
|
||||||
import org.koitharu.kotatsu.utils.ext.observe
|
import org.koitharu.kotatsu.utils.ext.observe
|
||||||
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
||||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||||
|
import java.io.File
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.EnumSet
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
private val connectivityManager = context.connectivityManager
|
||||||
|
|
||||||
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
|
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
|
||||||
remove(MangaSource.LOCAL)
|
remove(MangaSource.LOCAL)
|
||||||
@@ -42,14 +47,26 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val remoteMangaSources: Set<MangaSource>
|
val remoteMangaSources: Set<MangaSource>
|
||||||
get() = Collections.unmodifiableSet(remoteSources)
|
get() = Collections.unmodifiableSet(remoteSources)
|
||||||
|
|
||||||
|
var shelfSections: List<ShelfSection>
|
||||||
|
get() {
|
||||||
|
val raw = prefs.getString(KEY_SHELF_SECTIONS, null)
|
||||||
|
val values = enumValues<ShelfSection>()
|
||||||
|
if (raw.isNullOrEmpty()) {
|
||||||
|
return values.toList()
|
||||||
|
}
|
||||||
|
return raw.split('|')
|
||||||
|
.mapNotNull { values.getOrNull(it.toIntOrNull() ?: -1) }
|
||||||
|
.distinct()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
val raw = value.joinToString("|") { it.ordinal.toString() }
|
||||||
|
prefs.edit { putString(KEY_SHELF_SECTIONS, raw) }
|
||||||
|
}
|
||||||
|
|
||||||
var listMode: ListMode
|
var listMode: ListMode
|
||||||
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
|
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
|
||||||
|
|
||||||
var defaultSection: AppSection
|
|
||||||
get() = prefs.getEnumValue(KEY_APP_SECTION, AppSection.HISTORY)
|
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_APP_SECTION, value) }
|
|
||||||
|
|
||||||
val theme: Int
|
val theme: Int
|
||||||
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
|
|
||||||
@@ -63,6 +80,17 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getInt(KEY_GRID_SIZE, 100)
|
get() = prefs.getInt(KEY_GRID_SIZE, 100)
|
||||||
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
|
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
|
||||||
|
|
||||||
|
var appLocales: LocaleListCompat
|
||||||
|
get() {
|
||||||
|
val raw = prefs.getString(KEY_APP_LOCALE, null)
|
||||||
|
return LocaleListCompat.forLanguageTags(raw)
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
prefs.edit {
|
||||||
|
putString(KEY_APP_LOCALE, value.toLanguageTags())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val readerPageSwitch: Set<String>
|
val readerPageSwitch: Set<String>
|
||||||
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
|
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
|
||||||
|
|
||||||
@@ -131,6 +159,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getString(KEY_APP_PASSWORD, null)
|
get() = prefs.getString(KEY_APP_PASSWORD, null)
|
||||||
set(value) = prefs.edit { if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD) }
|
set(value) = prefs.edit { if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD) }
|
||||||
|
|
||||||
|
val isLoggingEnabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
|
||||||
|
|
||||||
var isBiometricProtectionEnabled: Boolean
|
var isBiometricProtectionEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true)
|
get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) }
|
set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) }
|
||||||
@@ -141,6 +172,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isDynamicShortcutsEnabled: Boolean
|
val isDynamicShortcutsEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_SHORTCUTS, true)
|
get() = prefs.getBoolean(KEY_SHORTCUTS, true)
|
||||||
|
|
||||||
|
fun isContentPrefetchEnabled(): Boolean {
|
||||||
|
val policy = NetworkPolicy.from(prefs.getString(KEY_PREFETCH_CONTENT, null), NetworkPolicy.NEVER)
|
||||||
|
return policy.isNetworkAllowed(connectivityManager)
|
||||||
|
}
|
||||||
|
|
||||||
var sourcesOrder: List<String>
|
var sourcesOrder: List<String>
|
||||||
get() = prefs.getString(KEY_SOURCES_ORDER, null)
|
get() = prefs.getString(KEY_SOURCES_ORDER, null)
|
||||||
?.split('|')
|
?.split('|')
|
||||||
@@ -206,6 +242,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isReaderBarEnabled: Boolean
|
val isReaderBarEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_READER_BAR, true)
|
get() = prefs.getBoolean(KEY_READER_BAR, true)
|
||||||
|
|
||||||
|
val isReaderSliderEnabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
||||||
|
|
||||||
val dnsOverHttps: DoHProvider
|
val dnsOverHttps: DoHProvider
|
||||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||||
|
|
||||||
@@ -213,12 +252,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
|
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
|
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
|
||||||
|
|
||||||
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
|
val isWebtoonZoomEnable: Boolean
|
||||||
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
|
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
|
||||||
NETWORK_ALWAYS -> true
|
|
||||||
NETWORK_NEVER -> false
|
fun isPagesPreloadEnabled(): Boolean {
|
||||||
else -> cm.isActiveNetworkMetered
|
val policy = NetworkPolicy.from(prefs.getString(KEY_PAGES_PRELOAD, null), NetworkPolicy.NON_METERED)
|
||||||
}
|
return policy.isNetworkAllowed(connectivityManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
|
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
|
||||||
@@ -272,7 +311,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val TRACK_FAVOURITES = "favourites"
|
const val TRACK_FAVOURITES = "favourites"
|
||||||
|
|
||||||
const val KEY_LIST_MODE = "list_mode_2"
|
const val KEY_LIST_MODE = "list_mode_2"
|
||||||
const val KEY_APP_SECTION = "app_section_2"
|
|
||||||
const val KEY_THEME = "theme"
|
const val KEY_THEME = "theme"
|
||||||
const val KEY_DYNAMIC_THEME = "dynamic_theme"
|
const val KEY_DYNAMIC_THEME = "dynamic_theme"
|
||||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
const val KEY_THEME_AMOLED = "amoled_theme"
|
||||||
@@ -328,16 +366,19 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_INCOGNITO_MODE = "incognito"
|
const val KEY_INCOGNITO_MODE = "incognito"
|
||||||
const val KEY_SYNC = "sync"
|
const val KEY_SYNC = "sync"
|
||||||
const val KEY_READER_BAR = "reader_bar"
|
const val KEY_READER_BAR = "reader_bar"
|
||||||
|
const val KEY_READER_SLIDER = "reader_slider"
|
||||||
const val KEY_SHORTCUTS = "dynamic_shortcuts"
|
const val KEY_SHORTCUTS = "dynamic_shortcuts"
|
||||||
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
|
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
|
||||||
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
||||||
|
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||||
|
const val KEY_SHELF_SECTIONS = "shelf_sections_2"
|
||||||
|
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||||
|
const val KEY_APP_LOCALE = "app_locale"
|
||||||
|
const val KEY_LOGGING_ENABLED = "logging"
|
||||||
|
const val KEY_LOGS_SHARE = "logs_share"
|
||||||
|
|
||||||
// About
|
// About
|
||||||
const val KEY_APP_UPDATE = "app_update"
|
const val KEY_APP_UPDATE = "app_update"
|
||||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||||
|
|
||||||
private const val NETWORK_NEVER = 0
|
|
||||||
private const val NETWORK_ALWAYS = 1
|
|
||||||
private const val NETWORK_NON_METERED = 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.core.prefs
|
||||||
|
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
|
||||||
|
enum class NetworkPolicy(
|
||||||
|
private val key: Int,
|
||||||
|
) {
|
||||||
|
|
||||||
|
NEVER(0),
|
||||||
|
ALWAYS(1),
|
||||||
|
NON_METERED(2);
|
||||||
|
|
||||||
|
fun isNetworkAllowed(cm: ConnectivityManager) = when (this) {
|
||||||
|
NEVER -> false
|
||||||
|
ALWAYS -> true
|
||||||
|
NON_METERED -> !cm.isActiveNetworkMetered
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun from(key: String?, default: NetworkPolicy): NetworkPolicy {
|
||||||
|
val intKey = key?.toIntOrNull() ?: return default
|
||||||
|
return enumValues<NetworkPolicy>().find { it.key == intKey } ?: default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
|
|||||||
override fun <T> get(key: ConfigKey<T>): T {
|
override fun <T> get(key: ConfigKey<T>): T {
|
||||||
return when (key) {
|
return when (key) {
|
||||||
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
|
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
|
||||||
|
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
|
||||||
} as T
|
} as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ class MangaErrorDialog : AlertDialogFragment<DialogMangaErrorBinding>() {
|
|||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.report) { _, _ ->
|
.setPositiveButton(R.string.report) { _, _ ->
|
||||||
dismiss()
|
dismiss()
|
||||||
error.report(TAG)
|
error.report()
|
||||||
}.setTitle(R.string.error_occurred)
|
}.setTitle(R.string.error_occurred)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,10 +52,13 @@ class ZipOutput(
|
|||||||
return if (entryNames.add(entry.name)) {
|
return if (entryNames.add(entry.name)) {
|
||||||
val zipEntry = ZipEntry(entry.name)
|
val zipEntry = ZipEntry(entry.name)
|
||||||
output.putNextEntry(zipEntry)
|
output.putNextEntry(zipEntry)
|
||||||
|
try {
|
||||||
other.getInputStream(entry).use { input ->
|
other.getInputStream(entry).use { input ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
output.closeEntry()
|
output.closeEntry()
|
||||||
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package org.koitharu.kotatsu.details.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
|
import org.koitharu.kotatsu.base.ui.CoroutineIntentService
|
||||||
|
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||||
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaChapters
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
|
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.getParcelableExtraCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MangaPrefetchService : CoroutineIntentService() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var cache: ContentCache
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var historyRepository: HistoryRepository
|
||||||
|
|
||||||
|
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
ACTION_PREFETCH_DETAILS -> prefetchDetails(
|
||||||
|
manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return,
|
||||||
|
)
|
||||||
|
|
||||||
|
ACTION_PREFETCH_PAGES -> prefetchPages(
|
||||||
|
chapter = intent.getParcelableExtraCompat<ParcelableMangaChapters>(EXTRA_CHAPTER)
|
||||||
|
?.chapters?.singleOrNull() ?: return,
|
||||||
|
)
|
||||||
|
|
||||||
|
ACTION_PREFETCH_LAST -> prefetchLast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(startId: Int, error: Throwable) = Unit
|
||||||
|
|
||||||
|
private suspend fun prefetchDetails(manga: Manga) {
|
||||||
|
val source = mangaRepositoryFactory.create(manga.source)
|
||||||
|
runCatchingCancellable { source.getDetails(manga) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun prefetchPages(chapter: MangaChapter) {
|
||||||
|
val source = mangaRepositoryFactory.create(chapter.source)
|
||||||
|
runCatchingCancellable { source.getPages(chapter) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun prefetchLast() {
|
||||||
|
val last = historyRepository.getLastOrNull() ?: return
|
||||||
|
if (last.source == MangaSource.LOCAL) return
|
||||||
|
val repo = mangaRepositoryFactory.create(last.source)
|
||||||
|
val details = runCatchingCancellable { repo.getDetails(last) }.getOrNull() ?: return
|
||||||
|
val chapters = details.chapters
|
||||||
|
if (chapters.isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val history = historyRepository.getOne(last)
|
||||||
|
val chapter = if (history == null) {
|
||||||
|
chapters.firstOrNull()
|
||||||
|
} else {
|
||||||
|
chapters.find { x -> x.id == history.chapterId } ?: chapters.firstOrNull()
|
||||||
|
} ?: return
|
||||||
|
runCatchingCancellable { repo.getPages(chapter) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val EXTRA_MANGA = "manga"
|
||||||
|
private const val EXTRA_CHAPTER = "manga"
|
||||||
|
private const val ACTION_PREFETCH_DETAILS = "details"
|
||||||
|
private const val ACTION_PREFETCH_PAGES = "pages"
|
||||||
|
private const val ACTION_PREFETCH_LAST = "last"
|
||||||
|
|
||||||
|
fun prefetchDetails(context: Context, manga: Manga) {
|
||||||
|
if (!isPrefetchAvailable(context, manga.source)) return
|
||||||
|
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||||
|
intent.action = ACTION_PREFETCH_DETAILS
|
||||||
|
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prefetchPages(context: Context, chapter: MangaChapter) {
|
||||||
|
if (!isPrefetchAvailable(context, chapter.source)) return
|
||||||
|
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||||
|
intent.action = ACTION_PREFETCH_PAGES
|
||||||
|
intent.putExtra(EXTRA_CHAPTER, ParcelableMangaChapters(listOf(chapter)))
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prefetchLast(context: Context) {
|
||||||
|
if (!isPrefetchAvailable(context, null)) return
|
||||||
|
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||||
|
intent.action = ACTION_PREFETCH_LAST
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
|
||||||
|
if (source == MangaSource.LOCAL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val entryPoint = EntryPointAccessors.fromApplication(context, PrefetchCompanionEntryPoint::class.java)
|
||||||
|
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.details.service
|
||||||
|
|
||||||
|
import dagger.hilt.EntryPoint
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
|
||||||
|
@EntryPoint
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface PrefetchCompanionEntryPoint {
|
||||||
|
val settings: AppSettings
|
||||||
|
val contentCache: ContentCache
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
package org.koitharu.kotatsu.details.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
||||||
@@ -24,8 +25,8 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
|||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
|
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
|
||||||
import org.koitharu.kotatsu.utils.ext.parents
|
|
||||||
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ChaptersFragment :
|
class ChaptersFragment :
|
||||||
BaseFragment<FragmentChaptersBinding>(),
|
BaseFragment<FragmentChaptersBinding>(),
|
||||||
@@ -102,6 +103,7 @@ class ChaptersFragment :
|
|||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_delete -> {
|
R.id.action_delete -> {
|
||||||
val ids = selectionController?.peekCheckedIds()
|
val ids = selectionController?.peekCheckedIds()
|
||||||
val manga = viewModel.manga.value
|
val manga = viewModel.manga.value
|
||||||
@@ -120,6 +122,7 @@ class ChaptersFragment :
|
|||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_select_range -> {
|
R.id.action_select_range -> {
|
||||||
val items = chaptersAdapter?.items ?: return false
|
val items = chaptersAdapter?.items ?: return false
|
||||||
val ids = HashSet(controller.peekCheckedIds())
|
val ids = HashSet(controller.peekCheckedIds())
|
||||||
@@ -139,11 +142,20 @@ class ChaptersFragment :
|
|||||||
controller.addAll(ids)
|
controller.addAll(ids)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_select_all -> {
|
R.id.action_select_all -> {
|
||||||
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
|
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
|
||||||
selectionController?.addAll(ids)
|
controller.addAll(ids)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.action_mark_current -> {
|
||||||
|
val id = controller.peekCheckedIds().singleOrNull() ?: return false
|
||||||
|
viewModel.markChapterAsCurrent(id)
|
||||||
|
mode.finish()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,6 +176,7 @@ class ChaptersFragment :
|
|||||||
x.chapter.source == MangaSource.LOCAL
|
x.chapter.source == MangaSource.LOCAL
|
||||||
}
|
}
|
||||||
menu.findItem(R.id.action_select_all).isVisible = items.size < allItems.size
|
menu.findItem(R.id.action_select_all).isVisible = items.size < allItems.size
|
||||||
|
menu.findItem(R.id.action_mark_current).isVisible = items.size == 1
|
||||||
mode.title = items.size.toString()
|
mode.title = items.size.toString()
|
||||||
var hasGap = false
|
var hasGap = false
|
||||||
for (i in 0 until items.size - 1) {
|
for (i in 0 until items.size - 1) {
|
||||||
|
|||||||
@@ -18,13 +18,12 @@ import androidx.core.graphics.Insets
|
|||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.badge.BadgeDrawable
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
import org.koitharu.kotatsu.base.domain.MangaIntent
|
||||||
@@ -35,14 +34,21 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|||||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||||
import org.koitharu.kotatsu.core.ui.MangaErrorDialog
|
import org.koitharu.kotatsu.core.ui.MangaErrorDialog
|
||||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
||||||
|
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
||||||
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.bindBadge
|
|
||||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ViewBadge
|
||||||
|
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.isReportable
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setNavigationBarTransparentCompat
|
||||||
|
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DetailsActivity :
|
class DetailsActivity :
|
||||||
@@ -60,7 +66,7 @@ class DetailsActivity :
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||||
|
|
||||||
private var badge: BadgeDrawable? = null
|
private lateinit var viewBadge: ViewBadge
|
||||||
|
|
||||||
private val viewModel: DetailsViewModel by assistedViewModels {
|
private val viewModel: DetailsViewModel by assistedViewModels {
|
||||||
viewModelFactory.create(MangaIntent(intent))
|
viewModelFactory.create(MangaIntent(intent))
|
||||||
@@ -83,6 +89,7 @@ class DetailsActivity :
|
|||||||
}
|
}
|
||||||
binding.buttonRead.setOnClickListener(this)
|
binding.buttonRead.setOnClickListener(this)
|
||||||
binding.buttonDropdown.setOnClickListener(this)
|
binding.buttonDropdown.setOnClickListener(this)
|
||||||
|
viewBadge = ViewBadge(binding.buttonRead, this)
|
||||||
|
|
||||||
chaptersMenuProvider = if (binding.layoutBottom != null) {
|
chaptersMenuProvider = if (binding.layoutBottom != null) {
|
||||||
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom))
|
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom))
|
||||||
@@ -116,6 +123,7 @@ class DetailsActivity :
|
|||||||
viewModel.branches.observe(this) {
|
viewModel.branches.observe(this) {
|
||||||
binding.buttonDropdown.isVisible = it.size > 1
|
binding.buttonDropdown.isVisible = it.size > 1
|
||||||
}
|
}
|
||||||
|
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||||
|
|
||||||
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
|
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
|
||||||
addMenuProvider(
|
addMenuProvider(
|
||||||
@@ -151,6 +159,7 @@ class DetailsActivity :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.button_dropdown -> showBranchPopupMenu()
|
R.id.button_dropdown -> showBranchPopupMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,10 +197,12 @@ class DetailsActivity :
|
|||||||
ExceptionResolver.canResolve(e) -> {
|
ExceptionResolver.canResolve(e) -> {
|
||||||
resolveError(e)
|
resolveError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
manga == null -> {
|
manga == null -> {
|
||||||
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val snackbar = makeSnackbar(
|
val snackbar = makeSnackbar(
|
||||||
e.getDisplayMessage(resources),
|
e.getDisplayMessage(resources),
|
||||||
@@ -221,18 +232,18 @@ class DetailsActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onHistoryChanged(info: HistoryInfo?) {
|
private fun onHistoryChanged(info: HistoryInfo) {
|
||||||
with(binding.buttonRead) {
|
with(binding.buttonRead) {
|
||||||
if (info?.history != null) {
|
if (info.history != null) {
|
||||||
setText(R.string._continue)
|
setText(R.string._continue)
|
||||||
setIconResource(R.drawable.ic_play)
|
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
|
||||||
} else {
|
} else {
|
||||||
setText(R.string.read)
|
setText(R.string.read)
|
||||||
setIconResource(R.drawable.ic_read)
|
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val text = when {
|
val text = when {
|
||||||
info == null -> getString(R.string.loading_)
|
!info.isValid -> getString(R.string.loading_)
|
||||||
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters)
|
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters)
|
||||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
||||||
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
||||||
@@ -242,7 +253,7 @@ class DetailsActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onNewChaptersChanged(newChapters: Int) {
|
private fun onNewChaptersChanged(newChapters: Int) {
|
||||||
badge = binding.buttonRead.bindBadge(badge, newChapters)
|
viewBadge.counter = newChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChapterMissingDialog(chapterId: Long) {
|
fun showChapterMissingDialog(chapterId: Long) {
|
||||||
@@ -318,6 +329,24 @@ class DetailsActivity :
|
|||||||
return sb
|
return sb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class PrefetchObserver(
|
||||||
|
private val context: Context,
|
||||||
|
) : Observer<List<ChapterListItem>> {
|
||||||
|
|
||||||
|
private var isCalled = false
|
||||||
|
|
||||||
|
override fun onChanged(t: List<ChapterListItem>?) {
|
||||||
|
if (t.isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isCalled) {
|
||||||
|
isCalled = true
|
||||||
|
val item = t.find { it.hasFlag(ChapterListItem.FLAG_CURRENT) } ?: t.first()
|
||||||
|
MangaPrefetchService.prefetchPages(context, item.chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newIntent(context: Context, manga: Manga): Intent {
|
fun newIntent(context: Context, manga: Manga): Intent {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import coil.request.ImageRequest
|
|||||||
import coil.util.CoilUtils
|
import coil.util.CoilUtils
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
@@ -27,9 +26,9 @@ import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
|||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
|
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
|
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
|
||||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
|
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||||
@@ -45,8 +44,20 @@ import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
|||||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||||
import org.koitharu.kotatsu.utils.FileSize
|
import org.koitharu.kotatsu.utils.FileSize
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.computeSize
|
||||||
|
import org.koitharu.kotatsu.utils.ext.crossfade
|
||||||
|
import org.koitharu.kotatsu.utils.ext.drawableTop
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty
|
||||||
|
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||||
|
import org.koitharu.kotatsu.utils.ext.referer
|
||||||
|
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||||
|
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||||
|
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||||
|
import org.koitharu.kotatsu.utils.ext.toFileOrNull
|
||||||
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DetailsFragment :
|
class DetailsFragment :
|
||||||
@@ -75,7 +86,7 @@ class DetailsFragment :
|
|||||||
binding.chipsTags.onChipClickListener = this
|
binding.chipsTags.onChipClickListener = this
|
||||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||||
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
|
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||||
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
|
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
|
||||||
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
|
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
|
||||||
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
||||||
@@ -123,12 +134,14 @@ class DetailsFragment :
|
|||||||
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_finished)
|
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_finished)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MangaState.ONGOING -> {
|
MangaState.ONGOING -> {
|
||||||
infoLayout.textViewState.apply {
|
infoLayout.textViewState.apply {
|
||||||
textAndVisible = resources.getString(R.string.state_ongoing)
|
textAndVisible = resources.getString(R.string.state_ongoing)
|
||||||
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_ongoing)
|
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_ongoing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> infoLayout.textViewState.isVisible = false
|
else -> infoLayout.textViewState.isVisible = false
|
||||||
}
|
}
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
if (manga.source == MangaSource.LOCAL) {
|
||||||
@@ -178,8 +191,8 @@ class DetailsFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onHistoryChanged(history: MangaHistory?) {
|
private fun onHistoryChanged(history: HistoryInfo) {
|
||||||
binding.progressView.setPercent(history?.percent ?: PROGRESS_NONE, animate = true)
|
binding.progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
@@ -229,6 +242,7 @@ class DetailsFragment :
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.textView_source -> {
|
R.id.textView_source -> {
|
||||||
startActivity(
|
startActivity(
|
||||||
MangaListActivity.newIntent(
|
MangaListActivity.newIntent(
|
||||||
@@ -237,9 +251,14 @@ class DetailsFragment :
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.imageView_cover -> {
|
R.id.imageView_cover -> {
|
||||||
startActivity(
|
startActivity(
|
||||||
ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }),
|
ImageActivity.newIntent(
|
||||||
|
v.context,
|
||||||
|
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
|
||||||
|
manga.source,
|
||||||
|
),
|
||||||
scaleUpActivityOptionsOf(v).toBundle(),
|
scaleUpActivityOptionsOf(v).toBundle(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -249,7 +268,7 @@ class DetailsFragment :
|
|||||||
override fun onLongClick(v: View): Boolean {
|
override fun onLongClick(v: View): Boolean {
|
||||||
when (v.id) {
|
when (v.id) {
|
||||||
R.id.button_read -> {
|
R.id.button_read -> {
|
||||||
if (viewModel.readingHistory.value == null) {
|
if (viewModel.historyInfo.value?.history == null) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val menu = PopupMenu(v.context, v)
|
val menu = PopupMenu(v.context, v)
|
||||||
@@ -271,12 +290,14 @@ class DetailsFragment :
|
|||||||
)
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
menu.show()
|
menu.show()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,6 +341,7 @@ class DetailsFragment :
|
|||||||
.target(binding.imageViewCover)
|
.target(binding.imageViewCover)
|
||||||
.size(CoverSizeResolver(binding.imageViewCover))
|
.size(CoverSizeResolver(binding.imageViewCover))
|
||||||
.data(imageUrl)
|
.data(imageUrl)
|
||||||
|
.tag(manga.source)
|
||||||
.crossfade(context)
|
.crossfade(context)
|
||||||
.referer(manga.publicUrl)
|
.referer(manga.publicUrl)
|
||||||
.lifecycle(viewLifecycleOwner)
|
.lifecycle(viewLifecycleOwner)
|
||||||
|
|||||||
@@ -13,10 +13,18 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import java.io.IOException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
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.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
@@ -46,6 +54,7 @@ import org.koitharu.kotatsu.utils.asFlowLiveData
|
|||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class DetailsViewModel @AssistedInject constructor(
|
class DetailsViewModel @AssistedInject constructor(
|
||||||
@Assisted intent: MangaIntent,
|
@Assisted intent: MangaIntent,
|
||||||
@@ -80,8 +89,14 @@ class DetailsViewModel @AssistedInject constructor(
|
|||||||
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
|
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||||
|
|
||||||
private val newChapters = trackingRepository.observeNewChaptersCount(delegate.mangaId)
|
private val newChapters = settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
.flatMapLatest { isEnabled ->
|
||||||
|
if (isEnabled) {
|
||||||
|
trackingRepository.observeNewChaptersCount(delegate.mangaId)
|
||||||
|
} else {
|
||||||
|
flowOf(0)
|
||||||
|
}
|
||||||
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||||
|
|
||||||
private val chaptersQuery = MutableStateFlow("")
|
private val chaptersQuery = MutableStateFlow("")
|
||||||
|
|
||||||
@@ -91,17 +106,18 @@ class DetailsViewModel @AssistedInject constructor(
|
|||||||
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
|
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
|
||||||
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
|
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
|
||||||
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
|
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
|
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
|
||||||
|
|
||||||
val historyInfo = combine(
|
val historyInfo: LiveData<HistoryInfo> = combine(
|
||||||
delegate.manga,
|
delegate.manga,
|
||||||
history,
|
history,
|
||||||
) { m, h ->
|
historyRepository.observeShouldSkip(delegate.manga),
|
||||||
HistoryInfo(m, h)
|
) { m, h, im ->
|
||||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null)
|
HistoryInfo(m, h, im)
|
||||||
|
}.asFlowLiveData(
|
||||||
|
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||||
|
defaultValue = HistoryInfo(null, null, false),
|
||||||
|
)
|
||||||
|
|
||||||
val bookmarks = delegate.manga.flatMapLatest {
|
val bookmarks = delegate.manga.flatMapLatest {
|
||||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
||||||
@@ -265,6 +281,17 @@ class DetailsViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun markChapterAsCurrent(chapterId: Long) {
|
||||||
|
launchJob(Dispatchers.Default) {
|
||||||
|
val manga = checkNotNull(delegate.manga.value)
|
||||||
|
val chapters = checkNotNull(manga.chapters)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||||
delegate.doLoad()
|
delegate.doLoad()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun chapterListItemAD(
|
|||||||
when (item.status) {
|
when (item.status) {
|
||||||
FLAG_UNREAD -> {
|
FLAG_UNREAD -> {
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
|
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse))
|
binding.textViewNumber.setTextColor(context.getThemeColor(com.google.android.material.R.attr.colorOnTertiary))
|
||||||
}
|
}
|
||||||
FLAG_CURRENT -> {
|
FLAG_CURRENT -> {
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
|
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ class ChaptersAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSectionText(context: Context, position: Int): CharSequence {
|
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||||
return items[position].chapter.number.toString()
|
val item = items.getOrNull(position) ?: return null
|
||||||
|
return item.chapter.number.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,12 @@ class HistoryInfo(
|
|||||||
val totalChapters: Int,
|
val totalChapters: Int,
|
||||||
val currentChapter: Int,
|
val currentChapter: Int,
|
||||||
val history: MangaHistory?,
|
val history: MangaHistory?,
|
||||||
|
val isIncognitoMode: Boolean,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val isValid: Boolean
|
||||||
|
get() = totalChapters >= 0
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@@ -18,6 +22,7 @@ class HistoryInfo(
|
|||||||
if (totalChapters != other.totalChapters) return false
|
if (totalChapters != other.totalChapters) return false
|
||||||
if (currentChapter != other.currentChapter) return false
|
if (currentChapter != other.currentChapter) return false
|
||||||
if (history != other.history) return false
|
if (history != other.history) return false
|
||||||
|
if (isIncognitoMode != other.isIncognitoMode) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -26,20 +31,21 @@ class HistoryInfo(
|
|||||||
var result = totalChapters
|
var result = totalChapters
|
||||||
result = 31 * result + currentChapter
|
result = 31 * result + currentChapter
|
||||||
result = 31 * result + (history?.hashCode() ?: 0)
|
result = 31 * result + (history?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + isIncognitoMode.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
fun HistoryInfo(manga: Manga?, history: MangaHistory?, isIncognitoMode: Boolean): HistoryInfo {
|
||||||
fun HistoryInfo(manga: Manga?, history: MangaHistory?): HistoryInfo? {
|
val chapters = manga?.chapters
|
||||||
val chapters = manga?.chapters ?: return null
|
|
||||||
return HistoryInfo(
|
return HistoryInfo(
|
||||||
totalChapters = chapters.size,
|
totalChapters = chapters?.size ?: -1,
|
||||||
currentChapter = if (history != null) {
|
currentChapter = if (history != null && !chapters.isNullOrEmpty()) {
|
||||||
chapters.indexOfFirst { it.id == history.chapterId }
|
chapters.indexOfFirst { it.id == history.chapterId }
|
||||||
} else {
|
} else {
|
||||||
-1
|
-1
|
||||||
},
|
},
|
||||||
history = history,
|
history = history,
|
||||||
|
isIncognitoMode = isIncognitoMode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ fun scrobblingInfoAD(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
|
binding.imageViewCover.newImageRequest(item.coverUrl /* TODO */, null)?.run {
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
fallback(R.drawable.ic_placeholder)
|
fallback(R.drawable.ic_placeholder)
|
||||||
error(R.drawable.ic_error_placeholder)
|
error(R.drawable.ic_error_placeholder)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ScrobblingInfoBottomSheet :
|
|||||||
R.id.imageView_cover -> {
|
R.id.imageView_cover -> {
|
||||||
val coverUrl = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.coverUrl ?: return
|
val coverUrl = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.coverUrl ?: return
|
||||||
val options = scaleUpActivityOptionsOf(v)
|
val options = scaleUpActivityOptionsOf(v)
|
||||||
startActivity(ImageActivity.newIntent(v.context, coverUrl), options.toBundle())
|
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,15 +115,13 @@ class ScrobblingInfoBottomSheet :
|
|||||||
binding.ratingBar.rating = scrobbling.rating * binding.ratingBar.numStars
|
binding.ratingBar.rating = scrobbling.rating * binding.ratingBar.numStars
|
||||||
binding.textViewDescription.text = scrobbling.description
|
binding.textViewDescription.text = scrobbling.description
|
||||||
binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
|
binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
|
||||||
ImageRequest.Builder(context ?: return)
|
binding.imageViewCover.newImageRequest(scrobbling.coverUrl)?.apply {
|
||||||
.target(binding.imageViewCover)
|
lifecycle(viewLifecycleOwner)
|
||||||
.data(scrobbling.coverUrl)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
.crossfade(context)
|
fallback(R.drawable.ic_placeholder)
|
||||||
.lifecycle(viewLifecycleOwner)
|
error(R.drawable.ic_error_placeholder)
|
||||||
.placeholder(R.drawable.ic_placeholder)
|
enqueueWith(coil)
|
||||||
.fallback(R.drawable.ic_placeholder)
|
}
|
||||||
.error(R.drawable.ic_error_placeholder)
|
|
||||||
.enqueueWith(coil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import kotlinx.coroutines.NonCancellable
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -36,6 +35,7 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||||
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.referer
|
import org.koitharu.kotatsu.utils.ext.referer
|
||||||
@@ -118,7 +118,7 @@ class DownloadManager @AssistedInject constructor(
|
|||||||
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
|
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
|
||||||
output = CbzMangaOutput.get(destination, data)
|
output = CbzMangaOutput.get(destination, data)
|
||||||
val coverUrl = data.largeCoverUrl ?: data.coverUrl
|
val coverUrl = data.largeCoverUrl ?: data.coverUrl
|
||||||
downloadFile(coverUrl, data.publicUrl, destination, tempFileName).let { file ->
|
downloadFile(coverUrl, data.publicUrl, destination, tempFileName, repo.source).let { file ->
|
||||||
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
|
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
|
||||||
}
|
}
|
||||||
val chapters = checkNotNull(
|
val chapters = checkNotNull(
|
||||||
@@ -139,7 +139,8 @@ class DownloadManager @AssistedInject constructor(
|
|||||||
for ((pageIndex, page) in pages.withIndex()) {
|
for ((pageIndex, page) in pages.withIndex()) {
|
||||||
runFailsafe(outState, pausingHandle) {
|
runFailsafe(outState, pausingHandle) {
|
||||||
val url = repo.getPageUrl(page)
|
val url = repo.getPageUrl(page)
|
||||||
val file = cache[url] ?: downloadFile(url, page.referer, destination, tempFileName)
|
val file = cache[url]
|
||||||
|
?: downloadFile(url, page.referer, destination, tempFileName, repo.source)
|
||||||
output.addPage(
|
output.addPage(
|
||||||
chapter = chapter,
|
chapter = chapter,
|
||||||
file = file,
|
file = file,
|
||||||
@@ -209,20 +210,25 @@ class DownloadManager @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadFile(url: String, referer: String, destination: File, tempFileName: String): File {
|
private suspend fun downloadFile(
|
||||||
|
url: String,
|
||||||
|
referer: String,
|
||||||
|
destination: File,
|
||||||
|
tempFileName: String,
|
||||||
|
source: MangaSource,
|
||||||
|
): File {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.header(CommonHeaders.REFERER, referer)
|
.header(CommonHeaders.REFERER, referer)
|
||||||
|
.tag(source)
|
||||||
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
val call = okHttp.newCall(request)
|
val call = okHttp.newCall(request)
|
||||||
val file = File(destination, tempFileName)
|
val file = File(destination, tempFileName)
|
||||||
val response = call.clone().await()
|
val response = call.clone().await()
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
file.outputStream().use { out ->
|
file.outputStream().use { out ->
|
||||||
checkNotNull(response.body).byteStream().copyTo(out)
|
checkNotNull(response.body).byteStream().copyToSuspending(out)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
@@ -245,6 +251,7 @@ class DownloadManager @AssistedInject constructor(
|
|||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(manga.coverUrl)
|
.data(manga.coverUrl)
|
||||||
.referer(manga.publicUrl)
|
.referer(manga.publicUrl)
|
||||||
|
.tag(manga.source)
|
||||||
.size(coverWidth, coverHeight)
|
.size(coverWidth, coverHeight)
|
||||||
.scale(Scale.FILL)
|
.scale(Scale.FILL)
|
||||||
.build(),
|
.build(),
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import org.koitharu.kotatsu.databinding.ItemDownloadBinding
|
|||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||||
import org.koitharu.kotatsu.parsers.util.format
|
import org.koitharu.kotatsu.parsers.util.format
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||||
|
import org.koitharu.kotatsu.utils.ext.referer
|
||||||
|
|
||||||
fun downloadItemAD(
|
fun downloadItemAD(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
@@ -40,7 +44,7 @@ fun downloadItemAD(
|
|||||||
bind {
|
bind {
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
job = item.progressAsFlow().onFirst { state ->
|
job = item.progressAsFlow().onFirst { state ->
|
||||||
binding.imageViewCover.newImageRequest(state.manga.coverUrl)?.run {
|
binding.imageViewCover.newImageRequest(state.manga.coverUrl, state.manga.source)?.run {
|
||||||
referer(state.manga.publicUrl)
|
referer(state.manga.publicUrl)
|
||||||
placeholder(state.cover)
|
placeholder(state.cover)
|
||||||
fallback(R.drawable.ic_placeholder)
|
fallback(R.drawable.ic_placeholder)
|
||||||
@@ -60,6 +64,7 @@ fun downloadItemAD(
|
|||||||
binding.buttonCancel.isVisible = false
|
binding.buttonCancel.isVisible = false
|
||||||
binding.buttonResume.isVisible = false
|
binding.buttonResume.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadState.Done -> {
|
is DownloadState.Done -> {
|
||||||
binding.textViewStatus.setText(R.string.download_complete)
|
binding.textViewStatus.setText(R.string.download_complete)
|
||||||
binding.progressBar.isIndeterminate = false
|
binding.progressBar.isIndeterminate = false
|
||||||
@@ -69,6 +74,7 @@ fun downloadItemAD(
|
|||||||
binding.buttonCancel.isVisible = false
|
binding.buttonCancel.isVisible = false
|
||||||
binding.buttonResume.isVisible = false
|
binding.buttonResume.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadState.Error -> {
|
is DownloadState.Error -> {
|
||||||
binding.textViewStatus.setText(R.string.error_occurred)
|
binding.textViewStatus.setText(R.string.error_occurred)
|
||||||
binding.progressBar.isIndeterminate = false
|
binding.progressBar.isIndeterminate = false
|
||||||
@@ -79,6 +85,7 @@ fun downloadItemAD(
|
|||||||
binding.buttonCancel.isVisible = state.canRetry
|
binding.buttonCancel.isVisible = state.canRetry
|
||||||
binding.buttonResume.isVisible = state.canRetry
|
binding.buttonResume.isVisible = state.canRetry
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadState.PostProcessing -> {
|
is DownloadState.PostProcessing -> {
|
||||||
binding.textViewStatus.setText(R.string.processing_)
|
binding.textViewStatus.setText(R.string.processing_)
|
||||||
binding.progressBar.isIndeterminate = true
|
binding.progressBar.isIndeterminate = true
|
||||||
@@ -88,6 +95,7 @@ fun downloadItemAD(
|
|||||||
binding.buttonCancel.isVisible = false
|
binding.buttonCancel.isVisible = false
|
||||||
binding.buttonResume.isVisible = false
|
binding.buttonResume.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadState.Preparing -> {
|
is DownloadState.Preparing -> {
|
||||||
binding.textViewStatus.setText(R.string.preparing_)
|
binding.textViewStatus.setText(R.string.preparing_)
|
||||||
binding.progressBar.isIndeterminate = true
|
binding.progressBar.isIndeterminate = true
|
||||||
@@ -97,6 +105,7 @@ fun downloadItemAD(
|
|||||||
binding.buttonCancel.isVisible = true
|
binding.buttonCancel.isVisible = true
|
||||||
binding.buttonResume.isVisible = false
|
binding.buttonResume.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadState.Progress -> {
|
is DownloadState.Progress -> {
|
||||||
binding.textViewStatus.setText(R.string.manga_downloading_)
|
binding.textViewStatus.setText(R.string.manga_downloading_)
|
||||||
binding.progressBar.isIndeterminate = false
|
binding.progressBar.isIndeterminate = false
|
||||||
@@ -109,6 +118,7 @@ fun downloadItemAD(
|
|||||||
binding.buttonCancel.isVisible = true
|
binding.buttonCancel.isVisible = true
|
||||||
binding.buttonResume.isVisible = false
|
binding.buttonResume.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadState.Queued -> {
|
is DownloadState.Queued -> {
|
||||||
binding.textViewStatus.setText(R.string.queued)
|
binding.textViewStatus.setText(R.string.queued)
|
||||||
binding.progressBar.isIndeterminate = false
|
binding.progressBar.isIndeterminate = false
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
|
|||||||
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
|
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
val adapter = DownloadsAdapter(lifecycleScope, coil)
|
val adapter = DownloadsAdapter(lifecycleScope, coil)
|
||||||
val spacing = resources.getDimensionPixelOffset(R.dimen.grid_spacing)
|
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||||
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
|
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.transformWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
@@ -85,7 +90,9 @@ class DownloadService : BaseService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
unregisterReceiver(controlReceiver)
|
unregisterReceiver(controlReceiver)
|
||||||
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
|
}
|
||||||
isRunning = false
|
isRunning = false
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@@ -169,6 +176,7 @@ class DownloadService : BaseService() {
|
|||||||
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
|
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
|
||||||
jobs[cancelId]?.cancel()
|
jobs[cancelId]?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTION_DOWNLOAD_RESUME -> {
|
ACTION_DOWNLOAD_RESUME -> {
|
||||||
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
|
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
|
||||||
jobs[cancelId]?.resume()
|
jobs[cancelId]?.resume()
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.explore.ui
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
@@ -11,11 +13,12 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.R
|
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.BaseFragment
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||||
|
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
|
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
|
||||||
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
|
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
@@ -31,6 +34,7 @@ import org.koitharu.kotatsu.search.ui.MangaListActivity
|
|||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ExploreFragment :
|
class ExploreFragment :
|
||||||
@@ -67,6 +71,7 @@ class ExploreFragment :
|
|||||||
}
|
}
|
||||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||||
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
|
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
|
||||||
|
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -95,6 +100,7 @@ class ExploreFragment :
|
|||||||
viewModel.openRandom()
|
viewModel.openRandom()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
@@ -105,6 +111,14 @@ class ExploreFragment :
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onItemLongClick(item: ExploreItem.Source, view: View): Boolean {
|
||||||
|
val menu = PopupMenu(view.context, view)
|
||||||
|
menu.inflate(R.menu.popup_source)
|
||||||
|
menu.setOnMenuItemClickListener(SourceMenuListener(item))
|
||||||
|
menu.show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onRetryClick(error: Throwable) = Unit
|
override fun onRetryClick(error: Throwable) = Unit
|
||||||
|
|
||||||
override fun onEmptyActionClick() = onManageClick(requireView())
|
override fun onEmptyActionClick() = onManageClick(requireView())
|
||||||
@@ -124,6 +138,37 @@ class ExploreFragment :
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(binding.recyclerView, action.stringResId, length)
|
||||||
|
if (handle != null) {
|
||||||
|
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
|
||||||
|
}
|
||||||
|
snackbar.anchorView = (activity as? BottomNavOwner)?.bottomNav
|
||||||
|
snackbar.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SourceMenuListener(
|
||||||
|
private val sourceItem: ExploreItem.Source,
|
||||||
|
) : PopupMenu.OnMenuItemClickListener {
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.action_settings -> {
|
||||||
|
startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), sourceItem.source))
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.action_hide -> {
|
||||||
|
viewModel.hideSource(sourceItem.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newInstance() = ExploreFragment()
|
fun newInstance() = ExploreFragment()
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
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 org.koitharu.kotatsu.R
|
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.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||||
@@ -16,6 +22,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ExploreViewModel @Inject constructor(
|
class ExploreViewModel @Inject constructor(
|
||||||
@@ -24,6 +31,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val onOpenManga = SingleLiveEvent<Manga>()
|
val onOpenManga = SingleLiveEvent<Manga>()
|
||||||
|
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||||
|
|
||||||
val content: LiveData<List<ExploreItem>> = isLoading.asFlow().flatMapLatest { loading ->
|
val content: LiveData<List<ExploreItem>> = isLoading.asFlow().flatMapLatest { loading ->
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -40,6 +48,16 @@ class ExploreViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideSource(source: MangaSource) {
|
||||||
|
launchJob(Dispatchers.Default) {
|
||||||
|
settings.hiddenSources += source.name
|
||||||
|
val rollback = ReversibleHandle {
|
||||||
|
settings.hiddenSources -= source.name
|
||||||
|
}
|
||||||
|
onActionDone.postCall(ReversibleAction(R.string.source_disabled, rollback))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createContentFlow() = settings.observe()
|
private fun createContentFlow() = settings.observe()
|
||||||
.filter {
|
.filter {
|
||||||
it == AppSettings.KEY_SOURCES_HIDDEN ||
|
it == AppSettings.KEY_SOURCES_HIDDEN ||
|
||||||
@@ -61,7 +79,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
sources.mapTo(result) { ExploreItem.Source(it) }
|
sources.mapTo(result) { ExploreItem.Source(it) }
|
||||||
} else {
|
} else {
|
||||||
result += ExploreItem.EmptyHint(
|
result += ExploreItem.EmptyHint(
|
||||||
icon = R.drawable.ic_empty_search,
|
icon = R.drawable.ic_empty_common,
|
||||||
textPrimary = R.string.no_manga_sources,
|
textPrimary = R.string.no_manga_sources,
|
||||||
textSecondary = R.string.no_manga_sources_text,
|
textSecondary = R.string.no_manga_sources_text,
|
||||||
actionStringRes = R.string.manage,
|
actionStringRes = R.string.manage,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.explore.ui.model
|
package org.koitharu.kotatsu.explore.ui.model
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
@@ -73,6 +72,7 @@ sealed interface ExploreItem : ListModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
class EmptyHint(
|
class EmptyHint(
|
||||||
@DrawableRes icon: Int,
|
@DrawableRes icon: Int,
|
||||||
@StringRes textPrimary: Int,
|
@StringRes textPrimary: Int,
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ abstract class FavouriteCategoriesDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
|
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
|
||||||
|
|
||||||
@Update
|
|
||||||
abstract suspend fun update(category: FavouriteCategoryEntity): Int
|
|
||||||
|
|
||||||
suspend fun delete(id: Long) = setDeletedAt(id, System.currentTimeMillis())
|
suspend fun delete(id: Long) = setDeletedAt(id, System.currentTimeMillis())
|
||||||
|
|
||||||
@Query("UPDATE favourite_categories SET title = :title, `order` = :order, `track` = :tracker WHERE category_id = :id")
|
@Query("UPDATE favourite_categories SET title = :title, `order` = :order, `track` = :tracker WHERE category_id = :id")
|
||||||
@@ -51,12 +48,8 @@ abstract class FavouriteCategoriesDao {
|
|||||||
return (getMaxSortKey() ?: 0) + 1
|
return (getMaxSortKey() ?: 0) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transaction
|
@Upsert
|
||||||
open suspend fun upsert(entity: FavouriteCategoryEntity) {
|
abstract suspend fun upsert(entity: FavouriteCategoryEntity)
|
||||||
if (update(entity) == 0) {
|
|
||||||
insert(entity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Query("UPDATE favourite_categories SET deleted_at = :deletedAt WHERE category_id = :id")
|
@Query("UPDATE favourite_categories SET deleted_at = :deletedAt WHERE category_id = :id")
|
||||||
protected abstract suspend fun setDeletedAt(id: Long, deletedAt: Long)
|
protected abstract suspend fun setDeletedAt(id: Long, deletedAt: Long)
|
||||||
|
|||||||
@@ -99,11 +99,6 @@ abstract class FavouritesDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
abstract suspend fun insert(favourite: FavouriteEntity)
|
abstract suspend fun insert(favourite: FavouriteEntity)
|
||||||
|
|
||||||
/** UPDATE **/
|
|
||||||
|
|
||||||
@Update
|
|
||||||
abstract suspend fun update(favourite: FavouriteEntity): Int
|
|
||||||
|
|
||||||
/** DELETE **/
|
/** DELETE **/
|
||||||
|
|
||||||
suspend fun delete(mangaId: Long) = setDeletedAt(
|
suspend fun delete(mangaId: Long) = setDeletedAt(
|
||||||
@@ -138,12 +133,8 @@ abstract class FavouritesDao {
|
|||||||
|
|
||||||
/** TOOLS **/
|
/** TOOLS **/
|
||||||
|
|
||||||
@Transaction
|
@Upsert
|
||||||
open suspend fun upsert(entity: FavouriteEntity) {
|
abstract suspend fun upsert(entity: FavouriteEntity)
|
||||||
if (update(entity) == 0) {
|
|
||||||
insert(entity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@RawQuery(observedEntities = [FavouriteEntity::class])
|
@RawQuery(observedEntities = [FavouriteEntity::class])
|
||||||
@@ -166,6 +157,7 @@ abstract class FavouritesDao {
|
|||||||
SortOrder.NEWEST,
|
SortOrder.NEWEST,
|
||||||
SortOrder.UPDATED,
|
SortOrder.UPDATED,
|
||||||
-> "created_at DESC"
|
-> "created_at DESC"
|
||||||
|
|
||||||
SortOrder.ALPHABETICAL -> "title ASC"
|
SortOrder.ALPHABETICAL -> "title ASC"
|
||||||
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
|
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
|
|||||||
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
|
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
|
||||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
||||||
|
import kotlin.text.Typography.dagger
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
|
class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
|
||||||
@@ -28,6 +29,7 @@ class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
|
|||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
if (fm.findFragmentById(R.id.container) == null) {
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
fm.commit {
|
fm.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
val fragment = FavouritesListFragment.newInstance(intent.getLongExtra(EXTRA_CATEGORY_ID, NO_ID))
|
val fragment = FavouritesListFragment.newInstance(intent.getLongExtra(EXTRA_CATEGORY_ID, NO_ID))
|
||||||
replace(R.id.container, fragment)
|
replace(R.id.container, fragment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import android.graphics.Color
|
|||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.*
|
import android.view.View.OnClickListener
|
||||||
|
import android.view.View.OnLongClickListener
|
||||||
|
import android.view.View.OnTouchListener
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
@@ -16,7 +18,11 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
|
||||||
|
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
|
||||||
fun categoryAD(
|
fun categoryAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
|
|||||||
@@ -7,15 +7,16 @@ import android.view.View
|
|||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.core.ui.titleRes
|
import org.koitharu.kotatsu.core.ui.titleRes
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener {
|
class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener {
|
||||||
@@ -23,9 +24,7 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: FavouritesListViewModel.Factory
|
lateinit var viewModelFactory: FavouritesListViewModel.Factory
|
||||||
|
|
||||||
override val viewModel by assistedViewModels<FavouritesListViewModel> {
|
override val viewModel by assistedViewModels { viewModelFactory.create(categoryId) }
|
||||||
viewModelFactory.create(categoryId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val categoryId: Long
|
private val categoryId: Long
|
||||||
get() = arguments?.getLong(ARG_CATEGORY_ID) ?: NO_ID
|
get() = arguments?.getLong(ARG_CATEGORY_ID) ?: NO_ID
|
||||||
@@ -34,6 +33,9 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
if (viewModel.categoryId != NO_ID) {
|
||||||
|
addMenuProvider(FavouritesListMenuProvider(view.context, viewModel))
|
||||||
|
}
|
||||||
viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() }
|
viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
|
|||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onActionItemClicked(controller, mode, item)
|
else -> super.onActionItemClicked(controller, mode, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package org.koitharu.kotatsu.favourites.ui.list
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import androidx.core.view.forEach
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.titleRes
|
||||||
|
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||||
|
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
|
||||||
|
class FavouritesListMenuProvider(
|
||||||
|
private val context: Context,
|
||||||
|
private val viewModel: FavouritesListViewModel,
|
||||||
|
) : MenuProvider {
|
||||||
|
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.opt_favourites, menu)
|
||||||
|
val subMenu = menu.findItem(R.id.action_order)?.subMenu ?: return
|
||||||
|
for (order in FavouriteCategoriesActivity.SORT_ORDERS) {
|
||||||
|
subMenu.add(R.id.group_order, Menu.NONE, order.ordinal, order.titleRes)
|
||||||
|
}
|
||||||
|
subMenu.setGroupCheckable(R.id.group_order, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareMenu(menu: Menu) {
|
||||||
|
super.onPrepareMenu(menu)
|
||||||
|
val order = viewModel.sortOrder.value ?: return
|
||||||
|
menu.findItem(R.id.action_order)?.subMenu?.forEach { item ->
|
||||||
|
if (item.order == order.ordinal) {
|
||||||
|
item.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
if (menuItem.groupId == R.id.group_order) {
|
||||||
|
val order = enumValues<SortOrder>()[menuItem.order]
|
||||||
|
viewModel.setSortOrder(order)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return when (menuItem.itemId) {
|
||||||
|
R.id.action_edit -> {
|
||||||
|
context.startActivity(
|
||||||
|
FavouritesCategoryEditActivity.newIntent(context, viewModel.categoryId),
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
|||||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||||
|
|
||||||
class FavouritesListViewModel @AssistedInject constructor(
|
class FavouritesListViewModel @AssistedInject constructor(
|
||||||
@Assisted private val categoryId: Long,
|
@Assisted val categoryId: Long,
|
||||||
private val repository: FavouritesRepository,
|
private val repository: FavouritesRepository,
|
||||||
private val trackingRepository: TrackingRepository,
|
private val trackingRepository: TrackingRepository,
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
@@ -55,7 +55,7 @@ class FavouritesListViewModel @AssistedInject constructor(
|
|||||||
} else {
|
} else {
|
||||||
repository.observeAll(categoryId)
|
repository.observeAll(categoryId)
|
||||||
},
|
},
|
||||||
createListModeFlow(),
|
listModeFlow,
|
||||||
) { list, mode ->
|
) { list, mode ->
|
||||||
when {
|
when {
|
||||||
list.isEmpty() -> listOf(
|
list.isEmpty() -> listOf(
|
||||||
@@ -117,7 +117,11 @@ class FavouritesListViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getCounter(mangaId: Long): Int {
|
override suspend fun getCounter(mangaId: Long): Int {
|
||||||
return trackingRepository.getNewChaptersCount(mangaId)
|
return if (settings.isTrackerEnabled) {
|
||||||
|
trackingRepository.getNewChaptersCount(mangaId)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getProgress(mangaId: Long): Float {
|
override suspend fun getProgress(mangaId: Long): Float {
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
package org.koitharu.kotatsu.history.domain
|
package org.koitharu.kotatsu.history.domain
|
||||||
|
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.entity.*
|
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toMangaTag
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||||
import org.koitharu.kotatsu.history.data.toMangaHistory
|
import org.koitharu.kotatsu.history.data.toMangaHistory
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -18,6 +26,7 @@ import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
|||||||
import org.koitharu.kotatsu.scrobbling.domain.tryScrobble
|
import org.koitharu.kotatsu.scrobbling.domain.tryScrobble
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val PROGRESS_NONE = -1f
|
const val PROGRESS_NONE = -1f
|
||||||
|
|
||||||
@@ -66,7 +75,7 @@ class HistoryRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int, percent: Float) {
|
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int, percent: Float) {
|
||||||
if (manga.isNsfw && settings.isHistoryExcludeNsfw || settings.isIncognitoModeEnabled) {
|
if (shouldSkip(manga)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val tags = manga.tags.toEntities()
|
val tags = manga.tags.toEntities()
|
||||||
@@ -138,6 +147,30 @@ class HistoryRepository @Inject constructor(
|
|||||||
return db.historyDao.findPopularTags(limit).map { x -> x.toMangaTag() }
|
return db.historyDao.findPopularTags(limit).map { x -> x.toMangaTag() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shouldSkip(manga: Manga): Boolean {
|
||||||
|
return manga.isNsfw && settings.isHistoryExcludeNsfw || settings.isIncognitoModeEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeShouldSkip(manga: Manga): Flow<Boolean> {
|
||||||
|
return settings.observe()
|
||||||
|
.filter { key -> key == AppSettings.KEY_INCOGNITO_MODE || key == AppSettings.KEY_HISTORY_EXCLUDE_NSFW }
|
||||||
|
.onStart { emit("") }
|
||||||
|
.map { shouldSkip(manga) }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observeShouldSkip(mangaFlow: Flow<Manga?>): Flow<Boolean> {
|
||||||
|
return mangaFlow
|
||||||
|
.distinctUntilChangedBy { it?.isNsfw }
|
||||||
|
.flatMapLatest { m ->
|
||||||
|
if (m != null) {
|
||||||
|
observeShouldSkip(m)
|
||||||
|
} else {
|
||||||
|
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun recover(ids: Collection<Long>) {
|
private suspend fun recover(ids: Collection<Long>) {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
for (id in ids) {
|
for (id in ids) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
|
import kotlin.text.Typography.dagger
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class HistoryActivity :
|
class HistoryActivity :
|
||||||
@@ -28,6 +29,7 @@ class HistoryActivity :
|
|||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
if (fm.findFragmentById(R.id.container) == null) {
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
fm.commit {
|
fm.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
val fragment = HistoryListFragment.newInstance()
|
val fragment = HistoryListFragment.newInstance()
|
||||||
replace(R.id.container, fragment)
|
replace(R.id.container, fragment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ class HistoryListAdapter(
|
|||||||
listener: MangaListListener
|
listener: MangaListListener
|
||||||
) : MangaListAdapter(coil, lifecycleOwner, listener), FastScroller.SectionIndexer {
|
) : MangaListAdapter(coil, lifecycleOwner, listener), FastScroller.SectionIndexer {
|
||||||
|
|
||||||
override fun getSectionText(context: Context, position: Int): CharSequence {
|
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||||
val list = items
|
val list = items
|
||||||
for (i in (0..position).reversed()) {
|
for (i in (0..position).reversed()) {
|
||||||
val item = list[i]
|
val item = list.getOrNull(i) ?: continue
|
||||||
if (item is DateTimeAgo) {
|
if (item is DateTimeAgo) {
|
||||||
return item.format(context.resources)
|
return item.format(context.resources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,6 @@ package org.koitharu.kotatsu.history.ui
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -21,11 +18,20 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|||||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.*
|
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.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HistoryListViewModel @Inject constructor(
|
class HistoryListViewModel @Inject constructor(
|
||||||
@@ -42,7 +48,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
override val content = combine(
|
override val content = combine(
|
||||||
repository.observeAllWithHistory(),
|
repository.observeAllWithHistory(),
|
||||||
historyGrouping,
|
historyGrouping,
|
||||||
createListModeFlow(),
|
listModeFlow,
|
||||||
) { list, grouped, mode ->
|
) { list, grouped, mode ->
|
||||||
when {
|
when {
|
||||||
list.isEmpty() -> listOf(
|
list.isEmpty() -> listOf(
|
||||||
@@ -53,6 +59,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
actionStringRes = 0,
|
actionStringRes = 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> mapList(list, grouped, mode)
|
else -> mapList(list, grouped, mode)
|
||||||
}
|
}
|
||||||
}.onStart {
|
}.onStart {
|
||||||
@@ -103,7 +110,11 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
prevDate = date
|
prevDate = date
|
||||||
}
|
}
|
||||||
val counter = trackingRepository.getNewChaptersCount(manga.id)
|
val counter = if (settings.isTrackerEnabled) {
|
||||||
|
trackingRepository.getNewChaptersCount(manga.id)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
val percent = if (showPercent) history.percent else PROGRESS_NONE
|
val percent = if (showPercent) history.percent else PROGRESS_NONE
|
||||||
result += when (mode) {
|
result += when (mode) {
|
||||||
ListMode.LIST -> manga.toListModel(counter, percent)
|
ListMode.LIST -> manga.toListModel(counter, percent)
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ import coil.target.ViewTarget
|
|||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.databinding.ActivityImageBinding
|
import org.koitharu.kotatsu.databinding.ActivityImageBinding
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.utils.ext.indicator
|
import org.koitharu.kotatsu.utils.ext.indicator
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ImageActivity : BaseActivity<ActivityImageBinding>() {
|
class ImageActivity : BaseActivity<ActivityImageBinding>() {
|
||||||
@@ -56,6 +57,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>() {
|
|||||||
.data(url)
|
.data(url)
|
||||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||||
.lifecycle(this)
|
.lifecycle(this)
|
||||||
|
.tag(intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource)
|
||||||
.target(SsivTarget(binding.ssiv))
|
.target(SsivTarget(binding.ssiv))
|
||||||
.indicator(binding.progressBar)
|
.indicator(binding.progressBar)
|
||||||
.enqueueWith(coil)
|
.enqueueWith(coil)
|
||||||
@@ -88,9 +90,12 @@ class ImageActivity : BaseActivity<ActivityImageBinding>() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun newIntent(context: Context, url: String): Intent {
|
private const val EXTRA_SOURCE = "source"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, url: String, source: MangaSource?): Intent {
|
||||||
return Intent(context, ImageActivity::class.java)
|
return Intent(context, ImageActivity::class.java)
|
||||||
.setData(Uri.parse(url))
|
.setData(Uri.parse(url))
|
||||||
|
.putExtra(EXTRA_SOURCE, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,23 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.CheckableButtonGroup
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.databinding.DialogListModeBinding
|
import org.koitharu.kotatsu.databinding.DialogListModeBinding
|
||||||
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
import org.koitharu.kotatsu.utils.ext.setValueRounded
|
||||||
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
|
import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ListModeSelectDialog :
|
class ListModeBottomSheet :
|
||||||
AlertDialogFragment<DialogListModeBinding>(),
|
BaseBottomSheet<DialogListModeBinding>(),
|
||||||
CheckableButtonGroup.OnCheckedChangeListener,
|
Slider.OnChangeListener,
|
||||||
Slider.OnChangeListener {
|
MaterialButtonToggleGroup.OnButtonCheckedListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settings: AppSettings
|
lateinit var settings: AppSettings
|
||||||
@@ -33,13 +32,6 @@ class ListModeSelectDialog :
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
) = DialogListModeBinding.inflate(inflater, container, false)
|
) = DialogListModeBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
|
|
||||||
return super.onBuildDialog(builder)
|
|
||||||
.setTitle(R.string.list_mode)
|
|
||||||
.setPositiveButton(R.string.done, null)
|
|
||||||
.setCancelable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
val mode = settings.listMode
|
val mode = settings.listMode
|
||||||
@@ -53,10 +45,13 @@ class ListModeSelectDialog :
|
|||||||
binding.sliderGrid.setValueRounded(settings.gridSize.toFloat())
|
binding.sliderGrid.setValueRounded(settings.gridSize.toFloat())
|
||||||
binding.sliderGrid.addOnChangeListener(this)
|
binding.sliderGrid.addOnChangeListener(this)
|
||||||
|
|
||||||
binding.checkableGroup.onCheckedChangeListener = this
|
binding.checkableGroup.addOnButtonCheckedListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int) {
|
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
|
||||||
|
if (!isChecked) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val mode = when (checkedId) {
|
val mode = when (checkedId) {
|
||||||
R.id.button_list -> ListMode.LIST
|
R.id.button_list -> ListMode.LIST
|
||||||
R.id.button_list_detailed -> ListMode.DETAILED_LIST
|
R.id.button_list_detailed -> ListMode.DETAILED_LIST
|
||||||
@@ -78,6 +73,6 @@ class ListModeSelectDialog :
|
|||||||
|
|
||||||
private const val TAG = "ListModeSelectDialog"
|
private const val TAG = "ListModeSelectDialog"
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG)
|
fun show(fm: FragmentManager) = ListModeBottomSheet().show(fm, TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package org.koitharu.kotatsu.list.ui
|
package org.koitharu.kotatsu.list.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
@@ -15,7 +19,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
import org.koitharu.kotatsu.base.domain.reverseAsync
|
||||||
@@ -42,13 +45,23 @@ import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
|||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||||
|
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
import org.koitharu.kotatsu.utils.ShareHelper
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||||
|
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
|
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||||
|
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||||
|
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||||
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
abstract class MangaListFragment :
|
abstract class MangaListFragment :
|
||||||
@@ -138,6 +151,20 @@ abstract class MangaListFragment :
|
|||||||
return selectionController?.onItemLongClick(item.id) ?: false
|
return selectionController?.onItemLongClick(item.id) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onReadClick(manga: Manga, view: View) {
|
||||||
|
if (selectionController?.onItemClick(manga.id) != true) {
|
||||||
|
val intent = ReaderActivity.newIntent(context ?: return, manga)
|
||||||
|
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
|
||||||
|
if (selectionController?.onItemClick(manga.id) != true) {
|
||||||
|
val intent = MangaListActivity.newIntent(context ?: return, setOf(tag))
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
binding.swipeRefreshLayout.isRefreshing = true
|
binding.swipeRefreshLayout.isRefreshing = true
|
||||||
@@ -150,7 +177,7 @@ abstract class MangaListFragment :
|
|||||||
|
|
||||||
private fun onError(e: Throwable) {
|
private fun onError(e: Throwable) {
|
||||||
if (e is CloudFlareProtectedException) {
|
if (e is CloudFlareProtectedException) {
|
||||||
CloudFlareDialog.newInstance(e.url).show(childFragmentManager, CloudFlareDialog.TAG)
|
CloudFlareDialog.newInstance(e.url, e.headers).show(childFragmentManager, CloudFlareDialog.TAG)
|
||||||
} else {
|
} else {
|
||||||
Snackbar.make(
|
Snackbar.make(
|
||||||
binding.recyclerView,
|
binding.recyclerView,
|
||||||
@@ -251,12 +278,14 @@ abstract class MangaListFragment :
|
|||||||
)
|
)
|
||||||
addItemDecoration(decoration)
|
addItemDecoration(decoration)
|
||||||
}
|
}
|
||||||
|
|
||||||
ListMode.DETAILED_LIST -> {
|
ListMode.DETAILED_LIST -> {
|
||||||
layoutManager = FitHeightLinearLayoutManager(context)
|
layoutManager = FitHeightLinearLayoutManager(context)
|
||||||
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||||
updatePadding(left = spacing, right = spacing)
|
updatePadding(left = spacing, right = spacing)
|
||||||
addItemDecoration(SpacingItemDecoration(spacing))
|
addItemDecoration(SpacingItemDecoration(spacing))
|
||||||
}
|
}
|
||||||
|
|
||||||
ListMode.GRID -> {
|
ListMode.GRID -> {
|
||||||
layoutManager = FitHeightGridLayoutManager(context, checkNotNull(spanResolver).spanCount).also {
|
layoutManager = FitHeightGridLayoutManager(context, checkNotNull(spanResolver).spanCount).also {
|
||||||
it.spanSizeLookup = spanSizeLookup
|
it.spanSizeLookup = spanSizeLookup
|
||||||
@@ -284,21 +313,25 @@ abstract class MangaListFragment :
|
|||||||
selectionController?.addAll(ids)
|
selectionController?.addAll(ids)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_share -> {
|
R.id.action_share -> {
|
||||||
ShareHelper(requireContext()).shareMangaLinks(selectedItems)
|
ShareHelper(requireContext()).shareMangaLinks(selectedItems)
|
||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_favourite -> {
|
R.id.action_favourite -> {
|
||||||
FavouriteCategoriesBottomSheet.show(childFragmentManager, selectedItems)
|
FavouriteCategoriesBottomSheet.show(childFragmentManager, selectedItems)
|
||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_save -> {
|
R.id.action_save -> {
|
||||||
DownloadService.confirmAndStart(requireContext(), selectedItems)
|
DownloadService.confirmAndStart(requireContext(), selectedItems)
|
||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ class MangaListMenuProvider(
|
|||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||||
R.id.action_list_mode -> {
|
R.id.action_list_mode -> {
|
||||||
ListModeSelectDialog.show(fragment.childFragmentManager)
|
ListModeBottomSheet.show(fragment.childFragmentManager)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,29 @@
|
|||||||
package org.koitharu.kotatsu.list.ui
|
package org.koitharu.kotatsu.list.ui
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
|
|
||||||
abstract class MangaListViewModel(
|
abstract class MangaListViewModel(
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
abstract val content: LiveData<List<ListModel>>
|
abstract val content: LiveData<List<ListModel>>
|
||||||
val listMode = MutableLiveData<ListMode>()
|
protected val listModeFlow = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
|
||||||
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, settings.listMode)
|
||||||
|
val listMode = listModeFlow.asFlowLiveData(viewModelScope.coroutineContext)
|
||||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||||
val gridScale = settings.observeAsLiveData(
|
val gridScale = settings.observeAsLiveData(
|
||||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||||
@@ -30,13 +33,6 @@ abstract class MangaListViewModel(
|
|||||||
|
|
||||||
open fun onUpdateFilter(tags: Set<MangaTag>) = Unit
|
open fun onUpdateFilter(tags: Set<MangaTag>) = Unit
|
||||||
|
|
||||||
protected fun createListModeFlow() = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
|
|
||||||
.onEach {
|
|
||||||
if (listMode.value != it) {
|
|
||||||
listMode.postValue(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun onRefresh()
|
abstract fun onRefresh()
|
||||||
|
|
||||||
abstract fun onRetry()
|
abstract fun onRetry()
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDec
|
|||||||
|
|
||||||
protected val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
protected val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
protected val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
protected val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
||||||
protected val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
protected val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.card_indicator_offset)
|
||||||
|
protected val iconSize = context.resources.getDimensionPixelOffset(R.dimen.card_indicator_size)
|
||||||
protected val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
protected val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||||
protected val fillColor = ColorUtils.setAlphaComponent(
|
protected val fillColor = ColorUtils.setAlphaComponent(
|
||||||
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
||||||
0x74
|
0x74,
|
||||||
)
|
)
|
||||||
protected val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner)
|
protected val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner)
|
||||||
|
|
||||||
@@ -65,8 +66,8 @@ open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDec
|
|||||||
setBounds(
|
setBounds(
|
||||||
(bounds.left + iconOffset).toInt(),
|
(bounds.left + iconOffset).toInt(),
|
||||||
(bounds.top + iconOffset).toInt(),
|
(bounds.top + iconOffset).toInt(),
|
||||||
(bounds.left + iconOffset + intrinsicWidth).toInt(),
|
(bounds.left + iconOffset + iconSize).toInt(),
|
||||||
(bounds.top + iconOffset + intrinsicHeight).toInt(),
|
(bounds.top + iconOffset + iconSize).toInt(),
|
||||||
)
|
)
|
||||||
draw(canvas)
|
draw(canvas)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
@file:SuppressLint("UnsafeOptInUsageError")
|
|
||||||
|
|
||||||
package org.koitharu.kotatsu.list.ui.adapter
|
package org.koitharu.kotatsu.list.ui.adapter
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.CheckResult
|
import androidx.annotation.CheckResult
|
||||||
import androidx.core.view.doOnNextLayout
|
import androidx.core.view.doOnNextLayout
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.adapter
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||||
|
|
||||||
|
fun emptyHintAD(
|
||||||
|
listener: ListStateHolderListener,
|
||||||
|
) = adapterDelegateViewBinding<EmptyHint, ListModel, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
|
||||||
|
interface MangaDetailsClickListener : OnListItemClickListener<Manga> {
|
||||||
|
|
||||||
|
fun onReadClick(manga: Manga, view: View)
|
||||||
|
|
||||||
|
fun onTagClick(manga: Manga, tag: MangaTag, view: View)
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ fun mangaGridItemAD(
|
|||||||
bind { payloads ->
|
bind { payloads ->
|
||||||
binding.textViewTitle.text = item.title
|
binding.textViewTitle.text = item.title
|
||||||
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads)
|
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads)
|
||||||
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
|
binding.imageViewCover.newImageRequest(item.coverUrl, item.source)?.run {
|
||||||
referer(item.manga.publicUrl)
|
referer(item.manga.publicUrl)
|
||||||
size(CoverSizeResolver(binding.imageViewCover))
|
size(CoverSizeResolver(binding.imageViewCover))
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
|
|||||||
@@ -1,40 +1,58 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.adapter
|
package org.koitharu.kotatsu.list.ui.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.google.android.material.badge.BadgeDrawable
|
import com.google.android.material.badge.BadgeDrawable
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
|
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
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.referer
|
||||||
|
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||||
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
||||||
|
|
||||||
fun mangaListDetailedItemAD(
|
fun mangaListDetailedItemAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
clickListener: OnListItemClickListener<Manga>,
|
clickListener: MangaDetailsClickListener,
|
||||||
) = adapterDelegateViewBinding<MangaListDetailedModel, ListModel, ItemMangaListDetailsBinding>(
|
) = adapterDelegateViewBinding<MangaListDetailedModel, ListModel, ItemMangaListDetailsBinding>(
|
||||||
{ inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) },
|
{ inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
var badge: BadgeDrawable? = null
|
var badge: BadgeDrawable? = null
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
val listenerAdapter = object : View.OnClickListener, View.OnLongClickListener, ChipsView.OnChipClickListener {
|
||||||
clickListener.onItemClick(item.manga, it)
|
override fun onClick(v: View) = when (v.id) {
|
||||||
|
R.id.button_read -> clickListener.onReadClick(item.manga, v)
|
||||||
|
else -> clickListener.onItemClick(item.manga, v)
|
||||||
}
|
}
|
||||||
itemView.setOnLongClickListener {
|
|
||||||
clickListener.onItemLongClick(item.manga, it)
|
override fun onLongClick(v: View): Boolean = clickListener.onItemLongClick(item.manga, v)
|
||||||
|
|
||||||
|
override fun onChipClick(chip: Chip, data: Any?) {
|
||||||
|
val tag = data as? MangaTag ?: return
|
||||||
|
clickListener.onTagClick(item.manga, tag, chip)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
itemView.setOnClickListener(listenerAdapter)
|
||||||
|
itemView.setOnLongClickListener(listenerAdapter)
|
||||||
|
binding.buttonRead.setOnClickListener(listenerAdapter)
|
||||||
|
binding.chipsTags.onChipClickListener = listenerAdapter
|
||||||
|
|
||||||
bind { payloads ->
|
bind { payloads ->
|
||||||
binding.textViewTitle.text = item.title
|
binding.textViewTitle.text = item.title
|
||||||
binding.textViewSubtitle.textAndVisible = item.subtitle
|
binding.textViewSubtitle.textAndVisible = item.subtitle
|
||||||
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads)
|
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads)
|
||||||
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
|
binding.imageViewCover.newImageRequest(item.coverUrl, item.source)?.run {
|
||||||
referer(item.manga.publicUrl)
|
referer(item.manga.publicUrl)
|
||||||
size(CoverSizeResolver(binding.imageViewCover))
|
size(CoverSizeResolver(binding.imageViewCover))
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
@@ -44,8 +62,9 @@ fun mangaListDetailedItemAD(
|
|||||||
lifecycle(lifecycleOwner)
|
lifecycle(lifecycleOwner)
|
||||||
enqueueWith(coil)
|
enqueueWith(coil)
|
||||||
}
|
}
|
||||||
binding.textViewRating.textAndVisible = item.rating
|
binding.chipsTags.setChips(item.tags)
|
||||||
binding.textViewTags.text = item.tags
|
binding.ratingBar.isVisible = item.manga.hasRating
|
||||||
|
binding.ratingBar.rating = binding.ratingBar.numStars * item.manga.rating
|
||||||
badge = itemView.bindBadge(badge, item.counter)
|
badge = itemView.bindBadge(badge, item.counter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import org.koitharu.kotatsu.databinding.ItemMangaListBinding
|
|||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
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.referer
|
||||||
|
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||||
|
|
||||||
fun mangaListItemAD(
|
fun mangaListItemAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
@@ -31,7 +35,7 @@ fun mangaListItemAD(
|
|||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = item.title
|
binding.textViewTitle.text = item.title
|
||||||
binding.textViewSubtitle.textAndVisible = item.subtitle
|
binding.textViewSubtitle.textAndVisible = item.subtitle
|
||||||
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
|
binding.imageViewCover.newImageRequest(item.coverUrl, item.source)?.run {
|
||||||
referer(item.manga.publicUrl)
|
referer(item.manga.publicUrl)
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
fallback(R.drawable.ic_placeholder)
|
fallback(R.drawable.ic_placeholder)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user