Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee26a3e434 | ||
|
|
5bb6eae673 | ||
|
|
3357c00578 | ||
|
|
1f2f40f077 | ||
|
|
c25ee93ccb | ||
|
|
4aa1b58109 | ||
|
|
c64115a268 | ||
|
|
33296217a4 | ||
|
|
0e384c134d | ||
|
|
7f37c1f99e | ||
|
|
d1aa0f0407 | ||
|
|
4d904fe12f | ||
|
|
3df8b8d170 | ||
|
|
42f0fa9bbf | ||
|
|
5cbc592d23 | ||
|
|
85c424580a | ||
|
|
0d0e3acd04 | ||
|
|
49f9fb0488 | ||
|
|
bbcd96b981 | ||
|
|
510c5b70c9 | ||
|
|
951a0db3f2 | ||
|
|
d85f23b320 | ||
|
|
0c0214a85e | ||
|
|
9054f5720f | ||
|
|
bb1dd74277 | ||
|
|
96d437b2a8 | ||
|
|
8f8d85d172 | ||
|
|
a242aa6633 | ||
|
|
1a0986212b | ||
|
|
22e7bab879 | ||
|
|
9bd7daef65 | ||
|
|
d1e17c8ec2 | ||
|
|
e674e0f36f | ||
|
|
7fd71c13f3 | ||
|
|
9a0b7c4700 | ||
|
|
c54d128c09 | ||
|
|
a1545fd889 | ||
|
|
6e1fdcb19a | ||
|
|
72bedfd92e | ||
|
|
c132f1d5c4 | ||
|
|
abc4ab92a9 | ||
|
|
0931e4e0e6 | ||
|
|
113cde2f07 | ||
|
|
bf2d82723b | ||
|
|
6463023736 | ||
|
|
b8d2fa69c4 | ||
|
|
904d12f611 | ||
|
|
71a5801a0c | ||
|
|
6b529f806f | ||
|
|
9b5510ac59 | ||
|
|
90be936c82 | ||
|
|
29e6eab0e7 | ||
|
|
75b3ea0bc9 | ||
|
|
a215d9ebfc | ||
|
|
cef5d91eec | ||
|
|
9c20559962 | ||
|
|
b1be45af8b | ||
|
|
5ed4d0b6b7 | ||
|
|
53e36d23b1 | ||
|
|
fa02cfd7e8 | ||
|
|
1b1540b35b | ||
|
|
b9f35f34ad | ||
|
|
12c8cdfd70 | ||
|
|
971f708e45 | ||
|
|
7e76e10591 | ||
|
|
7d24286c55 | ||
|
|
eaac271143 | ||
|
|
d135898b49 | ||
|
|
03dbd86363 | ||
|
|
908baebb62 | ||
|
|
5190ec3e98 | ||
|
|
17c20b2bf9 | ||
|
|
e2b65f6fb6 | ||
|
|
28a9659410 | ||
|
|
bdebd0578e | ||
|
|
53542f3f86 | ||
|
|
2772f0b3dd | ||
|
|
95a4bf41d2 | ||
|
|
e497781359 | ||
|
|
72fdc7796f | ||
|
|
a885709ba9 | ||
|
|
578c1c3825 | ||
|
|
a5fba83510 | ||
|
|
6f3ae19345 | ||
|
|
2135195f27 | ||
|
|
a8c22de601 | ||
|
|
56e145420c | ||
|
|
fb60b26f08 | ||
|
|
ff3ebbf1d9 | ||
|
|
4dc9df0515 | ||
|
|
e9bce8ef15 | ||
|
|
55fc1aeadd | ||
|
|
693f568b8e | ||
|
|
5293a8d209 | ||
|
|
1c46fc7f23 | ||
|
|
b7e4c6b8c0 | ||
|
|
df599e9d50 | ||
|
|
6009f089e7 | ||
|
|
0a4f2f848e | ||
|
|
85fc3a024c | ||
|
|
eeb536b1ac | ||
|
|
5b8e8d76c0 | ||
|
|
73cf2964b2 | ||
|
|
8372f9b5de | ||
|
|
d7181e35e7 | ||
|
|
55d824bb94 | ||
|
|
229d9fa2ae | ||
|
|
3eb68e1ff9 | ||
|
|
b103589bba | ||
|
|
0726c037a4 | ||
|
|
0ff64931e0 | ||
|
|
2374c96009 | ||
|
|
2dd51117e9 | ||
|
|
6c5f3c7d97 | ||
|
|
626bb20edb | ||
|
|
d363869dab | ||
|
|
774f33c63d | ||
|
|
079427346a | ||
|
|
a1a3125834 |
2
.idea/compiler.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="1.8" />
|
<bytecodeTargetLevel target="11" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
2
.idea/dictionaries/admin.xml
generated
@@ -1,12 +1,14 @@
|
|||||||
<component name="ProjectDictionaryState">
|
<component name="ProjectDictionaryState">
|
||||||
<dictionary name="admin">
|
<dictionary name="admin">
|
||||||
<words>
|
<words>
|
||||||
|
<w>amoled</w>
|
||||||
<w>chucker</w>
|
<w>chucker</w>
|
||||||
<w>desu</w>
|
<w>desu</w>
|
||||||
<w>failsafe</w>
|
<w>failsafe</w>
|
||||||
<w>koin</w>
|
<w>koin</w>
|
||||||
<w>kotatsu</w>
|
<w>kotatsu</w>
|
||||||
<w>manga</w>
|
<w>manga</w>
|
||||||
|
<w>snackbar</w>
|
||||||
<w>upsert</w>
|
<w>upsert</w>
|
||||||
<w>webtoon</w>
|
<w>webtoon</w>
|
||||||
</words>
|
</words>
|
||||||
|
|||||||
4
.idea/gradle.xml
generated
@@ -4,10 +4,9 @@
|
|||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="PLATFORM" />
|
<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="1.8" />
|
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
@@ -15,7 +14,6 @@
|
|||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveModulePerSourceSet" value="false" />
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
<option name="useQualifiedModuleNames" value="true" />
|
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
2
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -2,6 +2,8 @@
|
|||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="BooleanLiteralArgument" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
<inspection_tool class="BooleanLiteralArgument" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
2
.idea/misc.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
12
.idea/runConfigurations.xml
generated
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RunConfigurationProducerService">
|
|
||||||
<option name="ignoredProducers">
|
|
||||||
<set>
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
14
.travis.yml
@@ -1,15 +1,11 @@
|
|||||||
language: android
|
language: android
|
||||||
dist: trusty
|
dist: trusty
|
||||||
jdk:
|
|
||||||
- oraclejdk8
|
|
||||||
android:
|
android:
|
||||||
components:
|
components:
|
||||||
|
- android-30
|
||||||
|
- build-tools-30.0.3
|
||||||
|
- platform-tools-30.0.5
|
||||||
- tools
|
- tools
|
||||||
- platform-tools-29.0.6
|
before_install:
|
||||||
- build-tools-29.0.3
|
- yes | sdkmanager "platforms;android-30"
|
||||||
- android-29
|
|
||||||
licenses:
|
|
||||||
- android-sdk-preview-license-.+
|
|
||||||
- android-sdk-license-.+
|
|
||||||
- google-gdk-license-.+
|
|
||||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug
|
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlin-android-extensions'
|
|
||||||
id 'kotlin-kapt'
|
id 'kotlin-kapt'
|
||||||
|
id 'kotlin-parcelize'
|
||||||
}
|
}
|
||||||
|
|
||||||
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
|
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
buildToolsVersion '29.0.3'
|
buildToolsVersion '30.0.3'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
versionCode gitCommits
|
versionCode gitCommits
|
||||||
versionName '0.5-rc1'
|
versionName '1.0-rc1'
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
arguments {
|
arguments {
|
||||||
@@ -28,10 +28,6 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
|
|
||||||
}
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = '.debug'
|
applicationIdSuffix = '.debug'
|
||||||
@@ -43,59 +39,71 @@ android {
|
|||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'MissingTranslation'
|
disable 'MissingTranslation'
|
||||||
abortOnError false
|
abortOnError false
|
||||||
}
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.includeAndroidResources = true
|
unitTests.includeAndroidResources = true
|
||||||
unitTests.returnDefaultValues = true
|
unitTests.returnDefaultValues = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidExtensions {
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||||
experimental = true
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
freeCompilerArgs += [
|
||||||
|
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||||
|
'-Xopt-in=org.koin.core.component.KoinApiExtension'
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.5.0-alpha01'
|
implementation 'androidx.core:core-ktx:1.5.0-beta01'
|
||||||
implementation 'androidx.activity:activity-ktx:1.2.0-alpha06'
|
implementation 'androidx.activity:activity-ktx:1.2.0-rc01'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06'
|
implementation 'androidx.fragment:fragment-ktx:1.3.0-rc02'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-rc01'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-rc01'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-rc01'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-service:2.3.0-rc01'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-process:2.3.0-rc01'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
|
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.4.0-rc01'
|
implementation 'androidx.work:work-runtime-ktx:2.5.0'
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha01'
|
implementation 'com.google.android.material:material:1.3.0-rc01'
|
||||||
|
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||||
|
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0-rc01'
|
||||||
|
|
||||||
implementation 'androidx.room:room-runtime:2.2.5'
|
implementation 'androidx.room:room-runtime:2.2.6'
|
||||||
implementation 'androidx.room:room-ktx:2.2.5'
|
implementation 'androidx.room:room-ktx:2.2.6'
|
||||||
kapt 'androidx.room:room-compiler:2.2.5'
|
kapt 'androidx.room:room-compiler:2.2.6'
|
||||||
|
|
||||||
implementation 'com.github.moxy-community:moxy:2.1.2'
|
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
|
||||||
implementation 'com.github.moxy-community:moxy-androidx:2.1.2'
|
implementation 'com.squareup.okio:okio:2.10.0'
|
||||||
implementation 'com.github.moxy-community:moxy-material:2.1.2'
|
|
||||||
implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
|
|
||||||
kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
|
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.7.2'
|
|
||||||
implementation 'com.squareup.okio:okio:2.6.0'
|
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
|
|
||||||
implementation 'org.koin:koin-android:2.1.5'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
|
||||||
implementation 'io.coil-kt:coil:0.11.0'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0'
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
|
||||||
|
implementation 'org.koin:koin-android:2.2.2'
|
||||||
|
implementation 'org.koin:koin-androidx-viewmodel:2.2.2'
|
||||||
|
implementation 'io.coil-kt:coil-base:1.1.1'
|
||||||
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||||
implementation 'com.tomclaw.cache:cache:1.0'
|
implementation 'com.tomclaw.cache:cache:1.0'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
|
||||||
debugImplementation 'com.github.ChuckerTeam.Chucker:library:3.2.0'
|
|
||||||
releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.2.0'
|
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13.1'
|
||||||
testImplementation 'org.json:json:20200518'
|
testImplementation 'org.json:json:20201115'
|
||||||
|
testImplementation 'org.koin:koin-test:2.2.2'
|
||||||
}
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:tint="#E6000A">
|
|
||||||
<group android:scaleX="0.40188664"
|
|
||||||
android:scaleY="0.40188664"
|
|
||||||
android:translateX="32.90095"
|
|
||||||
android:translateY="18.7272">
|
|
||||||
<group android:translateY="139.39206">
|
|
||||||
<path android:pathData="M83.796875,-0L105.6875,-0L60.765625,-55.828125L103.09375,-101L82.078125,-101L32.25,-49.1875L32.25,-101L13.53125,-101L13.53125,-0L32.25,-0L32.25,-25.8125L48.234375,-42.265625L83.796875,-0Z"
|
|
||||||
android:fillColor="#E6000A"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1016 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#FFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
3
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name" translatable="false">Kotatsu Dev</string>
|
||||||
|
</resources>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
<activity android:name=".ui.list.MainActivity">
|
<activity android:name="org.koitharu.kotatsu.main.ui.MainActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@@ -32,59 +32,66 @@
|
|||||||
android:name="android.app.default_searchable"
|
android:name="android.app.default_searchable"
|
||||||
android:value=".ui.search.SearchActivity" />
|
android:value=".ui.search.SearchActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ui.details.MangaDetailsActivity">
|
<activity android:name="org.koitharu.kotatsu.details.ui.DetailsActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ui.reader.ReaderActivity" />
|
<activity android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="${applicationId}.action.READ_MANGA" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.search.SearchActivity"
|
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
||||||
android:label="@string/search" />
|
android:label="@string/search" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.settings.SettingsActivity"
|
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.SimpleSettingsActivity"
|
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
|
||||||
android:label="@string/settings">
|
android:label="@string/settings">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
|
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ui.browser.BrowserActivity" />
|
<activity android:name="org.koitharu.kotatsu.browser.BrowserActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.utils.CrashActivity"
|
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
|
||||||
android:label="@string/error_occurred"
|
android:label="@string/error_occurred"
|
||||||
android:theme="@android:style/Theme.DeviceDefault.Dialog"
|
android:theme="@android:style/Theme.DeviceDefault"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.ui.list.favourites.categories.CategoriesActivity"
|
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
|
||||||
android:label="@string/favourites_categories"
|
android:label="@string/favourites_categories"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.widget.shelf.ShelfConfigActivity"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
||||||
android:label="@string/manga_shelf">
|
android:label="@string/manga_shelf">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ui.search.global.GlobalSearchActivity"
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
|
||||||
android:label="@string/search" />
|
android:label="@string/search" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".ui.download.DownloadService"
|
android:name="org.koitharu.kotatsu.download.DownloadService"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
<service android:name=".ui.settings.AppUpdateService" />
|
|
||||||
<service
|
<service
|
||||||
android:name=".ui.widget.shelf.ShelfWidgetService"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
<service
|
<service
|
||||||
android:name=".ui.widget.recent.RecentWidgetService"
|
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".ui.search.MangaSuggestionsProvider"
|
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
||||||
android:authorities="${applicationId}.MangaSuggestionsProvider"
|
android:authorities="${applicationId}.MangaSuggestionsProvider"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<provider
|
<provider
|
||||||
@@ -98,7 +105,7 @@
|
|||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".ui.widget.shelf.ShelfWidgetProvider"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
|
||||||
android:label="@string/manga_shelf">
|
android:label="@string/manga_shelf">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
@@ -108,7 +115,7 @@
|
|||||||
android:resource="@xml/widget_shelf" />
|
android:resource="@xml/widget_shelf" />
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".ui.widget.recent.RecentWidgetProvider"
|
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
|
||||||
android:label="@string/recent_manga">
|
android:label="@string/recent_manga">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
|||||||
@@ -3,69 +3,58 @@ package org.koitharu.kotatsu
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.room.Room
|
import org.koin.android.ext.android.get
|
||||||
import coil.Coil
|
|
||||||
import coil.ComponentRegistry
|
|
||||||
import coil.ImageLoaderBuilder
|
|
||||||
import coil.util.CoilUtils
|
|
||||||
import com.chuckerteam.chucker.api.ChuckerCollector
|
|
||||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koin.dsl.module
|
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.core.db.DatabasePrePopulateCallback
|
import org.koitharu.kotatsu.core.db.databaseModule
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.github.githubModule
|
||||||
import org.koitharu.kotatsu.core.db.migrations.*
|
import org.koitharu.kotatsu.core.network.networkModule
|
||||||
import org.koitharu.kotatsu.core.local.CbzFetcher
|
import org.koitharu.kotatsu.core.parser.parserModule
|
||||||
import org.koitharu.kotatsu.core.local.PagesCache
|
|
||||||
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
|
|
||||||
import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache
|
|
||||||
import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePersistor
|
|
||||||
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.core.parser.UserAgentInterceptor
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.domain.MangaLoaderContext
|
import org.koitharu.kotatsu.core.ui.AppCrashHandler
|
||||||
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
|
import org.koitharu.kotatsu.core.ui.uiModule
|
||||||
import org.koitharu.kotatsu.domain.history.HistoryRepository
|
import org.koitharu.kotatsu.details.detailsModule
|
||||||
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.ui.widget.WidgetUpdater
|
import org.koitharu.kotatsu.favourites.favouritesModule
|
||||||
import org.koitharu.kotatsu.utils.CacheUtils
|
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||||
import java.util.concurrent.TimeUnit
|
import org.koitharu.kotatsu.history.historyModule
|
||||||
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.local.localModule
|
||||||
|
import org.koitharu.kotatsu.main.mainModule
|
||||||
|
import org.koitharu.kotatsu.reader.readerModule
|
||||||
|
import org.koitharu.kotatsu.remotelist.remoteListModule
|
||||||
|
import org.koitharu.kotatsu.search.searchModule
|
||||||
|
import org.koitharu.kotatsu.settings.settingsModule
|
||||||
|
import org.koitharu.kotatsu.tracker.trackerModule
|
||||||
|
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||||
|
import org.koitharu.kotatsu.widget.appWidgetModule
|
||||||
|
|
||||||
class KotatsuApp : Application() {
|
class KotatsuApp : Application() {
|
||||||
|
|
||||||
private val cookieJar by lazy {
|
|
||||||
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext))
|
|
||||||
}
|
|
||||||
|
|
||||||
private val chuckerCollector by lazy(LazyThreadSafetyMode.NONE) {
|
|
||||||
ChuckerCollector(applicationContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
|
StrictMode.setThreadPolicy(
|
||||||
.detectAll()
|
StrictMode.ThreadPolicy.Builder()
|
||||||
.penaltyLog()
|
.detectAll()
|
||||||
.build())
|
.penaltyLog()
|
||||||
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
|
.build()
|
||||||
.detectAll()
|
)
|
||||||
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
|
StrictMode.setVmPolicy(
|
||||||
.setClassInstanceLimit(PagesCache::class.java, 1)
|
StrictMode.VmPolicy.Builder()
|
||||||
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
|
.detectAll()
|
||||||
.penaltyLog()
|
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
|
||||||
.build())
|
.setClassInstanceLimit(PagesCache::class.java, 1)
|
||||||
|
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
|
||||||
|
.penaltyLog()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
initKoin()
|
initKoin()
|
||||||
initCoil()
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
||||||
if (BuildConfig.DEBUG) {
|
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
||||||
initErrorHandler()
|
|
||||||
}
|
|
||||||
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
|
|
||||||
val widgetUpdater = WidgetUpdater(applicationContext)
|
val widgetUpdater = WidgetUpdater(applicationContext)
|
||||||
FavouritesRepository.subscribe(widgetUpdater)
|
FavouritesRepository.subscribe(widgetUpdater)
|
||||||
HistoryRepository.subscribe(widgetUpdater)
|
HistoryRepository.subscribe(widgetUpdater)
|
||||||
@@ -73,71 +62,25 @@ class KotatsuApp : Application() {
|
|||||||
|
|
||||||
private fun initKoin() {
|
private fun initKoin() {
|
||||||
startKoin {
|
startKoin {
|
||||||
androidLogger()
|
androidContext(this@KotatsuApp)
|
||||||
androidContext(applicationContext)
|
|
||||||
modules(
|
modules(
|
||||||
module {
|
networkModule,
|
||||||
factory {
|
databaseModule,
|
||||||
okHttp()
|
githubModule,
|
||||||
.cache(CacheUtils.createHttpCache(applicationContext))
|
uiModule,
|
||||||
.build()
|
parserModule,
|
||||||
}
|
mainModule,
|
||||||
single {
|
searchModule,
|
||||||
mangaDb().build()
|
localModule,
|
||||||
}
|
favouritesModule,
|
||||||
single {
|
historyModule,
|
||||||
MangaLoaderContext()
|
remoteListModule,
|
||||||
}
|
detailsModule,
|
||||||
single {
|
trackerModule,
|
||||||
AppSettings(applicationContext)
|
settingsModule,
|
||||||
}
|
readerModule,
|
||||||
single {
|
appWidgetModule
|
||||||
PagesCache(applicationContext)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initCoil() {
|
|
||||||
Coil.setImageLoader(
|
|
||||||
ImageLoaderBuilder(applicationContext)
|
|
||||||
.okHttpClient(
|
|
||||||
okHttp()
|
|
||||||
.cache(CoilUtils.createDefaultCache(applicationContext))
|
|
||||||
.build()
|
|
||||||
).componentRegistry(
|
|
||||||
ComponentRegistry.Builder()
|
|
||||||
.add(CbzFetcher())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initErrorHandler() {
|
|
||||||
val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { t, e ->
|
|
||||||
chuckerCollector.onError("CRASH", e)
|
|
||||||
exceptionHandler?.uncaughtException(t, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun okHttp() = OkHttpClient.Builder().apply {
|
|
||||||
connectTimeout(20, TimeUnit.SECONDS)
|
|
||||||
readTimeout(60, TimeUnit.SECONDS)
|
|
||||||
writeTimeout(20, TimeUnit.SECONDS)
|
|
||||||
cookieJar(cookieJar)
|
|
||||||
addInterceptor(UserAgentInterceptor)
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
addInterceptor(ChuckerInterceptor(applicationContext, collector = chuckerCollector))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mangaDb() = Room.databaseBuilder(
|
|
||||||
applicationContext,
|
|
||||||
MangaDatabase::class.java,
|
|
||||||
"kotatsu-db"
|
|
||||||
).addMigrations(Migration1To2, Migration2To3, Migration3To4, Migration4To5, Migration5To6)
|
|
||||||
.addCallback(DatabasePrePopulateCallback(resources))
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.domain
|
package org.koitharu.kotatsu.base.domain
|
||||||
|
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.inject
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||||
@@ -10,9 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity
|
|||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
|
|
||||||
class MangaDataRepository : KoinComponent {
|
class MangaDataRepository(private val db: MangaDatabase) {
|
||||||
|
|
||||||
private val db: MangaDatabase by inject()
|
|
||||||
|
|
||||||
suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
|
suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
|
||||||
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||||
@@ -36,6 +32,12 @@ class MangaDataRepository : KoinComponent {
|
|||||||
return db.mangaDao.find(mangaId)?.toManga()
|
return db.mangaDao.find(mangaId)?.toManga()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
|
||||||
|
intent.manga != null -> intent.manga
|
||||||
|
intent.mangaId != MangaIntent.ID_NONE -> db.mangaDao.find(intent.mangaId)?.toManga()
|
||||||
|
else -> null // TODO resolve uri
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun storeManga(manga: Manga) {
|
suspend fun storeManga(manga: Manga) {
|
||||||
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.koitharu.kotatsu.base.domain
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
|
||||||
|
data class MangaIntent(
|
||||||
|
val manga: Manga?,
|
||||||
|
val mangaId: Long,
|
||||||
|
val uri: Uri?
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun from(intent: Intent?) = MangaIntent(
|
||||||
|
manga = intent?.getParcelableExtra(KEY_MANGA),
|
||||||
|
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||||
|
uri = intent?.data
|
||||||
|
)
|
||||||
|
|
||||||
|
fun from(args: Bundle?) = MangaIntent(
|
||||||
|
manga = args?.getParcelable(KEY_MANGA),
|
||||||
|
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
|
||||||
|
uri = null
|
||||||
|
)
|
||||||
|
|
||||||
|
const val ID_NONE = 0L
|
||||||
|
|
||||||
|
const val KEY_MANGA = "manga"
|
||||||
|
const val KEY_ID = "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.koitharu.kotatsu.base.domain
|
||||||
|
|
||||||
|
import okhttp3.*
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||||
|
import org.koitharu.kotatsu.utils.ext.await
|
||||||
|
|
||||||
|
open class MangaLoaderContext(
|
||||||
|
private val okHttp: OkHttpClient,
|
||||||
|
private val cookieJar: CookieJar
|
||||||
|
) : KoinComponent {
|
||||||
|
|
||||||
|
suspend fun httpGet(url: String): Response {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.get()
|
||||||
|
.url(url)
|
||||||
|
return okHttp.newCall(request.build()).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun httpPost(
|
||||||
|
url: String,
|
||||||
|
form: Map<String, String>
|
||||||
|
): Response {
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
form.forEach { (k, v) ->
|
||||||
|
body.addEncoded(k, v)
|
||||||
|
}
|
||||||
|
val request = Request.Builder()
|
||||||
|
.post(body.build())
|
||||||
|
.url(url)
|
||||||
|
return okHttp.newCall(request.build()).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun httpPost(
|
||||||
|
url: String,
|
||||||
|
payload: String
|
||||||
|
): Response {
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
payload.split('&').forEach {
|
||||||
|
val pos = it.indexOf('=')
|
||||||
|
if (pos != -1) {
|
||||||
|
val k = it.substring(0, pos)
|
||||||
|
val v = it.substring(pos + 1)
|
||||||
|
body.addEncoded(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val request = Request.Builder()
|
||||||
|
.post(body.build())
|
||||||
|
.url(url)
|
||||||
|
return okHttp.newCall(request.build()).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getSettings(source: MangaSource) = SourceSettings(get(), source)
|
||||||
|
|
||||||
|
fun insertCookies(domain: String, vararg cookies: String) {
|
||||||
|
val url = HttpUrl.Builder()
|
||||||
|
.scheme(SCHEME_HTTP)
|
||||||
|
.host(domain)
|
||||||
|
.build()
|
||||||
|
cookieJar.saveFromResponse(url, cookies.mapNotNull {
|
||||||
|
Cookie.parse(url, it)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
private const val SCHEME_HTTP = "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package org.koitharu.kotatsu.base.domain
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
|
||||||
|
object MangaProviderFactory {
|
||||||
|
|
||||||
|
fun getSources(settings: AppSettings, includeHidden: Boolean): List<MangaSource> {
|
||||||
|
val list = MangaSource.values().toList() - MangaSource.LOCAL
|
||||||
|
val order = settings.sourcesOrder
|
||||||
|
val hidden = settings.hiddenSources
|
||||||
|
val sorted = list.sortedBy { x ->
|
||||||
|
val e = order.indexOf(x.ordinal)
|
||||||
|
if (e == -1) order.size + x.ordinal else e
|
||||||
|
}
|
||||||
|
return if (includeHidden) {
|
||||||
|
sorted
|
||||||
|
} else {
|
||||||
|
sorted.filterNot { x ->
|
||||||
|
x.name in hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.domain
|
package org.koitharu.kotatsu.base.domain
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -6,11 +6,12 @@ import android.util.Size
|
|||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.get
|
import org.koin.core.component.get
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.model.MangaPage
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
|
import org.koitharu.kotatsu.utils.CacheUtils
|
||||||
import org.koitharu.kotatsu.utils.ext.await
|
import org.koitharu.kotatsu.utils.ext.await
|
||||||
import org.koitharu.kotatsu.utils.ext.medianOrNull
|
import org.koitharu.kotatsu.utils.ext.medianOrNull
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -24,10 +25,10 @@ object MangaUtils : KoinComponent {
|
|||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun determineReaderMode(pages: List<MangaPage>): ReaderMode? {
|
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
|
||||||
try {
|
try {
|
||||||
val page = pages.medianOrNull() ?: return null
|
val page = pages.medianOrNull() ?: return null
|
||||||
val url = MangaProviderFactory.create(page.source).getPageFullUrl(page)
|
val url = page.source.repository.getPageUrl(page)
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
val size = if (uri.scheme == "cbz") {
|
val size = if (uri.scheme == "cbz") {
|
||||||
val zip = ZipFile(uri.schemeSpecificPart)
|
val zip = ZipFile(uri.schemeSpecificPart)
|
||||||
@@ -40,15 +41,14 @@ object MangaUtils : KoinComponent {
|
|||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.get()
|
.get()
|
||||||
|
.header(CommonHeaders.REFERER, page.referer)
|
||||||
|
.cacheControl(CacheUtils.CONTROL_DISABLED)
|
||||||
.build()
|
.build()
|
||||||
client.newCall(request).await().use {
|
client.newCall(request).await().use {
|
||||||
getBitmapSize(it.body?.byteStream())
|
getBitmapSize(it.body?.byteStream())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return when {
|
return size.width * 2 < size.height
|
||||||
size.width * 2 < size.height -> ReaderMode.WEBTOON
|
|
||||||
else -> ReaderMode.STANDARD
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -57,7 +57,6 @@ object MangaUtils : KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
private fun getBitmapSize(input: InputStream?): Size {
|
private fun getBitmapSize(input: InputStream?): Size {
|
||||||
val options = BitmapFactory.Options().apply {
|
val options = BitmapFactory.Options().apply {
|
||||||
inJustDecodeBounds = true
|
inJustDecodeBounds = true
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
|
||||||
|
abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
|
||||||
|
|
||||||
|
private var viewBinding: B? = null
|
||||||
|
|
||||||
|
protected val binding: B
|
||||||
|
get() = checkNotNull(viewBinding)
|
||||||
|
|
||||||
|
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext())
|
||||||
|
val binding = onInflateView(inflater, null)
|
||||||
|
viewBinding = binding
|
||||||
|
return AlertDialog.Builder(requireContext(), theme)
|
||||||
|
.setView(binding.root)
|
||||||
|
.also(::onBuildDialog)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) = viewBinding?.root
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onDestroyView() {
|
||||||
|
viewBinding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onBuildDialog(builder: AlertDialog.Builder) = Unit
|
||||||
|
|
||||||
|
protected fun bindingOrNull(): B? = viewBinding
|
||||||
|
|
||||||
|
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||||
|
}
|
||||||
100
app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.appcompat.widget.ActionBarContextView
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.*
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
|
|
||||||
|
abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindowInsetsListener {
|
||||||
|
|
||||||
|
protected lateinit var binding: B
|
||||||
|
private set
|
||||||
|
|
||||||
|
|
||||||
|
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
ExceptionResolver(this, supportFragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
if (get<AppSettings>().isAmoledTheme) {
|
||||||
|
setTheme(R.style.AppTheme_Amoled)
|
||||||
|
}
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
|
||||||
|
override fun setContentView(layoutResID: Int) {
|
||||||
|
super.setContentView(layoutResID)
|
||||||
|
setupToolbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
|
||||||
|
override fun setContentView(view: View?) {
|
||||||
|
super.setContentView(view)
|
||||||
|
setupToolbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setContentView(binding: B) {
|
||||||
|
this.binding = binding
|
||||||
|
super.setContentView(binding.root)
|
||||||
|
(binding.root.findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||||
|
onWindowInsetsChanged(insets.getInsets(WindowInsetsCompat.Type.systemBars()))
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
|
||||||
|
onBackPressed()
|
||||||
|
true
|
||||||
|
} else super.onOptionsItemSelected(item)
|
||||||
|
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) { // TODO remove
|
||||||
|
ActivityCompat.recreate(this)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun onWindowInsetsChanged(insets: Insets)
|
||||||
|
|
||||||
|
private fun setupToolbar() {
|
||||||
|
(findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||||
|
super.onSupportActionModeStarted(mode)
|
||||||
|
val insets = ViewCompat.getRootWindowInsets(binding.root)
|
||||||
|
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
|
||||||
|
val view = findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)
|
||||||
|
view?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = insets.top
|
||||||
|
}
|
||||||
|
window?.statusBarColor = ContextCompat.getColor(this, R.color.grey_dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportActionModeFinished(mode: ActionMode) {
|
||||||
|
super.onSupportActionModeFinished(mode)
|
||||||
|
window?.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatDialog
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
|
abstract class BaseBottomSheet<B : ViewBinding> :
|
||||||
|
BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private var viewBinding: B? = null
|
||||||
|
|
||||||
|
protected val binding: B
|
||||||
|
get() = checkNotNull(viewBinding)
|
||||||
|
|
||||||
|
final override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val binding = onInflateView(inflater, container)
|
||||||
|
viewBinding = binding
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
viewBinding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
return if (resources.getBoolean(R.bool.is_tablet)) {
|
||||||
|
AppCompatDialog(context, theme)
|
||||||
|
} else super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.OnApplyWindowInsetsListener
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
|
|
||||||
|
abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsListener {
|
||||||
|
|
||||||
|
private var viewBinding: B? = null
|
||||||
|
|
||||||
|
protected val binding: B
|
||||||
|
get() = checkNotNull(viewBinding)
|
||||||
|
|
||||||
|
protected val exceptionResolver by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
ExceptionResolver(viewLifecycleOwner, childFragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val binding = onInflateView(inflater, container)
|
||||||
|
viewBinding = binding
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(view, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
viewBinding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getTitle(): CharSequence? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
getTitle()?.let {
|
||||||
|
activity?.title = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||||
|
onWindowInsetsChanged(insets.getInsets(WindowInsetsCompat.Type.systemBars()))
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun bindingOrNull() = viewBinding
|
||||||
|
|
||||||
|
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
||||||
|
|
||||||
|
protected abstract fun onWindowInsetsChanged(insets: Insets)
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
|
||||||
|
|
||||||
|
abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
|
||||||
|
View.OnSystemUiVisibilityChangeListener {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
with(window) {
|
||||||
|
statusBarColor = Color.TRANSPARENT
|
||||||
|
navigationBarColor = Color.TRANSPARENT
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
attributes.layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
}
|
||||||
|
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
|
||||||
|
}
|
||||||
|
showSystemUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onSystemUiVisibilityChange(visibility: Int) {
|
||||||
|
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO WindowInsetsControllerCompat works incorrect
|
||||||
|
protected fun hideSystemUI() {
|
||||||
|
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun showSystemUI() {
|
||||||
|
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
|
||||||
|
const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||||
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.view.OnApplyWindowInsetsListener
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
|
||||||
|
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||||
|
PreferenceFragmentCompat(), OnApplyWindowInsetsListener {
|
||||||
|
|
||||||
|
protected val settings by inject<AppSettings>()
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
listView.clipToPadding = false
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(view, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
activity?.setTitle(titleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onApplyWindowInsets(v: View?, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||||
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
listView.updatePadding(
|
||||||
|
left = systemBars.left,
|
||||||
|
right = systemBars.right,
|
||||||
|
bottom = systemBars.bottom
|
||||||
|
)
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleService
|
||||||
|
|
||||||
|
abstract class BaseService : LifecycleService()
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
abstract class BaseViewModel : ViewModel() {
|
||||||
|
|
||||||
|
val onError = SingleLiveEvent<Throwable>()
|
||||||
|
val isLoading = MutableLiveData(false)
|
||||||
|
|
||||||
|
protected fun launchJob(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
|
||||||
|
|
||||||
|
protected fun launchLoadingJob(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
|
||||||
|
isLoading.postValue(true)
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
isLoading.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
throwable.printStackTrace()
|
||||||
|
}
|
||||||
|
if (throwable !is CancellationException) {
|
||||||
|
onError.postCall(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.dialog
|
package org.koitharu.kotatsu.base.ui.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.google.android.material.checkbox.MaterialCheckBox
|
import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
|
|
||||||
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
|
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
|
||||||
DialogInterface by delegate {
|
DialogInterface by delegate {
|
||||||
@@ -18,13 +15,10 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
|
|||||||
|
|
||||||
class Builder(context: Context) {
|
class Builder(context: Context) {
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
private val binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
|
||||||
private val view = LayoutInflater.from(context)
|
|
||||||
.inflate(R.layout.dialog_checkbox, null, false)
|
|
||||||
private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox)
|
|
||||||
|
|
||||||
private val delegate = MaterialAlertDialogBuilder(context)
|
private val delegate = AlertDialog.Builder(context)
|
||||||
.setView(view)
|
.setView(binding.root)
|
||||||
|
|
||||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
fun setTitle(@StringRes titleResId: Int): Builder {
|
||||||
delegate.setTitle(titleResId)
|
delegate.setTitle(titleResId)
|
||||||
@@ -47,12 +41,12 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setCheckBoxText(@StringRes textId: Int): Builder {
|
fun setCheckBoxText(@StringRes textId: Int): Builder {
|
||||||
checkBox.setText(textId)
|
binding.checkbox.setText(textId)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCheckBoxChecked(isChecked: Boolean): Builder {
|
fun setCheckBoxChecked(isChecked: Boolean): Builder {
|
||||||
checkBox.isChecked = isChecked
|
binding.checkbox.isChecked = isChecked
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +60,7 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
|
|||||||
listener: (DialogInterface, Boolean) -> Unit
|
listener: (DialogInterface, Boolean) -> Unit
|
||||||
): Builder {
|
): Builder {
|
||||||
delegate.setPositiveButton(textId) { dialog, _ ->
|
delegate.setPositiveButton(textId) { dialog, _ ->
|
||||||
listener(dialog, checkBox.isChecked)
|
listener(dialog, binding.checkbox.isChecked)
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.dialog
|
package org.koitharu.kotatsu.base.ui.dialog
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
@@ -7,10 +7,9 @@ import android.view.ViewGroup
|
|||||||
import android.widget.BaseAdapter
|
import android.widget.BaseAdapter
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import kotlinx.android.synthetic.main.item_storage.view.*
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
|
import org.koitharu.kotatsu.databinding.ItemStorageBinding
|
||||||
|
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||||
import org.koitharu.kotatsu.utils.ext.inflate
|
import org.koitharu.kotatsu.utils.ext.inflate
|
||||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||||
@@ -24,7 +23,7 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
|
|||||||
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
|
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
|
||||||
|
|
||||||
private val adapter = VolumesAdapter(context)
|
private val adapter = VolumesAdapter(context)
|
||||||
private val delegate = MaterialAlertDialogBuilder(context)
|
private val delegate = AlertDialog.Builder(context)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (adapter.isEmpty) {
|
if (adapter.isEmpty) {
|
||||||
@@ -65,8 +64,9 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
|
|||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
val view = convertView ?: parent.inflate(R.layout.item_storage)
|
val view = convertView ?: parent.inflate(R.layout.item_storage)
|
||||||
val item = volumes[position]
|
val item = volumes[position]
|
||||||
view.textView_title.text = item.second
|
val binding = ItemStorageBinding.bind(view)
|
||||||
view.textView_subtitle.text = item.first.path
|
binding.textViewTitle.text = item.second
|
||||||
|
binding.textViewSubtitle.text = item.first.path
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,14 +78,13 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnStorageSelectListener {
|
fun interface OnStorageSelectListener {
|
||||||
|
|
||||||
fun onStorageSelected(file: File)
|
fun onStorageSelected(file: File)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getAvailableVolumes(context: Context): List<Pair<File, String>> {
|
fun getAvailableVolumes(context: Context): List<Pair<File, String>> {
|
||||||
return LocalMangaRepository.getAvailableStorageDirs(context).map {
|
return LocalMangaRepository.getAvailableStorageDirs(context).map {
|
||||||
it to it.getStorageName(context)
|
it to it.getStorageName(context)
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.dialog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import org.koitharu.kotatsu.databinding.DialogInputBinding
|
||||||
|
|
||||||
|
class TextInputDialog private constructor(
|
||||||
|
private val delegate: AlertDialog
|
||||||
|
) : DialogInterface by delegate {
|
||||||
|
|
||||||
|
fun show() = delegate.show()
|
||||||
|
|
||||||
|
class Builder(context: Context) {
|
||||||
|
|
||||||
|
private val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
|
||||||
|
|
||||||
|
private val delegate = AlertDialog.Builder(context)
|
||||||
|
.setView(binding.root)
|
||||||
|
|
||||||
|
fun setTitle(@StringRes titleResId: Int): Builder {
|
||||||
|
delegate.setTitle(titleResId)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTitle(title: CharSequence): Builder {
|
||||||
|
delegate.setTitle(title)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHint(@StringRes hintResId: Int): Builder {
|
||||||
|
binding.inputLayout.hint = binding.root.context.getString(hintResId)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMaxLength(maxLength: Int, strict: Boolean): Builder {
|
||||||
|
with(binding.inputLayout) {
|
||||||
|
counterMaxLength = maxLength
|
||||||
|
isCounterEnabled = maxLength > 0
|
||||||
|
}
|
||||||
|
if (strict && maxLength > 0) {
|
||||||
|
binding.inputEdit.filters += InputFilter.LengthFilter(maxLength)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInputType(inputType: Int): Builder {
|
||||||
|
binding.inputEdit.inputType = inputType
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setText(text: String): Builder {
|
||||||
|
binding.inputEdit.setText(text)
|
||||||
|
binding.inputEdit.setSelection(text.length)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPositiveButton(
|
||||||
|
@StringRes textId: Int,
|
||||||
|
listener: (DialogInterface, String) -> Unit
|
||||||
|
): Builder {
|
||||||
|
delegate.setPositiveButton(textId) { dialog, _ ->
|
||||||
|
listener(dialog, binding.inputEdit.text.toString().orEmpty())
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNegativeButton(
|
||||||
|
@StringRes textId: Int,
|
||||||
|
listener: DialogInterface.OnClickListener? = null
|
||||||
|
): Builder {
|
||||||
|
delegate.setNegativeButton(textId, listener)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnCancelListener(listener: DialogInterface.OnCancelListener): Builder {
|
||||||
|
delegate.setOnCancelListener(listener)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create() =
|
||||||
|
TextInputDialog(delegate.create())
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.list
|
package org.koitharu.kotatsu.base.ui.list
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
|
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
|
||||||
|
|
||||||
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.list
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
|
abstract class BaseViewHolder<T, E, B : ViewBinding> protected constructor(val binding: B) :
|
||||||
|
RecyclerView.ViewHolder(binding.root), KoinComponent {
|
||||||
|
|
||||||
|
var boundData: T? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
val context get() = itemView.context!!
|
||||||
|
|
||||||
|
fun bind(data: T, extra: E) {
|
||||||
|
boundData = data
|
||||||
|
onBind(data, extra)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requireData(): T {
|
||||||
|
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onRecycled() = Unit
|
||||||
|
|
||||||
|
abstract fun onBind(data: T, extra: E)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.list
|
package org.koitharu.kotatsu.base.ui.list
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.list
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
interface OnListItemClickListener<I> {
|
||||||
|
|
||||||
|
fun onItemClick(item: I, view: View)
|
||||||
|
|
||||||
|
fun onItemLongClick(item: I, view: View) = false
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.list
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class PaginationScrollListener(offset: Int, private val callback: Callback) :
|
||||||
|
BoundsScrollListener(0, offset) {
|
||||||
|
|
||||||
|
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
|
||||||
|
|
||||||
|
override fun onScrolledToEnd(recyclerView: RecyclerView) {
|
||||||
|
callback.onScrolledToEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
|
||||||
|
fun onScrolledToEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.list.decor
|
package org.koitharu.kotatsu.base.ui.list.decor
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.list.decor
|
package org.koitharu.kotatsu.base.ui.list.decor
|
||||||
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.list.decor
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.Px
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
outRect.set(spacing, spacing, spacing, spacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.widgets
|
package org.koitharu.kotatsu.base.ui.widgets
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@@ -54,14 +54,13 @@ class CheckableImageView @JvmOverloads constructor(
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnCheckedChangeListener {
|
fun interface OnCheckedChangeListener {
|
||||||
|
|
||||||
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)
|
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
|
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.widgets
|
package org.koitharu.kotatsu.base.ui.widgets
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.withStyledAttributes
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
|
|
||||||
@@ -15,10 +15,9 @@ class CoverImageView @JvmOverloads constructor(
|
|||||||
private var orientation: Int = HORIZONTAL
|
private var orientation: Int = HORIZONTAL
|
||||||
|
|
||||||
init {
|
init {
|
||||||
context.theme.obtainStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr, 0)
|
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
|
||||||
.use {
|
orientation = getInt(R.styleable.CoverImageView_android_orientation, HORIZONTAL)
|
||||||
orientation = it.getInt(R.styleable.CoverImageView_android_orientation, HORIZONTAL)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.widgets
|
package org.koitharu.kotatsu.base.ui.widgets
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.ui.browser
|
package org.koitharu.kotatsu.browser
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
@@ -8,30 +8,36 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.activity_browser.*
|
import androidx.core.view.updatePadding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.ui.common.BaseActivity
|
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
class BrowserActivity : BaseActivity(), BrowserCallback {
|
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_browser)
|
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setHomeAsUpIndicator(R.drawable.ic_cross)
|
setHomeAsUpIndicator(R.drawable.ic_cross)
|
||||||
}
|
}
|
||||||
with(webView.settings) {
|
with(binding.webView.settings) {
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
}
|
}
|
||||||
webView.webViewClient = BrowserClient(this)
|
binding.webView.webViewClient = BrowserClient(this)
|
||||||
val url = intent?.dataString
|
val url = intent?.dataString
|
||||||
if (url.isNullOrEmpty()) {
|
if (url.isNullOrEmpty()) {
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
} else {
|
} else {
|
||||||
webView.loadUrl(url)
|
onTitleChanged(
|
||||||
|
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
|
||||||
|
url
|
||||||
|
)
|
||||||
|
binding.webView.loadUrl(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,13 +48,13 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
webView.stopLoading()
|
binding.webView.stopLoading()
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.action_browser -> {
|
R.id.action_browser -> {
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
intent.data = Uri.parse(webView.url)
|
intent.data = Uri.parse(binding.webView.url)
|
||||||
try {
|
try {
|
||||||
startActivity(Intent.createChooser(intent, item.title))
|
startActivity(Intent.createChooser(intent, item.title))
|
||||||
} catch (_: ActivityNotFoundException) {
|
} catch (_: ActivityNotFoundException) {
|
||||||
@@ -59,25 +65,25 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (webView.canGoBack()) {
|
if (binding.webView.canGoBack()) {
|
||||||
webView.goBack()
|
binding.webView.goBack()
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
webView.onPause()
|
binding.webView.onPause()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
webView.onResume()
|
binding.webView.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
progressBar.isVisible = isLoading
|
binding.progressBar.isVisible = isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
||||||
@@ -85,10 +91,19 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
|
|||||||
supportActionBar?.subtitle = subtitle
|
supportActionBar?.subtitle = subtitle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
binding.appbar.updatePadding(top = insets.top)
|
||||||
|
binding.webView.updatePadding(bottom = insets.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@JvmStatic
|
private const val EXTRA_TITLE = "title"
|
||||||
fun newIntent(context: Context, url: String) = Intent(context, BrowserActivity::class.java)
|
|
||||||
.setData(Uri.parse(url))
|
fun newIntent(context: Context, url: String, title: String?): Intent {
|
||||||
|
return Intent(context, BrowserActivity::class.java)
|
||||||
|
.setData(Uri.parse(url))
|
||||||
|
.putExtra(EXTRA_TITLE, title)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.ui.browser
|
package org.koitharu.kotatsu.browser
|
||||||
|
|
||||||
interface BrowserCallback {
|
interface BrowserCallback {
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.koitharu.kotatsu.browser
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.webkit.WebResourceResponse
|
||||||
|
import android.webkit.WebView
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.koitharu.kotatsu.core.network.WebViewClientCompat
|
||||||
|
|
||||||
|
class BrowserClient(private val callback: BrowserCallback) : WebViewClientCompat(), KoinComponent {
|
||||||
|
|
||||||
|
private val okHttp by inject<OkHttpClient>()
|
||||||
|
|
||||||
|
override fun onPageFinished(webView: WebView, url: String) {
|
||||||
|
super.onPageFinished(webView, url)
|
||||||
|
callback.onLoadingStateChanged(isLoading = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
callback.onLoadingStateChanged(isLoading = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageCommitVisible(view: WebView, url: String?) {
|
||||||
|
super.onPageCommitVisible(view, url)
|
||||||
|
callback.onTitleChanged(view.title.orEmpty(), url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
|
||||||
|
return runCatching {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.build()
|
||||||
|
val response = okHttp.newCall(request).execute()
|
||||||
|
val ct = response.body?.contentType()
|
||||||
|
WebResourceResponse(
|
||||||
|
"${ct?.type}/${ct?.subtype}",
|
||||||
|
ct?.charset()?.name() ?: "utf-8",
|
||||||
|
response.body?.byteStream()
|
||||||
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.koitharu.kotatsu.browser.cloudflare
|
||||||
|
|
||||||
|
interface CloudFlareCallback {
|
||||||
|
|
||||||
|
fun onPageLoaded()
|
||||||
|
|
||||||
|
fun onCheckPassed()
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.koitharu.kotatsu.browser.cloudflare
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.webkit.WebView
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
||||||
|
import org.koitharu.kotatsu.core.network.WebViewClientCompat
|
||||||
|
|
||||||
|
class CloudFlareClient(
|
||||||
|
private val cookieJar: AndroidCookieJar,
|
||||||
|
private val callback: CloudFlareCallback,
|
||||||
|
private val targetUrl: String
|
||||||
|
) : WebViewClientCompat() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
cookieJar.remove(targetUrl, CF_UID, CF_CLEARANCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
checkClearance()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageCommitVisible(view: WebView?, url: String?) {
|
||||||
|
super.onPageCommitVisible(view, url)
|
||||||
|
callback.onPageLoaded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
callback.onPageLoaded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkClearance() {
|
||||||
|
val cookies = cookieJar.loadForRequest(targetUrl.toHttpUrl())
|
||||||
|
if (cookies.any { it.name == CF_CLEARANCE }) {
|
||||||
|
callback.onCheckPassed()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val CF_UID = "__cfduid"
|
||||||
|
const val CF_CLEARANCE = "cf_clearance"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package org.koitharu.kotatsu.browser.cloudflare
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebSettings
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.fragment.app.setFragmentResult
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||||
|
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
||||||
|
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
||||||
|
import org.koitharu.kotatsu.utils.ext.stringArgument
|
||||||
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
|
||||||
|
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
||||||
|
|
||||||
|
private val url by stringArgument(ARG_URL)
|
||||||
|
private val pendingResult = Bundle(1)
|
||||||
|
|
||||||
|
override fun onInflateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?
|
||||||
|
) = FragmentCloudflareBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
with(binding.webView.settings) {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
|
domStorageEnabled = true
|
||||||
|
databaseEnabled = true
|
||||||
|
userAgentString = UserAgentInterceptor.userAgent
|
||||||
|
}
|
||||||
|
binding.webView.webViewClient = CloudFlareClient(get(), this, url.orEmpty())
|
||||||
|
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
||||||
|
if (url.isNullOrEmpty()) {
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
} else {
|
||||||
|
binding.webView.loadUrl(url.orEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
binding.webView.stopLoading()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
binding.webView.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
binding.webView.onPause()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
setFragmentResult(TAG, pendingResult)
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageLoaded() {
|
||||||
|
bindingOrNull()?.progressBar?.isInvisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCheckPassed() {
|
||||||
|
pendingResult.putBoolean(EXTRA_RESULT, true)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val TAG = "CloudFlareDialog"
|
||||||
|
const val EXTRA_RESULT = "result"
|
||||||
|
private const val ARG_URL = "url"
|
||||||
|
|
||||||
|
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {
|
||||||
|
putString(ARG_URL, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.utils.MutableZipFile
|
||||||
|
import org.koitharu.kotatsu.utils.ext.format
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class BackupArchive(file: File) : MutableZipFile(file) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun put(entry: BackupEntry) {
|
||||||
|
put(entry.name, entry.data.toString(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getEntry(name: String): BackupEntry {
|
||||||
|
val json = withContext(Dispatchers.Default) {
|
||||||
|
JSONArray(getContent(name))
|
||||||
|
}
|
||||||
|
return BackupEntry(name, json)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DIR_BACKUPS = "backups"
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
suspend fun createNew(context: Context): BackupArchive = withContext(Dispatchers.IO) {
|
||||||
|
val dir = context.run {
|
||||||
|
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
|
||||||
|
}
|
||||||
|
dir.mkdirs()
|
||||||
|
val filename = buildString {
|
||||||
|
append(context.getString(R.string.app_name).toLowerCase(Locale.ROOT))
|
||||||
|
append('_')
|
||||||
|
append(Date().format("ddMMyyyy"))
|
||||||
|
append(".bak")
|
||||||
|
}
|
||||||
|
BackupArchive(File(dir, filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
|
||||||
|
data class BackupEntry(
|
||||||
|
val name: String,
|
||||||
|
val data: JSONArray
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object Names {
|
||||||
|
|
||||||
|
const val INDEX = "index"
|
||||||
|
const val HISTORY = "history"
|
||||||
|
const val CATEGORIES = "categories"
|
||||||
|
const val FAVOURITES = "favourites"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||||
|
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||||
|
|
||||||
|
class BackupRepository(private val db: MangaDatabase) {
|
||||||
|
|
||||||
|
suspend fun dumpHistory(): BackupEntry {
|
||||||
|
var offset = 0
|
||||||
|
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
|
||||||
|
while (true) {
|
||||||
|
val history = db.historyDao.findAll(offset, PAGE_SIZE)
|
||||||
|
if (history.isEmpty()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset += history.size
|
||||||
|
for (item in history) {
|
||||||
|
val manga = item.manga.toJson()
|
||||||
|
val tags = JSONArray()
|
||||||
|
item.tags.forEach { tags.put(it.toJson()) }
|
||||||
|
manga.put("tags", tags)
|
||||||
|
val json = item.history.toJson()
|
||||||
|
json.put("manga", manga)
|
||||||
|
entry.data.put(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun dumpCategories(): BackupEntry {
|
||||||
|
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
|
||||||
|
val categories = db.favouriteCategoriesDao.findAll()
|
||||||
|
for (item in categories) {
|
||||||
|
entry.data.put(item.toJson())
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun dumpFavourites(): BackupEntry {
|
||||||
|
var offset = 0
|
||||||
|
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
|
||||||
|
while (true) {
|
||||||
|
val favourites = db.favouritesDao.findAll(offset, PAGE_SIZE)
|
||||||
|
if (favourites.isEmpty()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset += favourites.size
|
||||||
|
for (item in favourites) {
|
||||||
|
val manga = item.manga.toJson()
|
||||||
|
val tags = JSONArray()
|
||||||
|
item.tags.forEach { tags.put(it.toJson()) }
|
||||||
|
manga.put("tags", tags)
|
||||||
|
val json = item.favourite.toJson()
|
||||||
|
json.put("manga", manga)
|
||||||
|
entry.data.put(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createIndex(): BackupEntry {
|
||||||
|
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
|
||||||
|
val json = JSONObject()
|
||||||
|
json.put("app_id", BuildConfig.APPLICATION_ID)
|
||||||
|
json.put("app_version", BuildConfig.VERSION_CODE)
|
||||||
|
json.put("created_at", System.currentTimeMillis())
|
||||||
|
entry.data.put(json)
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MangaEntity.toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("id", id)
|
||||||
|
jo.put("title", title)
|
||||||
|
jo.put("alt_title", altTitle)
|
||||||
|
jo.put("url", url)
|
||||||
|
jo.put("public_url", publicUrl)
|
||||||
|
jo.put("rating", rating)
|
||||||
|
jo.put("cover_url", coverUrl)
|
||||||
|
jo.put("large_cover_url", largeCoverUrl)
|
||||||
|
jo.put("state", state)
|
||||||
|
jo.put("author", author)
|
||||||
|
jo.put("source", source)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TagEntity.toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("id", id)
|
||||||
|
jo.put("title", title)
|
||||||
|
jo.put("key", key)
|
||||||
|
jo.put("source", source)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HistoryEntity.toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("manga_id", mangaId)
|
||||||
|
jo.put("created_at", createdAt)
|
||||||
|
jo.put("updated_at", updatedAt)
|
||||||
|
jo.put("chapter_id", chapterId)
|
||||||
|
jo.put("page", page)
|
||||||
|
jo.put("scroll", scroll)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun FavouriteCategoryEntity.toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("category_id", categoryId)
|
||||||
|
jo.put("created_at", createdAt)
|
||||||
|
jo.put("sort_key", sortKey)
|
||||||
|
jo.put("title", title)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun FavouriteEntity.toJson(): JSONObject {
|
||||||
|
val jo = JSONObject()
|
||||||
|
jo.put("manga_id", mangaId)
|
||||||
|
jo.put("category_id", categoryId)
|
||||||
|
jo.put("created_at", createdAt)
|
||||||
|
return jo
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val PAGE_SIZE = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
|
class CompositeResult {
|
||||||
|
|
||||||
|
private var successCount: Int = 0
|
||||||
|
private val errors = ArrayList<Throwable?>()
|
||||||
|
|
||||||
|
val size: Int
|
||||||
|
get() = successCount + errors.size
|
||||||
|
|
||||||
|
val failures: List<Throwable>
|
||||||
|
get() = errors.filterNotNull()
|
||||||
|
|
||||||
|
val isAllSuccess: Boolean
|
||||||
|
get() = errors.none { it != null }
|
||||||
|
|
||||||
|
val isAllFailed: Boolean
|
||||||
|
get() = successCount == 0 && errors.isNotEmpty()
|
||||||
|
|
||||||
|
operator fun plusAssign(result: Result<*>) {
|
||||||
|
when {
|
||||||
|
result.isSuccess -> successCount++
|
||||||
|
result.isFailure -> errors.add(result.exceptionOrNull())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun plusAssign(other: CompositeResult) {
|
||||||
|
this.successCount += other.successCount
|
||||||
|
this.errors += other.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun plus(other: CompositeResult): CompositeResult {
|
||||||
|
val result = CompositeResult()
|
||||||
|
result.successCount = this.successCount + other.successCount
|
||||||
|
result.errors.addAll(this.errors)
|
||||||
|
result.errors.addAll(other.errors)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
|
import androidx.room.withTransaction
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||||
|
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getStringOrNull
|
||||||
|
import org.koitharu.kotatsu.utils.ext.iterator
|
||||||
|
import org.koitharu.kotatsu.utils.ext.map
|
||||||
|
|
||||||
|
class RestoreRepository(private val db: MangaDatabase) {
|
||||||
|
|
||||||
|
suspend fun upsertHistory(entry: BackupEntry): CompositeResult {
|
||||||
|
val result = CompositeResult()
|
||||||
|
for (item in entry.data) {
|
||||||
|
val mangaJson = item.getJSONObject("manga")
|
||||||
|
val manga = parseManga(mangaJson)
|
||||||
|
val tags = mangaJson.getJSONArray("tags").map {
|
||||||
|
parseTag(it)
|
||||||
|
}
|
||||||
|
val history = parseHistory(item)
|
||||||
|
result += runCatching {
|
||||||
|
db.withTransaction {
|
||||||
|
db.tagsDao.upsert(tags)
|
||||||
|
db.mangaDao.upsert(manga, tags)
|
||||||
|
db.historyDao.upsert(history)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun upsertCategories(entry: BackupEntry): CompositeResult {
|
||||||
|
val result = CompositeResult()
|
||||||
|
for (item in entry.data) {
|
||||||
|
val category = parseCategory(item)
|
||||||
|
result += runCatching {
|
||||||
|
db.favouriteCategoriesDao.upsert(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun upsertFavourites(entry: BackupEntry): CompositeResult {
|
||||||
|
val result = CompositeResult()
|
||||||
|
for (item in entry.data) {
|
||||||
|
val mangaJson = item.getJSONObject("manga")
|
||||||
|
val manga = parseManga(mangaJson)
|
||||||
|
val tags = mangaJson.getJSONArray("tags").map {
|
||||||
|
parseTag(it)
|
||||||
|
}
|
||||||
|
val favourite = parseFavourite(item)
|
||||||
|
result += runCatching {
|
||||||
|
db.withTransaction {
|
||||||
|
db.tagsDao.upsert(tags)
|
||||||
|
db.mangaDao.upsert(manga, tags)
|
||||||
|
db.favouritesDao.upsert(favourite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseManga(json: JSONObject) = MangaEntity(
|
||||||
|
id = json.getLong("id"),
|
||||||
|
title = json.getString("title"),
|
||||||
|
altTitle = json.getStringOrNull("alt_title"),
|
||||||
|
url = json.getString("url"),
|
||||||
|
publicUrl = json.getStringOrNull("public_url").orEmpty(),
|
||||||
|
rating = json.getDouble("rating").toFloat(),
|
||||||
|
coverUrl = json.getString("cover_url"),
|
||||||
|
largeCoverUrl = json.getStringOrNull("large_cover_url"),
|
||||||
|
state = json.getStringOrNull("state"),
|
||||||
|
author = json.getStringOrNull("author"),
|
||||||
|
source = json.getString("source")
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseTag(json: JSONObject) = TagEntity(
|
||||||
|
id = json.getLong("id"),
|
||||||
|
title = json.getString("title"),
|
||||||
|
key = json.getString("key"),
|
||||||
|
source = json.getString("source")
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseHistory(json: JSONObject) = HistoryEntity(
|
||||||
|
mangaId = json.getLong("manga_id"),
|
||||||
|
createdAt = json.getLong("created_at"),
|
||||||
|
updatedAt = json.getLong("updated_at"),
|
||||||
|
chapterId = json.getLong("chapter_id"),
|
||||||
|
page = json.getInt("page"),
|
||||||
|
scroll = json.getDouble("scroll").toFloat()
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity(
|
||||||
|
categoryId = json.getInt("category_id"),
|
||||||
|
createdAt = json.getLong("created_at"),
|
||||||
|
sortKey = json.getInt("sort_key"),
|
||||||
|
title = json.getString("title")
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
|
||||||
|
mangaId = json.getLong("manga_id"),
|
||||||
|
categoryId = json.getLong("category_id"),
|
||||||
|
createdAt = json.getLong("created_at")
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
|
import androidx.room.Room
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import org.koitharu.kotatsu.core.db.migrations.*
|
||||||
|
|
||||||
|
val databaseModule
|
||||||
|
get() = module {
|
||||||
|
single {
|
||||||
|
Room.databaseBuilder(
|
||||||
|
androidContext(),
|
||||||
|
MangaDatabase::class.java,
|
||||||
|
"kotatsu-db"
|
||||||
|
).addMigrations(
|
||||||
|
Migration1To2(),
|
||||||
|
Migration2To3(),
|
||||||
|
Migration3To4(),
|
||||||
|
Migration4To5(),
|
||||||
|
Migration5To6(),
|
||||||
|
Migration6To7()
|
||||||
|
).addCallback(
|
||||||
|
DatabasePrePopulateCallback(androidContext().resources)
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.FavouriteManga
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
abstract class FavouritesDao {
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
|
|
||||||
abstract suspend fun findAll(): List<FavouriteManga>
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
|
|
||||||
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
|
|
||||||
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
|
|
||||||
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)")
|
|
||||||
abstract suspend fun findAllManga(): List<MangaEntity>
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
|
|
||||||
abstract suspend fun find(id: Long): FavouriteManga?
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
abstract suspend fun add(favourite: FavouriteEntity)
|
|
||||||
|
|
||||||
@Query("DELETE FROM favourites WHERE manga_id = :mangaId AND category_id = :categoryId")
|
|
||||||
abstract suspend fun delete(categoryId: Long, mangaId: Long)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.HistoryWithManga
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
abstract class HistoryDao {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @hide
|
|
||||||
*/
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
|
|
||||||
abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)")
|
|
||||||
abstract suspend fun findAllManga(): List<MangaEntity>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM history WHERE manga_id = :id")
|
|
||||||
abstract suspend fun find(id: Long): HistoryEntity?
|
|
||||||
|
|
||||||
@Query("DELETE FROM history")
|
|
||||||
abstract suspend fun clear()
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
abstract suspend fun insert(entity: HistoryEntity): Long
|
|
||||||
|
|
||||||
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, updated_at = :updatedAt WHERE manga_id = :mangaId")
|
|
||||||
abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, scroll: Float, updatedAt: Long): Int
|
|
||||||
|
|
||||||
@Query("DELETE FROM history WHERE manga_id = :mangaId")
|
|
||||||
abstract suspend fun delete(mangaId: Long)
|
|
||||||
|
|
||||||
suspend fun update(entity: HistoryEntity) = update(entity.mangaId, entity.page, entity.chapterId, entity.scroll, entity.updatedAt)
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
open suspend fun upsert(entity: HistoryEntity): Boolean {
|
|
||||||
return if (update(entity) == 0) {
|
|
||||||
insert(entity)
|
|
||||||
true
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,21 @@ package org.koitharu.kotatsu.core.db
|
|||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.dao.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.*
|
import org.koitharu.kotatsu.core.db.entity.*
|
||||||
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
|
||||||
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.data.FavouritesDao
|
||||||
|
import org.koitharu.kotatsu.history.data.HistoryDao
|
||||||
|
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
||||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
|
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
|
||||||
TrackEntity::class, TrackLogEntity::class
|
TrackEntity::class, TrackLogEntity::class
|
||||||
], version = 6
|
], version = 7
|
||||||
)
|
)
|
||||||
abstract class MangaDatabase : RoomDatabase() {
|
abstract class MangaDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TagsDao {
|
abstract class TagsDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM tags")
|
@Query("SELECT * FROM tags")
|
||||||
suspend fun getAllTags(): List<TagEntity>
|
abstract suspend fun getAllTags(): List<TagEntity>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun insert(tag: TagEntity): Long
|
abstract suspend fun insert(tag: TagEntity): Long
|
||||||
|
|
||||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun update(tag: TagEntity): Int
|
abstract suspend fun update(tag: TagEntity): Int
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
suspend fun upsert(tags: Iterable<TagEntity>) {
|
open suspend fun upsert(tags: Iterable<TagEntity>) {
|
||||||
tags.forEach { tag ->
|
tags.forEach { tag ->
|
||||||
if (update(tag) <= 0) {
|
if (update(tag) <= 0) {
|
||||||
insert(tag)
|
insert(tag)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
|
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import org.koitharu.kotatsu.core.db.entity.TrackEntity
|
import org.koitharu.kotatsu.core.db.entity.TrackEntity
|
||||||
@@ -15,6 +15,7 @@ data class MangaEntity(
|
|||||||
@ColumnInfo(name = "title") val title: String,
|
@ColumnInfo(name = "title") val title: String,
|
||||||
@ColumnInfo(name = "alt_title") val altTitle: String? = null,
|
@ColumnInfo(name = "alt_title") val altTitle: String? = null,
|
||||||
@ColumnInfo(name = "url") val url: String,
|
@ColumnInfo(name = "url") val url: String,
|
||||||
|
@ColumnInfo(name = "public_url") val publicUrl: String,
|
||||||
@ColumnInfo(name = "rating") val rating: Float = Manga.NO_RATING, //normalized value [0..1] or -1
|
@ColumnInfo(name = "rating") val rating: Float = Manga.NO_RATING, //normalized value [0..1] or -1
|
||||||
@ColumnInfo(name = "cover_url") val coverUrl: String,
|
@ColumnInfo(name = "cover_url") val coverUrl: String,
|
||||||
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String? = null,
|
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String? = null,
|
||||||
@@ -30,6 +31,7 @@ data class MangaEntity(
|
|||||||
state = this.state?.let { MangaState.valueOf(it) },
|
state = this.state?.let { MangaState.valueOf(it) },
|
||||||
rating = this.rating,
|
rating = this.rating,
|
||||||
url = this.url,
|
url = this.url,
|
||||||
|
publicUrl = this.publicUrl,
|
||||||
coverUrl = this.coverUrl,
|
coverUrl = this.coverUrl,
|
||||||
largeCoverUrl = this.largeCoverUrl,
|
largeCoverUrl = this.largeCoverUrl,
|
||||||
author = this.author,
|
author = this.author,
|
||||||
@@ -42,6 +44,7 @@ data class MangaEntity(
|
|||||||
fun from(manga: Manga) = MangaEntity(
|
fun from(manga: Manga) = MangaEntity(
|
||||||
id = manga.id,
|
id = manga.id,
|
||||||
url = manga.url,
|
url = manga.url,
|
||||||
|
publicUrl = manga.publicUrl,
|
||||||
source = manga.source.name,
|
source = manga.source.name,
|
||||||
largeCoverUrl = manga.largeCoverUrl,
|
largeCoverUrl = manga.largeCoverUrl,
|
||||||
coverUrl = manga.coverUrl,
|
coverUrl = manga.coverUrl,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.db.entity
|
|||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
|
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||||
|
|
||||||
data class MangaWithTags(
|
data class MangaWithTags(
|
||||||
@Embedded val manga: MangaEntity,
|
@Embedded val manga: MangaEntity,
|
||||||
@@ -14,7 +15,7 @@ data class MangaWithTags(
|
|||||||
val tags: List<TagEntity>
|
val tags: List<TagEntity>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun toManga() = manga.toManga(tags.map {
|
fun toManga() = manga.toManga(tags.mapToSet {
|
||||||
it.toMangaTag()
|
it.toMangaTag()
|
||||||
}.toSet())
|
})
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
data class TrackEntity (
|
data class TrackEntity(
|
||||||
@PrimaryKey(autoGenerate = false)
|
@PrimaryKey(autoGenerate = false)
|
||||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||||
@ColumnInfo(name = "chapters_total") val totalChapters: Int,
|
@ColumnInfo(name = "chapters_total") val totalChapters: Int,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.room.Embedded
|
|||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
data class TrackLogWithManga(
|
data class TrackLogWithManga(
|
||||||
@@ -24,7 +25,7 @@ data class TrackLogWithManga(
|
|||||||
fun toTrackingLogItem() = TrackingLogItem(
|
fun toTrackingLogItem() = TrackingLogItem(
|
||||||
id = trackLog.id,
|
id = trackLog.id,
|
||||||
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
|
chapters = trackLog.chapters.split('\n').filterNot { x -> x.isEmpty() },
|
||||||
manga = manga.toManga(tags.map { x -> x.toMangaTag() }.toSet()),
|
manga = manga.toManga(tags.mapToSet { x -> x.toMangaTag() }),
|
||||||
createdAt = Date(trackLog.createdAt)
|
createdAt = Date(trackLog.createdAt)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
|||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
object Migration1To2 : Migration(1, 2) {
|
class Migration1To2 : Migration(1, 2) {
|
||||||
/**
|
/**
|
||||||
* Adding foreign keys
|
* Adding foreign keys
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
|||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
object Migration2To3 : Migration(2, 3) {
|
class Migration2To3 : Migration(2, 3) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")
|
database.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
|||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
object Migration3To4 : Migration(3, 4) {
|
class Migration3To4 : Migration(3, 4) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
database.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
|||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
object Migration4To5 : Migration(4, 5) {
|
class Migration4To5 : Migration(4, 5) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0")
|
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.migrations
|
|||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
object Migration5To6 : Migration(5, 6) {
|
class Migration5To6 : Migration(5, 6) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
|
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.migrations
|
||||||
|
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
class Migration6To7 : Migration(6, 7) {
|
||||||
|
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("ALTER TABLE manga ADD COLUMN public_url TEXT NOT NULL DEFAULT ''")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu.core.exceptions
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
|
||||||
|
|
||||||
|
class AuthRequiredException(
|
||||||
|
val url: String
|
||||||
|
) : RuntimeException("Authorization required"), ResolvableException {
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
override val resolveTextId: Int = R.string.sign_in
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.core.exceptions
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import okio.IOException
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
|
||||||
|
|
||||||
|
class CloudFlareProtectedException(
|
||||||
|
val url: String
|
||||||
|
) : IOException("Protected by CloudFlare"), ResolvableException {
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
override val resolveTextId: Int = R.string.captcha_solve
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions
|
package org.koitharu.kotatsu.core.exceptions
|
||||||
|
|
||||||
class ParseException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
|
class ParseException(message: String? = null, cause: Throwable? = null) :
|
||||||
|
RuntimeException(message, cause)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package org.koitharu.kotatsu.core.exceptions
|
||||||
|
|
||||||
|
class WrongPasswordException : SecurityException()
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.koitharu.kotatsu.core.exceptions.resolve
|
||||||
|
|
||||||
|
import android.util.ArrayMap
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.AuthRequiredException
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
class ExceptionResolver(
|
||||||
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
|
private val fm: FragmentManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
|
||||||
|
|
||||||
|
suspend fun resolve(e: ResolvableException): Boolean = when (e) {
|
||||||
|
is CloudFlareProtectedException -> resolveCF(e.url)
|
||||||
|
is AuthRequiredException -> false //TODO
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveCF(url: String) = suspendCancellableCoroutine<Boolean> { cont ->
|
||||||
|
val dialog = CloudFlareDialog.newInstance(url)
|
||||||
|
fm.clearFragmentResult(CloudFlareDialog.TAG)
|
||||||
|
continuations[CloudFlareDialog.TAG] = cont
|
||||||
|
fm.setFragmentResultListener(CloudFlareDialog.TAG, lifecycleOwner) { key, result ->
|
||||||
|
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
|
||||||
|
}
|
||||||
|
dialog.show(fm, CloudFlareDialog.TAG)
|
||||||
|
cont.invokeOnCancellation {
|
||||||
|
continuations.remove(CloudFlareDialog.TAG, cont)
|
||||||
|
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package org.koitharu.kotatsu.core.exceptions.resolve
|
||||||
|
|
||||||
|
interface ResolvableException {
|
||||||
|
|
||||||
|
val resolveTextId: Int
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.core.github
|
package org.koitharu.kotatsu.core.github
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppVersion(
|
data class AppVersion(
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.core.github
|
||||||
|
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val githubModule
|
||||||
|
get() = module {
|
||||||
|
single {
|
||||||
|
GithubRepository(get())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,10 @@ package org.koitharu.kotatsu.core.github
|
|||||||
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.inject
|
|
||||||
import org.koitharu.kotatsu.utils.ext.await
|
import org.koitharu.kotatsu.utils.ext.await
|
||||||
import org.koitharu.kotatsu.utils.ext.parseJson
|
import org.koitharu.kotatsu.utils.ext.parseJson
|
||||||
|
|
||||||
class GithubRepository : KoinComponent {
|
class GithubRepository(private val okHttp: OkHttpClient) {
|
||||||
|
|
||||||
private val okHttp by inject<OkHttpClient>()
|
|
||||||
|
|
||||||
suspend fun getLatestVersion(): AppVersion {
|
suspend fun getLatestVersion(): AppVersion {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ data class VersionId(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
private fun variantWeight(variantType: String) =
|
private fun variantWeight(variantType: String) =
|
||||||
when (variantType.toLowerCase(Locale.ROOT)) {
|
when (variantType.toLowerCase(Locale.ROOT)) {
|
||||||
"a", "alpha" -> 1
|
"a", "alpha" -> 1
|
||||||
@@ -42,7 +41,6 @@ data class VersionId(
|
|||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun parse(versionName: String): VersionId {
|
fun parse(versionName: String): VersionId {
|
||||||
val parts = versionName.substringBeforeLast('-').split('.')
|
val parts = versionName.substringBeforeLast('-').split('.')
|
||||||
val variant = versionName.substringAfterLast('-', "")
|
val variant = versionName.substringAfterLast('-', "")
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.local
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FilenameFilter
|
|
||||||
|
|
||||||
class CbzFilter : FilenameFilter {
|
|
||||||
|
|
||||||
override fun accept(dir: File, name: String) =
|
|
||||||
name.endsWith(".cbz", ignoreCase = true) || name.endsWith(".zip", ignoreCase = true)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Francisco José Montiel Navarro.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.koitharu.kotatsu.core.local.cookies
|
|
||||||
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This interface extends [okhttp3.CookieJar] and adds methods to clear the cookies.
|
|
||||||
*/
|
|
||||||
interface ClearableCookieJar : CookieJar {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all the session cookies while maintaining the persisted ones.
|
|
||||||
*/
|
|
||||||
fun clearSession()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all the cookies from persistence and from the cache.
|
|
||||||
*/
|
|
||||||
fun clear()
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Francisco José Montiel Navarro.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.koitharu.kotatsu.core.local.cookies
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.local.cookies.persistence.CookiePersistor
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import org.koitharu.kotatsu.core.local.cookies.cache.CookieCache
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class PersistentCookieJar(
|
|
||||||
private val cache: CookieCache,
|
|
||||||
private val persistor: CookiePersistor
|
|
||||||
) : ClearableCookieJar {
|
|
||||||
|
|
||||||
init {
|
|
||||||
cache.addAll(persistor.loadAll())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
|
||||||
cache.addAll(cookies)
|
|
||||||
persistor.saveAll(filterPersistentCookies(cookies))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
|
||||||
val cookiesToRemove: MutableList<Cookie> = ArrayList()
|
|
||||||
val validCookies: MutableList<Cookie> = ArrayList()
|
|
||||||
val it = cache.iterator()
|
|
||||||
while (it.hasNext()) {
|
|
||||||
val currentCookie = it.next()
|
|
||||||
if (isCookieExpired(currentCookie)) {
|
|
||||||
cookiesToRemove.add(currentCookie)
|
|
||||||
it.remove()
|
|
||||||
} else if (currentCookie.matches(url)) {
|
|
||||||
validCookies.add(currentCookie)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
persistor.removeAll(cookiesToRemove)
|
|
||||||
return validCookies
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun clearSession() {
|
|
||||||
cache.clear()
|
|
||||||
cache.addAll(persistor.loadAll())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun clear() {
|
|
||||||
cache.clear()
|
|
||||||
persistor.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun filterPersistentCookies(cookies: List<Cookie>): List<Cookie> {
|
|
||||||
val persistentCookies: MutableList<Cookie> = ArrayList()
|
|
||||||
for (cookie in cookies) {
|
|
||||||
if (cookie.persistent) {
|
|
||||||
persistentCookies.add(cookie)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return persistentCookies
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun isCookieExpired(cookie: Cookie): Boolean {
|
|
||||||
return cookie.expiresAt < System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Francisco José Montiel Navarro.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.koitharu.kotatsu.core.local.cookies.cache
|
|
||||||
|
|
||||||
import okhttp3.Cookie
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A CookieCache handles the volatile cookie session storage.
|
|
||||||
*/
|
|
||||||
interface CookieCache : MutableIterable<Cookie> {
|
|
||||||
/**
|
|
||||||
* Add all the new cookies to the session, existing cookies will be overwritten.
|
|
||||||
*
|
|
||||||
* @param newCookies
|
|
||||||
*/
|
|
||||||
fun addAll(newCookies: Collection<Cookie>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all the cookies from the session.
|
|
||||||
*/
|
|
||||||
fun clear()
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Francisco José Montiel Navarro.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.koitharu.kotatsu.core.local.cookies.cache
|
|
||||||
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class decorates a Cookie to re-implements equals() and hashcode() methods in order to identify
|
|
||||||
* the cookie by the following attributes: name, domain, path, secure & hostOnly.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This new behaviour will be useful in determining when an already existing cookie in session must be overwritten.
|
|
||||||
*/
|
|
||||||
internal class IdentifiableCookie(val cookie: Cookie) {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other !is IdentifiableCookie) return false
|
|
||||||
return other.cookie.name == cookie.name && other.cookie.domain == cookie.domain
|
|
||||||
&& other.cookie.path == cookie.path && other.cookie.secure == cookie.secure
|
|
||||||
&& other.cookie.hostOnly == cookie.hostOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var hash = 17
|
|
||||||
hash = 31 * hash + cookie.name.hashCode()
|
|
||||||
hash = 31 * hash + cookie.domain.hashCode()
|
|
||||||
hash = 31 * hash + cookie.path.hashCode()
|
|
||||||
hash = 31 * hash + if (cookie.secure) 0 else 1
|
|
||||||
hash = 31 * hash + if (cookie.hostOnly) 0 else 1
|
|
||||||
return hash
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun decorateAll(cookies: Collection<Cookie>): List<IdentifiableCookie> {
|
|
||||||
val identifiableCookies: MutableList<IdentifiableCookie> = ArrayList(cookies.size)
|
|
||||||
for (cookie in cookies) {
|
|
||||||
identifiableCookies.add(IdentifiableCookie(cookie))
|
|
||||||
}
|
|
||||||
return identifiableCookies
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Francisco José Montiel Navarro.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.koitharu.kotatsu.core.local.cookies.cache
|
|
||||||
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import org.koitharu.kotatsu.core.local.cookies.cache.IdentifiableCookie.Companion.decorateAll
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
class SetCookieCache : CookieCache {
|
|
||||||
|
|
||||||
private val cookies: MutableSet<IdentifiableCookie> = Collections.newSetFromMap(ConcurrentHashMap())
|
|
||||||
|
|
||||||
override fun addAll(newCookies: Collection<Cookie>) {
|
|
||||||
for (cookie in decorateAll(newCookies)) {
|
|
||||||
cookies.remove(cookie)
|
|
||||||
cookies.add(cookie)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clear() {
|
|
||||||
cookies.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator()
|
|
||||||
|
|
||||||
private inner class SetCookieCacheIterator : MutableIterator<Cookie> {
|
|
||||||
|
|
||||||
private val iterator = cookies.iterator()
|
|
||||||
|
|
||||||
override fun hasNext(): Boolean {
|
|
||||||
return iterator.hasNext()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun next(): Cookie {
|
|
||||||
return iterator.next().cookie
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun remove() {
|
|
||||||
iterator.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Francisco José Montiel Navarro.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.koitharu.kotatsu.core.local.cookies.persistence
|
|
||||||
|
|
||||||
import okhttp3.Cookie
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A CookiePersistor handles the persistent cookie storage.
|
|
||||||
*/
|
|
||||||
interface CookiePersistor {
|
|
||||||
|
|
||||||
fun loadAll(): List<Cookie>
|
|
||||||
/**
|
|
||||||
* Persist all cookies, existing cookies will be overwritten.
|
|
||||||
*
|
|
||||||
* @param cookies cookies persist
|
|
||||||
*/
|
|
||||||
fun saveAll(cookies: Collection<Cookie>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes indicated cookies from persistence.
|
|
||||||
*
|
|
||||||
* @param cookies cookies to remove from persistence
|
|
||||||
*/
|
|
||||||
fun removeAll(cookies: Collection<Cookie>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all cookies from persistence.
|
|
||||||
*/
|
|
||||||
fun clear()
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Francisco José Montiel Navarro.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.koitharu.kotatsu.core.local.cookies.persistence
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import java.io.*
|
|
||||||
|
|
||||||
class SerializableCookie : Serializable {
|
|
||||||
|
|
||||||
@Transient
|
|
||||||
private var cookie: Cookie? = null
|
|
||||||
|
|
||||||
fun encode(cookie: Cookie?): String? {
|
|
||||||
this.cookie = cookie
|
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
|
||||||
var objectOutputStream: ObjectOutputStream? = null
|
|
||||||
try {
|
|
||||||
objectOutputStream = ObjectOutputStream(byteArrayOutputStream)
|
|
||||||
objectOutputStream.writeObject(this)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.d(TAG, "IOException in encodeCookie", e)
|
|
||||||
return null
|
|
||||||
} finally {
|
|
||||||
if (objectOutputStream != null) {
|
|
||||||
try { // Closing a ByteArrayOutputStream has no effect, it can be used later (and is used in the return statement)
|
|
||||||
objectOutputStream.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.d(TAG, "Stream not closed in encodeCookie", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return byteArrayToHexString(byteArrayOutputStream.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decode(encodedCookie: String): Cookie? {
|
|
||||||
val bytes = hexStringToByteArray(encodedCookie)
|
|
||||||
val byteArrayInputStream = ByteArrayInputStream(
|
|
||||||
bytes)
|
|
||||||
var cookie: Cookie? = null
|
|
||||||
var objectInputStream: ObjectInputStream? = null
|
|
||||||
try {
|
|
||||||
objectInputStream = ObjectInputStream(byteArrayInputStream)
|
|
||||||
cookie = (objectInputStream.readObject() as SerializableCookie).cookie
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.d(TAG, "IOException in decodeCookie", e)
|
|
||||||
} catch (e: ClassNotFoundException) {
|
|
||||||
Log.d(TAG, "ClassNotFoundException in decodeCookie", e)
|
|
||||||
} finally {
|
|
||||||
if (objectInputStream != null) {
|
|
||||||
try {
|
|
||||||
objectInputStream.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.d(TAG, "Stream not closed in decodeCookie", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cookie
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun writeObject(out: ObjectOutputStream) {
|
|
||||||
out.writeObject(cookie!!.name)
|
|
||||||
out.writeObject(cookie!!.value)
|
|
||||||
out.writeLong(if (cookie!!.persistent) cookie!!.expiresAt else NON_VALID_EXPIRES_AT)
|
|
||||||
out.writeObject(cookie!!.domain)
|
|
||||||
out.writeObject(cookie!!.path)
|
|
||||||
out.writeBoolean(cookie!!.secure)
|
|
||||||
out.writeBoolean(cookie!!.httpOnly)
|
|
||||||
out.writeBoolean(cookie!!.hostOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class, ClassNotFoundException::class)
|
|
||||||
private fun readObject(`in`: ObjectInputStream) {
|
|
||||||
val builder = Cookie.Builder()
|
|
||||||
builder.name((`in`.readObject() as String))
|
|
||||||
builder.value((`in`.readObject() as String))
|
|
||||||
val expiresAt = `in`.readLong()
|
|
||||||
if (expiresAt != NON_VALID_EXPIRES_AT) {
|
|
||||||
builder.expiresAt(expiresAt)
|
|
||||||
}
|
|
||||||
val domain = `in`.readObject() as String
|
|
||||||
builder.domain(domain)
|
|
||||||
builder.path((`in`.readObject() as String))
|
|
||||||
if (`in`.readBoolean()) builder.secure()
|
|
||||||
if (`in`.readBoolean()) builder.httpOnly()
|
|
||||||
if (`in`.readBoolean()) builder.hostOnlyDomain(domain)
|
|
||||||
cookie = builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
private val TAG = SerializableCookie::class.java.simpleName
|
|
||||||
|
|
||||||
const val serialVersionUID = -8594045714036645534L
|
|
||||||
private const val NON_VALID_EXPIRES_AT = -1L
|
|
||||||
/**
|
|
||||||
* Using some super basic byte array <-> hex conversions so we don't
|
|
||||||
* have to rely on any large Base64 libraries. Can be overridden if you
|
|
||||||
* like!
|
|
||||||
*
|
|
||||||
* @param bytes byte array to be converted
|
|
||||||
* @return string containing hex values
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
private fun byteArrayToHexString(bytes: ByteArray): String {
|
|
||||||
val sb = StringBuilder(bytes.size * 2)
|
|
||||||
for (element in bytes) {
|
|
||||||
val v: Int = element.toInt() and 0xff
|
|
||||||
if (v < 16) {
|
|
||||||
sb.append('0')
|
|
||||||
}
|
|
||||||
sb.append(Integer.toHexString(v))
|
|
||||||
}
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts hex values from strings to byte array
|
|
||||||
*
|
|
||||||
* @param hexString string of hex-encoded values
|
|
||||||
* @return decoded byte array
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
private fun hexStringToByteArray(hexString: String): ByteArray {
|
|
||||||
val len = hexString.length
|
|
||||||
val data = ByteArray(len / 2)
|
|
||||||
var i = 0
|
|
||||||
while (i < len) {
|
|
||||||
data[i / 2] = ((Character.digit(hexString[i], 16) shl 4) + Character
|
|
||||||
.digit(hexString[i + 1], 16)).toByte()
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||