Compare commits
225
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ebbf6e349 | ||
|
|
1b71f6a60a | ||
|
|
a3241e48df | ||
|
|
69396f3d59 | ||
|
|
9042d065ae | ||
|
|
300505e127 | ||
|
|
f7bf1d624b | ||
|
|
d7b08a0181 | ||
|
|
3941f59bbc | ||
|
|
802a30e16c | ||
|
|
032563d7a4 | ||
|
|
fe7ad2032b | ||
|
|
fcde4b3fc2 | ||
|
|
1073ed6c24 | ||
|
|
e5ce3450e8 | ||
|
|
a6f1e91747 | ||
|
|
8dcf5a3654 | ||
|
|
22f9cf7c4c | ||
|
|
38f2879d82 | ||
|
|
4060695db6 | ||
|
|
81fab695ff | ||
|
|
5259428106 | ||
|
|
9774bd858f | ||
|
|
d30379251d | ||
|
|
0e6156e51c | ||
|
|
d610c9727d | ||
|
|
adba04e2dd | ||
|
|
b2aa6552b1 | ||
|
|
8b4b060425 | ||
|
|
2f11853f0d | ||
|
|
388685b784 | ||
|
|
d6939b2913 | ||
|
|
90ee3b749f | ||
|
|
e51c297a7d | ||
|
|
283e8f20b3 | ||
|
|
850c3fb8cd | ||
|
|
7046e994d7 | ||
|
|
099d802a03 | ||
|
|
453df64f69 | ||
|
|
8a2cb42546 | ||
|
|
6413ed67c6 | ||
|
|
4dc7a97322 | ||
|
|
a2d8189c74 | ||
|
|
f806c82c33 | ||
|
|
f46598cd74 | ||
|
|
919d34c90d | ||
|
|
e230af8991 | ||
|
|
09842f5d72 | ||
|
|
911ff31f69 | ||
|
|
f1c9c4e231 | ||
|
|
fa04ce5465 | ||
|
|
45ff03abfd | ||
|
|
20cebb8a05 | ||
|
|
f79e3ff51f | ||
|
|
45a67a7f3d | ||
|
|
281469fe5c | ||
|
|
461ad7fa42 | ||
|
|
899d221b67 | ||
|
|
b246e163d2 | ||
|
|
d83bb0f964 | ||
|
|
a9626475e6 | ||
|
|
d1374eca15 | ||
|
|
6289162684 | ||
|
|
25b3187094 | ||
|
|
bd2c9b172a | ||
|
|
246f953b4d | ||
|
|
433098a388 | ||
|
|
f442d9d030 | ||
|
|
61d7ae03f6 | ||
|
|
22e144cb3b | ||
|
|
a3f0d7092d | ||
|
|
27efdba185 | ||
|
|
59a1fa9992 | ||
|
|
a5140ca293 | ||
|
|
a639b02890 | ||
|
|
ead64d3c73 | ||
|
|
be761071f8 | ||
|
|
e48f19269f | ||
|
|
82ebfb99c5 | ||
|
|
4d69bf2aba | ||
|
|
4e4f77de90 | ||
|
|
fe0c3176d8 | ||
|
|
8cfbdf46e0 | ||
|
|
2234d056fc | ||
|
|
4a4c447bd2 | ||
|
|
933a95023c | ||
|
|
1d8b5f81a1 | ||
|
|
81df632e48 | ||
|
|
b0df7cbc68 | ||
|
|
25d90a3c78 | ||
|
|
384637efdf | ||
|
|
ef3d3e6443 | ||
|
|
675e98fa1a | ||
|
|
0b059c71c1 | ||
|
|
8988f08243 | ||
|
|
adb54d9d7c | ||
|
|
fdf58388a3 | ||
|
|
a5244cdb41 | ||
|
|
6caec5dbce | ||
|
|
4419ab154b | ||
|
|
67c529ffe9 | ||
|
|
057ba0c6e1 | ||
|
|
6117ea755f | ||
|
|
a56c923f0e | ||
|
|
3686241bf5 | ||
|
|
0311c6f52b | ||
|
|
f59dccd5fd | ||
|
|
01d8dabef4 | ||
|
|
773993c6f2 | ||
|
|
2c5646b1c2 | ||
|
|
b8d71e24e7 | ||
|
|
4db3f7a742 | ||
|
|
298203f909 | ||
|
|
a1cafe62cf | ||
|
|
cab75ef0d6 | ||
|
|
33a8d86684 | ||
|
|
b4d329725b | ||
|
|
6efe8922b3 | ||
|
|
f5bd097584 | ||
|
|
7492841b5f | ||
|
|
1430bd9e88 | ||
|
|
e0400305ab | ||
|
|
65d5120272 | ||
|
|
cbb398e58c | ||
|
|
57d8735624 | ||
|
|
fd99bd2ed1 | ||
|
|
46c89534fd | ||
|
|
5650ee580f | ||
|
|
62f126b5d9 | ||
|
|
51cb354f35 | ||
|
|
d4f591354d | ||
|
|
0895af0ecc | ||
|
|
fafe25fee4 | ||
|
|
3db6cc1a1c | ||
|
|
408f5a05fc | ||
|
|
28ee23c5f3 | ||
|
|
37804e67c1 | ||
|
|
a7bce9c8e9 | ||
|
|
bcf2d16a10 | ||
|
|
28d2b92abb | ||
|
|
2f3b6df529 | ||
|
|
fa2cbbb0cc | ||
|
|
d1ab9e77de | ||
|
|
3484261b50 | ||
|
|
7a03fb53ea | ||
|
|
d7f050efc4 | ||
|
|
2d11a29a79 | ||
|
|
0b5aecfdb5 | ||
|
|
487925b1f2 | ||
|
|
54c8183666 | ||
|
|
c878952237 | ||
|
|
26d6428836 | ||
|
|
cf31a31430 | ||
|
|
e10e1c665f | ||
|
|
0834ff26be | ||
|
|
014b8db20d | ||
|
|
34445e6f92 | ||
|
|
7d00e01e00 | ||
|
|
300fabca21 | ||
|
|
e17dc78aeb | ||
|
|
1b9e00efe1 | ||
|
|
152b4d3765 | ||
|
|
e0b95ab2ae | ||
|
|
e0520edccc | ||
|
|
581b32e8a2 | ||
|
|
e71d61ef37 | ||
|
|
0a51d48703 | ||
|
|
30c7f9e020 | ||
|
|
71f7771a97 | ||
|
|
b36e5b7906 | ||
|
|
bb25449b35 | ||
|
|
dc8053dfcd | ||
|
|
20be8e23ba | ||
|
|
d83f476439 | ||
|
|
29352b8838 | ||
|
|
c0390c3c5d | ||
|
|
81dd2eeb47 | ||
|
|
3beedf4dfe | ||
|
|
f5be71ecbd | ||
|
|
58ba5dfedc | ||
|
|
040d265f3c | ||
|
|
fccf4db163 | ||
|
|
1122498b54 | ||
|
|
6a98aea209 | ||
|
|
ae4d589c8e | ||
|
|
6ccb59957d | ||
|
|
edd4e865b3 | ||
|
|
6ff7614e94 | ||
|
|
065a14952a | ||
|
|
1e620931a2 | ||
|
|
a46a531a89 | ||
|
|
06c95b8d36 | ||
|
|
39967a9211 | ||
|
|
a18e6f4b63 | ||
|
|
ef475000c3 | ||
|
|
f08a2444be | ||
|
|
c746d58210 | ||
|
|
ccb48505bf | ||
|
|
c9ddd25e99 | ||
|
|
42234d9163 | ||
|
|
e058885c7c | ||
|
|
1e3556cc8e | ||
|
|
b1cda242df | ||
|
|
b593fce745 | ||
|
|
282cdeaad0 | ||
|
|
6e54c7d672 | ||
|
|
51a12ee5cb | ||
|
|
fcea40685c | ||
|
|
07450a7a51 | ||
|
|
19fa8d15a6 | ||
|
|
db2980f598 | ||
|
|
b3e4fd65aa | ||
|
|
d46eca750c | ||
|
|
d441e92adc | ||
|
|
5f70d71203 | ||
|
|
59ffb2ea8e | ||
|
|
ce59b8037f | ||
|
|
56e67ba70a | ||
|
|
108e9e216e | ||
|
|
b219b4e832 | ||
|
|
ef4e0d76ec | ||
|
|
61c75c06a9 | ||
|
|
091705b3f2 | ||
|
|
6aa5b06619 | ||
|
|
df8fc0b229 |
No files matched your search
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
|
||||
|
||||
- name: Push Apks
|
||||
uses: cpina/github-action-push-to-another-repository@v1.5
|
||||
uses: Ehviewer-Overhauled/github-action-push-to-another-repository@main
|
||||
env:
|
||||
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||
with:
|
||||
@@ -68,5 +68,6 @@ jobs:
|
||||
user-name: 'github-actions[bot]'
|
||||
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||
target-branch: deploy
|
||||
create-target-branch: true
|
||||
commit-message: Add apks from $GITHUB_REF
|
||||
target-directory: fdroid/tmp-apks
|
||||
@@ -1,10 +1,10 @@
|
||||
<p align="right">
|
||||
<strong>English<strong>
|
||||
<strong>English</strong>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
|
||||
简体中文
|
||||
</a>
|
||||
<span> | <span>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
|
||||
正體中文
|
||||
</a>
|
||||
@@ -66,19 +66,13 @@ with Material Design 3 and Dynamic Color Support
|
||||
|
||||
# Download
|
||||
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Git it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
|
||||
|
||||
# Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
# Localization
|
||||
|
||||
Thank you to each of the **Ehviewer Overhauled** translators, and if you would like to contribute, please submit a translation via [Weblate](https://hosted.weblate.org/engage/ehviewer-overhauled/).
|
||||
|
||||
[<img src="https://hosted.weblate.org/widgets/ehviewer-overhauled/-/horizontal-auto.svg" alt="" />](https://hosted.weblate.org/engage/ehviewer-overhauled/)
|
||||
|
||||
# Thanks
|
||||
|
||||
Here is the libraries
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-parcelize'
|
||||
id 'com.google.devtools.ksp'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "33.0.1"
|
||||
ndkVersion "25.1.8937393"
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable = true
|
||||
reset()
|
||||
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
|
||||
universalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
sign {
|
||||
storeFile file(signStoreFile)
|
||||
storePassword signStorePassword
|
||||
keyAlias signkeyAlias
|
||||
keyPassword signkeyPassword
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "moe.tarsin.ehviewer"
|
||||
minSdkVersion 28
|
||||
targetSdkVersion 33
|
||||
versionCode 180031
|
||||
versionName "1.8.5.3"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th', 'nb-rNO'
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'src/main/cpp/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_19
|
||||
targetCompatibility JavaVersion.VERSION_19
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '19'
|
||||
freeCompilerArgs += [
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive"
|
||||
]
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError true
|
||||
checkReleaseBuilds false
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += "/META-INF/CHANGES"
|
||||
excludes += "/META-INF/README.md"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo.includeInApk false
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.sign
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
compose true
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.0"
|
||||
}
|
||||
|
||||
namespace 'com.hippo.ehviewer'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation "androidx.activity:activity-compose:1.6.1"
|
||||
implementation "androidx.appcompat:appcompat:1.6.0"
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
implementation "androidx.browser:browser:1.4.0"
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
||||
implementation platform("androidx.compose:compose-bom:2023.01.00")
|
||||
implementation "androidx.compose.material:material-icons-extended"
|
||||
implementation "androidx.compose.material3:material3"
|
||||
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha05"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.5.1"
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||
def nav_version = "2.5.3"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-compose:$nav_version"
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.2.0-alpha03"
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/room
|
||||
def room_version = "2.5.0"
|
||||
ksp "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-paging:$room_version"
|
||||
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
implementation 'com.drakeet.drawer:drawer:1.0.3'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Dead Dependency
|
||||
implementation 'com.github.tachiyomiorg:DirectionalViewPager:1.0.0' // Dead Dependency
|
||||
// https://github.com/google/accompanist/releases
|
||||
implementation "com.google.accompanist:accompanist-themeadapter-material3:0.28.0"
|
||||
implementation 'com.google.android.material:material:1.8.0-rc01'
|
||||
|
||||
// https://square.github.io/okhttp/changelogs/changelog/
|
||||
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp-coroutines'
|
||||
|
||||
implementation 'com.squareup.okio:okio-jvm:3.3.0'
|
||||
|
||||
implementation 'dev.chrisbanes.insetter:insetter:0.6.1' // Dead Dependency
|
||||
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
|
||||
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
|
||||
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
|
||||
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
|
||||
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
|
||||
// https://coil-kt.github.io/coil/changelog/
|
||||
// implementation 'io.coil-kt:coil:2.2.2'
|
||||
implementation 'com.github.Ehviewer-Overhauled.coil:coil:d65881d'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
|
||||
exclude group: 'dev.rikka.rikkax.material', module: 'material'
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-android-extensions-runtime'
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "33.0.1"
|
||||
ndkVersion = "25.2.9519653"
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
val signConfig = signingConfigs.create("release") {
|
||||
storeFile = File(projectDir.path + "/keystore/androidkey.jks")
|
||||
storePassword = "000000"
|
||||
keyAlias = "key0"
|
||||
keyPassword = "000000"
|
||||
|
||||
enableV3Signing = true
|
||||
enableV4Signing = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
versionCode = 180031
|
||||
versionName = "1.8.6.0-alpha03"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
"zh-rCN",
|
||||
"zh-rHK",
|
||||
"zh-rTW",
|
||||
"es",
|
||||
"ja",
|
||||
"ko",
|
||||
"fr",
|
||||
"de",
|
||||
"th",
|
||||
"tr",
|
||||
"nb-rNO"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = File("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_19
|
||||
targetCompatibility = JavaVersion.VERSION_19
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "19"
|
||||
freeCompilerArgs = listOf(
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive",
|
||||
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = true
|
||||
checkReleaseBuilds = false
|
||||
disable.add("MissingTranslation")
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/CHANGES"
|
||||
excludes += "/META-INF/README.md"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo.includeInApk = false
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles("proguard-rules.pro")
|
||||
signingConfig = signConfig
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.2"
|
||||
}
|
||||
|
||||
namespace = "com.hippo.ehviewer"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation("androidx.activity:activity-compose:1.6.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
|
||||
implementation("androidx.browser:browser:1.5.0")
|
||||
implementation("androidx.collection:collection-ktx:1.3.0-alpha02")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
||||
implementation(platform("androidx.compose:compose-bom:2023.01.00"))
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.10.0-alpha02")
|
||||
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.0-alpha07")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.5.1")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||
val nav_version = "2.5.3"
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
|
||||
implementation("androidx.navigation:navigation-compose:$nav_version")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||
implementation("androidx.paging:paging-runtime-ktx:3.2.0-alpha04")
|
||||
implementation("androidx.paging:paging-compose:1.0.0-alpha18")
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.0-rc01")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/room
|
||||
val room_version = "2.5.0"
|
||||
ksp("androidx.room:room-compiler:$room_version")
|
||||
implementation("androidx.room:room-paging:$room_version")
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
implementation("com.drakeet.drawer:drawer:1.0.3")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
|
||||
// https://github.com/google/accompanist/releases
|
||||
implementation("com.google.accompanist:accompanist-themeadapter-material3:0.28.0")
|
||||
implementation("com.google.android.material:material:1.8.0")
|
||||
|
||||
// https://square.github.io/okhttp/changelogs/changelog/
|
||||
implementation("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11")
|
||||
implementation("com.squareup.okhttp3:okhttp-coroutines")
|
||||
|
||||
implementation("com.squareup.okio:okio-jvm:3.3.0")
|
||||
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.6.1") // Dead Dependency
|
||||
implementation("dev.rikka.rikkax.core:core-ktx:1.4.1")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
||||
implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3")
|
||||
implementation("dev.rikka.rikkax.material:material-preference:2.0.0")
|
||||
// https://coil-kt.github.io/coil/changelog/
|
||||
// implementation 'io.coil-kt:coil:2.2.2'
|
||||
implementation("com.github.Ehviewer-Overhauled.coil:coil-compose:53302f2")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.0-RC")
|
||||
implementation("org.jsoup:jsoup:1.15.3")
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude("dev.rikka.rikkax.appcompat", "appcompat")
|
||||
exclude("dev.rikka.rikkax.material", "material")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-android-extensions-runtime")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
|
||||
}
|
||||
Vendored
+8
@@ -6,8 +6,16 @@
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# https://issuetracker.google.com/issues/190382641
|
||||
-keepclassmembers class kotlin.SafePublicationLazyImpl {
|
||||
java.lang.Object _value;
|
||||
}
|
||||
|
||||
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
-keepattributes LineNumberTable,SourceFile
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
#include "ehviewer.h"
|
||||
|
||||
// TODO: Replace it with AFileDescriptor_getFd when minsdk 31
|
||||
// https://developer.android.com/ndk/reference/group/file-descriptor
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||
jint fd = -1;
|
||||
@@ -23,21 +25,3 @@ Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_Native_mapFd(JNIEnv *env, jclass clazz, jint fd, jlong capability) {
|
||||
void *addr = mmap(0, capability, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
|
||||
if (addr != MAP_FAILED) {
|
||||
return (*env)->NewDirectByteBuffer(env, addr, capability);
|
||||
} else {
|
||||
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_Native_unmapDirectByteBuffer(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
if (munmap(addr, size)) LOGE("munmap addr:%p, size%zu failed!", addr, size);
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
package androidx.navigation.ui
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavOptions
|
||||
|
||||
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
|
||||
@@ -27,13 +25,6 @@ fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boo
|
||||
.setPopEnterAnim(com.hippo.ehviewer.R.anim.scene_close_enter)
|
||||
.setPopExitAnim(com.hippo.ehviewer.R.anim.scene_close_exit)
|
||||
}
|
||||
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
|
||||
builder.setPopUpTo(
|
||||
navController.graph.findStartDestination().id,
|
||||
inclusive = false,
|
||||
saveState = false
|
||||
)
|
||||
}
|
||||
val options = builder.build()
|
||||
return try {
|
||||
// TODO provide proper API instead of using Exceptions as Control-Flow.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
@file:JvmName("GifDecodeUtils")
|
||||
|
||||
package coil.decode
|
||||
|
||||
import kotlin.experimental.and
|
||||
import okio.BufferedSource
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
|
||||
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
private val GIF_HEADER_87A = "GIF87a".encodeUtf8()
|
||||
private val GIF_HEADER_89A = "GIF89a".encodeUtf8()
|
||||
|
||||
// https://developers.google.com/speed/webp/docs/riff_container
|
||||
private val WEBP_HEADER_RIFF = "RIFF".encodeUtf8()
|
||||
private val WEBP_HEADER_WEBP = "WEBP".encodeUtf8()
|
||||
private val WEBP_HEADER_VPX8 = "VP8X".encodeUtf8()
|
||||
|
||||
// https://nokiatech.github.io/heif/technical.html
|
||||
private val HEIF_HEADER_FTYP = "ftyp".encodeUtf8()
|
||||
private val HEIF_HEADER_MSF1 = "msf1".encodeUtf8()
|
||||
private val HEIF_HEADER_HEVC = "hevc".encodeUtf8()
|
||||
private val HEIF_HEADER_HEVX = "hevx".encodeUtf8()
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains a GIF image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isGif(source: BufferedSource): Boolean {
|
||||
return source.rangeEquals(0, GIF_HEADER_89A) ||
|
||||
source.rangeEquals(0, GIF_HEADER_87A)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains a WebP image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isWebP(source: BufferedSource): Boolean {
|
||||
return source.rangeEquals(0, WEBP_HEADER_RIFF) &&
|
||||
source.rangeEquals(8, WEBP_HEADER_WEBP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains an animated WebP image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isAnimatedWebP(source: BufferedSource): Boolean {
|
||||
return isWebP(source) &&
|
||||
source.rangeEquals(12, WEBP_HEADER_VPX8) &&
|
||||
source.request(17) &&
|
||||
(source.buffer[16] and 0b00000010) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains an HEIF image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isHeif(source: BufferedSource): Boolean {
|
||||
return source.rangeEquals(4, HEIF_HEADER_FTYP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains an animated HEIF image sequence. The [source] is not
|
||||
* consumed.
|
||||
*/
|
||||
fun DecodeUtils.isAnimatedHeif(source: BufferedSource): Boolean {
|
||||
return isHeif(source) &&
|
||||
(source.rangeEquals(8, HEIF_HEADER_MSF1) ||
|
||||
source.rangeEquals(8, HEIF_HEADER_HEVC) ||
|
||||
source.rangeEquals(8, HEIF_HEADER_HEVX))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@file:Suppress("SameParameterValue")
|
||||
|
||||
package coil.decode
|
||||
|
||||
import okio.Buffer
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
|
||||
/**
|
||||
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
|
||||
* below a threshold.
|
||||
*/
|
||||
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
|
||||
|
||||
// An intermediary buffer so we can read and alter the data before it's written to the destination.
|
||||
private val buffer = Buffer()
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
// Ensure our buffer has enough bytes to satisfy this read.
|
||||
request(byteCount)
|
||||
|
||||
// Short circuit if there are no bytes in the buffer.
|
||||
if (buffer.size == 0L) {
|
||||
return if (byteCount == 0L) 0L else -1L
|
||||
}
|
||||
|
||||
// Search through the buffer and rewrite any frame delays below the threshold.
|
||||
var bytesWritten = 0L
|
||||
while (true) {
|
||||
val index = indexOf(FRAME_DELAY_START_MARKER)
|
||||
if (index == -1L) break
|
||||
|
||||
// Write up until the end of the frame delay start marker.
|
||||
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
|
||||
|
||||
// Check for the end of the graphics control extension block.
|
||||
if (!request(5) || buffer[4] != 0.toByte()) continue
|
||||
|
||||
// Rewrite the frame delay if it is below the threshold.
|
||||
// The frame delay is stored as two unsigned bytes in reverse order
|
||||
// (i.e. the most significant bits are in the byte that's read second).
|
||||
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
|
||||
if (frameDelay < MINIMUM_FRAME_DELAY) {
|
||||
sink.writeByte(buffer[0].toInt())
|
||||
sink.writeByte(DEFAULT_FRAME_DELAY)
|
||||
sink.writeByte(0)
|
||||
buffer.skip(3)
|
||||
}
|
||||
}
|
||||
|
||||
// Write anything left in the source.
|
||||
if (bytesWritten < byteCount) {
|
||||
bytesWritten += write(sink, byteCount - bytesWritten)
|
||||
}
|
||||
return if (bytesWritten == 0L) -1 else bytesWritten
|
||||
}
|
||||
|
||||
private fun indexOf(bytes: ByteString): Long {
|
||||
var index = -1L
|
||||
while (true) {
|
||||
index = buffer.indexOf(bytes[0], index + 1)
|
||||
if (index == -1L) break
|
||||
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
private fun write(sink: Buffer, byteCount: Long): Long {
|
||||
return buffer.read(sink, byteCount).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun request(byteCount: Long): Boolean {
|
||||
if (buffer.size >= byteCount) return true
|
||||
val toRead = byteCount - buffer.size
|
||||
return super.read(buffer, toRead) == toRead
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
// See: "Graphics Control Extension"
|
||||
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
|
||||
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
|
||||
private const val MINIMUM_FRAME_DELAY = 2
|
||||
private const val DEFAULT_FRAME_DELAY = 10
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package com.hippo
|
||||
|
||||
import java.io.FileDescriptor
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object Native {
|
||||
fun initialize() {
|
||||
@@ -27,10 +26,4 @@ object Native {
|
||||
|
||||
@JvmStatic
|
||||
external fun getFd(fd: FileDescriptor?): Int
|
||||
|
||||
@JvmStatic
|
||||
external fun mapFd(fd: Int, capability: Long): ByteBuffer?
|
||||
|
||||
@JvmStatic
|
||||
external fun unmapDirectByteBuffer(buffer: ByteBuffer)
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package com.hippo
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import com.hippo.image.Image
|
||||
import java.nio.ByteBuffer
|
||||
@@ -40,14 +41,15 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
pfd.close()
|
||||
}
|
||||
|
||||
fun getImageSource(index: Int): Image.ByteBufferSource? {
|
||||
fun getImageSource(index: Int): Image.CloseableSource? {
|
||||
val buffer = extractToByteBuffer(index)
|
||||
buffer ?: return null
|
||||
check(buffer.isDirect)
|
||||
return object : Image.ByteBufferSource {
|
||||
override fun getByteBuffer(): ByteBuffer {
|
||||
return buffer
|
||||
}
|
||||
Image.rewriteGifSource(buffer)
|
||||
val source = ImageDecoder.createSource(buffer)
|
||||
return object : Image.CloseableSource {
|
||||
override val source: ImageDecoder.Source
|
||||
get() = source
|
||||
|
||||
override fun close() {
|
||||
releaseByteBuffer(buffer)
|
||||
|
||||
@@ -26,9 +26,12 @@ import androidx.appcompat.app.AlertDialog
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
|
||||
class ListCheckBoxDialogBuilder constructor(
|
||||
context: Context, items: Array<CharSequence>,
|
||||
listener: OnItemClickListener?, checkText: String?, checked: Boolean
|
||||
class ListCheckBoxDialogBuilder(
|
||||
context: Context,
|
||||
items: List<CharSequence>,
|
||||
listener: (ListCheckBoxDialogBuilder?, AlertDialog?, Int) -> Unit,
|
||||
checkText: String?,
|
||||
checked: Boolean
|
||||
) : BaseDialogBuilder(
|
||||
context
|
||||
) {
|
||||
@@ -42,10 +45,6 @@ class ListCheckBoxDialogBuilder constructor(
|
||||
return mDialog as AlertDialog
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(builder: ListCheckBoxDialogBuilder?, dialog: AlertDialog?, position: Int)
|
||||
}
|
||||
|
||||
init {
|
||||
val view =
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null)
|
||||
@@ -55,7 +54,7 @@ class ListCheckBoxDialogBuilder constructor(
|
||||
listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)
|
||||
listView.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
listener?.onItemClick(this@ListCheckBoxDialogBuilder, mDialog, position)
|
||||
listener(this@ListCheckBoxDialogBuilder, mDialog, position)
|
||||
mDialog?.dismiss()
|
||||
}
|
||||
mCheckBox.text = checkText
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.hippo.compat
|
||||
|
||||
import com.hippo.ehviewer.spider.SpiderInfo
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import java.io.InputStream
|
||||
|
||||
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
||||
fun read(): String {
|
||||
return IOUtils.readAsciiLine(inputStream)
|
||||
}
|
||||
|
||||
fun readInt(): Int {
|
||||
return read().toInt()
|
||||
}
|
||||
|
||||
fun readLong(): Long {
|
||||
return read().toLong()
|
||||
}
|
||||
repeat(2) { read() } // We assert that only info v2
|
||||
val gid = readLong()
|
||||
val token = read()
|
||||
read()
|
||||
val previewPages = readInt()
|
||||
val previewPerPage = readInt()
|
||||
val pages = read().toInt()
|
||||
val pTokenMap = hashMapOf<Int, String>()
|
||||
val info = SpiderInfo(gid, pages, pTokenMap, 0, token, previewPages, previewPerPage)
|
||||
runCatching {
|
||||
val line = read()
|
||||
val pos = line.indexOf(" ")
|
||||
if (pos > 0) {
|
||||
val index = line.substring(0, pos).toInt()
|
||||
val pToken = line.substring(pos + 1)
|
||||
if (pToken.isNotEmpty()) {
|
||||
pTokenMap[index] = pToken
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.disk.DiskCache
|
||||
import com.hippo.Native
|
||||
import com.hippo.app.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
@@ -42,15 +41,12 @@ import com.hippo.ehviewer.client.EhRequestBuilder
|
||||
import com.hippo.ehviewer.client.EhSSLSocketFactory
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhX509TrustManager
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.parser.EventPaneParser
|
||||
import com.hippo.ehviewer.dao.buildMainDB
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.spider.SpiderDen
|
||||
import com.hippo.ehviewer.ui.CommonOperations
|
||||
import com.hippo.ehviewer.ui.EhActivity
|
||||
import com.hippo.util.BitmapUtils
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
@@ -61,13 +57,9 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import okhttp3.Cache
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
import java.net.Proxy
|
||||
import java.security.KeyStore
|
||||
import java.util.Arrays
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
@@ -107,8 +99,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
Settings.initialize()
|
||||
ReadableTime.initialize(this)
|
||||
AppConfig.initialize(this)
|
||||
SpiderDen.initialize(this)
|
||||
BitmapUtils.initialize(this)
|
||||
EhTagDatabase.update()
|
||||
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
|
||||
launchIO {
|
||||
@@ -154,14 +144,8 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
if (!Settings.getRequestNews()) {
|
||||
return
|
||||
}
|
||||
val store = ehCookieStore
|
||||
val eh = EhUrl.HOST_E.toHttpUrl()
|
||||
|
||||
if (store.contains(eh, EhCookieStore.KEY_IPD_MEMBER_ID) || store.contains(
|
||||
eh,
|
||||
EhCookieStore.KEY_IPD_PASS_HASH
|
||||
)
|
||||
) {
|
||||
if (ehCookieStore.hasSignedIn()) {
|
||||
val referer = EhUrl.REFERER_E
|
||||
val request = EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
@@ -268,6 +252,7 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
|
||||
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
crossfade(true)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@@ -290,29 +275,16 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
val okHttpClient by lazy {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(ehCookieStore)
|
||||
.cache(Cache(File(application.cacheDir, "http_cache"), 50L * 1024L * 1024L))
|
||||
.dns(EhDns)
|
||||
.proxySelector(ehProxySelector)
|
||||
|
||||
if (Settings.getDF()) {
|
||||
var trustManager: X509TrustManager
|
||||
try {
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(
|
||||
TrustManagerFactory.getDefaultAlgorithm()
|
||||
)
|
||||
trustManagerFactory.init(null as KeyStore?)
|
||||
val trustManagers = trustManagerFactory.trustManagers
|
||||
check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) {
|
||||
"Unexpected default trust managers:" + Arrays.toString(
|
||||
trustManagers
|
||||
)
|
||||
}
|
||||
trustManager = trustManagers[0] as X509TrustManager
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
trustManager = EhX509TrustManager
|
||||
}
|
||||
val factory =
|
||||
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())!!
|
||||
factory.init(null as KeyStore?)
|
||||
val manager = factory.trustManagers!!
|
||||
|
||||
val trustManager = manager.filterIsInstance<X509TrustManager>().first()
|
||||
builder.sslSocketFactory(EhSSLSocketFactory, trustManager)
|
||||
builder.proxy(Proxy.NO_PROXY)
|
||||
}
|
||||
@@ -328,12 +300,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val spiderInfoCache by lazy {
|
||||
DiskCache.Builder().directory(File(application.cacheDir, "spider_info"))
|
||||
.maxSizeBytes(20 * 1024 * 1024).build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val downloadManager by lazy { DownloadManager() }
|
||||
|
||||
|
||||
@@ -111,28 +111,9 @@ public class EhDB {
|
||||
return list;
|
||||
}
|
||||
|
||||
public static synchronized void moveDownloadInfo(int fromPosition, int toPosition) {
|
||||
if (fromPosition == toPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean reverse = fromPosition > toPosition;
|
||||
int offset = reverse ? toPosition : fromPosition;
|
||||
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
|
||||
|
||||
public static synchronized void updateDownloadInfo(List<DownloadInfo> downloadInfos) {
|
||||
DownloadsDao dao = db.downloadsDao();
|
||||
List<DownloadInfo> list = dao.list(offset, limit);
|
||||
|
||||
int step = reverse ? 1 : -1;
|
||||
int start = reverse ? limit - 1 : 0;
|
||||
int end = reverse ? 0 : limit - 1;
|
||||
long toTime = list.get(end).time;
|
||||
for (int i = end; reverse ? i < start : i > start; i += step) {
|
||||
list.get(i).time = list.get(i + step).time;
|
||||
}
|
||||
list.get(start).time = toTime;
|
||||
|
||||
dao.update(list);
|
||||
dao.update(downloadInfos);
|
||||
}
|
||||
|
||||
// Insert or update
|
||||
@@ -362,6 +343,21 @@ public class EhDB {
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfoNonRefresh(GalleryInfo info) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
var i = dao.load(info.getGid());
|
||||
if (null != i) {
|
||||
HistoryInfo historyInfo;
|
||||
if (info instanceof HistoryInfo) {
|
||||
historyInfo = (HistoryInfo) info;
|
||||
} else {
|
||||
historyInfo = new HistoryInfo(info);
|
||||
historyInfo.time = i.time;
|
||||
}
|
||||
dao.update(historyInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
for (HistoryInfo info : historyInfoList) {
|
||||
|
||||
@@ -45,6 +45,9 @@ public class Settings {
|
||||
public static final String KEY_THEME = "theme";
|
||||
|
||||
public static final String KEY_ACCOUNT = "account";
|
||||
public static final String KEY_IMAGE_LIMITS = "image_limits";
|
||||
public static final String KEY_U_CONFIG = "uconfig";
|
||||
public static final String KEY_MY_TAGS = "mytags";
|
||||
public static final String KEY_BLACK_DARK_THEME = "black_dark_theme";
|
||||
public static final int THEME_SYSTEM = -1;
|
||||
public static final String KEY_GALLERY_SITE = "gallery_site";
|
||||
@@ -52,11 +55,24 @@ public class Settings {
|
||||
public static final String KEY_DETAIL_SIZE = "detail_size";
|
||||
public static final String KEY_THUMB_SIZE = "thumb_size_";
|
||||
public static final String KEY_THUMB_RESOLUTION = "thumb_resolution";
|
||||
public static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
|
||||
public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations";
|
||||
public static final String KEY_TAG_TRANSLATIONS_SOURCE = "tag_translations_source";
|
||||
public static final String KEY_DEFAULT_CATEGORIES = "default_categories";
|
||||
public static final int DEFAULT_DEFAULT_CATEGORIES = EhConfig.ALL_CATEGORY;
|
||||
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
|
||||
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
|
||||
public static final String KEY_REQUEST_NEWS = "request_news";
|
||||
public static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
|
||||
public static final String[] SIGN_IN_REQUIRED = {
|
||||
KEY_IMAGE_LIMITS,
|
||||
KEY_GALLERY_SITE,
|
||||
KEY_U_CONFIG,
|
||||
KEY_MY_TAGS,
|
||||
KEY_SHOW_JPN_TITLE,
|
||||
KEY_REQUEST_NEWS,
|
||||
KEY_HIDE_HV_EVENTS
|
||||
};
|
||||
/********************
|
||||
****** Privacy and Security
|
||||
********************/
|
||||
@@ -87,7 +103,6 @@ public class Settings {
|
||||
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
|
||||
public static final String KEY_BYPASS_VPN = "bypass_vpn";
|
||||
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
|
||||
private static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
|
||||
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
|
||||
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
|
||||
private static final boolean DEFAULT_SHOW_COMMENTS = true;
|
||||
@@ -110,7 +125,6 @@ public class Settings {
|
||||
private static final int DEFAULT_DETAIL_SIZE = 0;
|
||||
private static final int DEFAULT_THUMB_SIZE = 120;
|
||||
private static final int DEFAULT_THUMB_RESOLUTION = 0;
|
||||
private static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
|
||||
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
|
||||
private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages";
|
||||
private static final boolean DEFAULT_SHOW_GALLERY_PAGES = false;
|
||||
@@ -191,7 +205,6 @@ public class Settings {
|
||||
private static final int DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0;
|
||||
private static final String KEY_DOWNLOAD_DELAY = "download_delay";
|
||||
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
|
||||
private static final String KEY_REQUEST_NEWS = "request_news";
|
||||
private static final boolean DEFAULT_REQUEST_NEWS = true;
|
||||
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
|
||||
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
|
||||
@@ -675,7 +688,7 @@ public class Settings {
|
||||
putBoolean(KEY_DOMAIN_FRONTING, value);
|
||||
}
|
||||
|
||||
public static boolean getBypassVPN() {
|
||||
public static boolean getBypassVpn() {
|
||||
return getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.client;
|
||||
|
||||
public class EhCacheKeyFactory {
|
||||
|
||||
public static String getThumbKey(long gid) {
|
||||
return "preview:large:" + gid + ":" + 0; // "thumb:" + gid;
|
||||
}
|
||||
|
||||
public static String getNormalPreviewKey(long gid, int index) {
|
||||
return "preview:normal:" + gid + ":" + index;
|
||||
}
|
||||
|
||||
public static String getLargePreviewKey(long gid, int index) {
|
||||
return "preview:large:" + gid + ":" + index;
|
||||
}
|
||||
|
||||
public static String getLargePreviewSetKey(long gid, int index) {
|
||||
return "large_preview_set:" + gid + ":" + index;
|
||||
}
|
||||
|
||||
public static String getImageKey(long gid, int index) {
|
||||
return "image:" + gid + ":" + index;
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -13,6 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client.exception
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
class Image509Exception : EhException("509")
|
||||
object EhCacheKeyFactory {
|
||||
fun getImageKey(gid: Long, index: Int): String {
|
||||
return "image:$gid:$index"
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,12 @@ object EhCookieStore : CookieJar {
|
||||
|
||||
fun hasSignedIn(): Boolean {
|
||||
val url = EhUrl.HOST_E.toHttpUrl()
|
||||
return contains(url, KEY_IPD_MEMBER_ID) &&
|
||||
contains(url, KEY_IPD_PASS_HASH)
|
||||
return contains(url, KEY_IPB_MEMBER_ID) &&
|
||||
contains(url, KEY_IPB_PASS_HASH)
|
||||
}
|
||||
|
||||
const val KEY_IPD_MEMBER_ID = "ipb_member_id"
|
||||
const val KEY_IPD_PASS_HASH = "ipb_pass_hash"
|
||||
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
||||
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
||||
const val KEY_IGNEOUS = "igneous"
|
||||
private val sTipsCookie: Cookie = Cookie.Builder()
|
||||
.name(EhConfig.KEY_CONTENT_WARNING)
|
||||
|
||||
@@ -41,6 +41,7 @@ import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageApiParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.client.parser.ProfileParser
|
||||
import com.hippo.ehviewer.client.parser.RateGalleryParser
|
||||
import com.hippo.ehviewer.client.parser.SignInParser
|
||||
@@ -50,6 +51,8 @@ import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||
import com.hippo.network.StatusCodeException
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.yorozuya.AssertUtils
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
@@ -85,7 +88,7 @@ object EhEngine {
|
||||
}
|
||||
|
||||
// Check sad panda(without panda)
|
||||
if (headers != null && "text/html; charset=UTF-8" == headers["Content-Type"] && "0" == headers["Content-Length"]) {
|
||||
if (headers != null && "text/html; charset=UTF-8" == headers["Content-Type"] && "0" == headers["Content-Length"] && EhUtils.isExHentai) {
|
||||
throw EhException("Sad Panda\n(without panda)")
|
||||
}
|
||||
|
||||
@@ -99,11 +102,14 @@ object EhEngine {
|
||||
throw EhException(error)
|
||||
}
|
||||
}
|
||||
if (code >= 400) {
|
||||
throw StatusCodeException(code)
|
||||
}
|
||||
if (e is ParseException) {
|
||||
if (body != null && !body.contains("<")) {
|
||||
throw EhException(body)
|
||||
} else if (TextUtils.isEmpty(body)) {
|
||||
if (TextUtils.isEmpty(body)) {
|
||||
throw EhException(GetText.getString(R.string.error_empty_html))
|
||||
} else if (body != null && !body.contains("<")) {
|
||||
throw EhException(body)
|
||||
} else {
|
||||
if (Settings.getSaveParseErrorBody()) {
|
||||
AppConfig.saveParseErrorBody(e as ParseException?)
|
||||
@@ -111,9 +117,6 @@ object EhEngine {
|
||||
throw EhException(GetText.getString(R.string.error_parse_error))
|
||||
}
|
||||
}
|
||||
if (code >= 400) {
|
||||
throw StatusCodeException(code)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
@@ -323,7 +326,7 @@ object EhEngine {
|
||||
if (html != null) {
|
||||
application.showEventPane(html)
|
||||
}
|
||||
return GalleryDetailParser.parse(body)
|
||||
return GalleryDetailParser.parse(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
@@ -348,8 +351,8 @@ object EhEngine {
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
return Pair.create(
|
||||
GalleryDetailParser.parsePreviewSet(body),
|
||||
GalleryDetailParser.parsePreviewPages(body)
|
||||
GalleryDetailParser.parsePreviewSet(body!!),
|
||||
GalleryDetailParser.parsePreviewPages(body!!)
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -760,7 +763,7 @@ object EhEngine {
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getFunds(): ArchiveParser.Funds? {
|
||||
suspend fun getFunds(): HomeParser.Funds {
|
||||
val url = EhUrl.URL_FUNDS
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, null).build()
|
||||
@@ -773,7 +776,69 @@ object EhEngine {
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
return ArchiveParser.parseFunds(body!!)
|
||||
return HomeParser.parseFunds(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
private suspend fun getImageLimitsInternal(): HomeParser.Limits {
|
||||
val url = EhUrl.URL_HOME
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, null).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
var body: String? = null
|
||||
var headers: Headers? = null
|
||||
var code = -1
|
||||
try {
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
return HomeParser.parse(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getImageLimits(): HomeParser.Result = coroutineScope {
|
||||
val limitsDeferred = async {
|
||||
getImageLimitsInternal()
|
||||
}
|
||||
val fundsDeferred = async {
|
||||
getFunds()
|
||||
}
|
||||
HomeParser.Result(limitsDeferred.await(), fundsDeferred.await())
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun resetImageLimits(): HomeParser.Limits? {
|
||||
val builder = FormBody.Builder()
|
||||
.add("act", "limits")
|
||||
.add("reset", "Reset Limit")
|
||||
val url = EhUrl.URL_HOME
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, null)
|
||||
.post(builder.build())
|
||||
.build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
var body: String? = null
|
||||
var headers: Headers? = null
|
||||
var code = -1
|
||||
try {
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
return HomeParser.parseResetLimits(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
@@ -830,8 +895,7 @@ object EhEngine {
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getUConfig(): Void? {
|
||||
val url = EhUrl.uConfigUrl
|
||||
suspend fun getUConfig(url: String = EhUrl.uConfigUrl): Void? {
|
||||
Log.d(TAG, url)
|
||||
var request = EhRequestBuilder(url, null).build()
|
||||
var call = okHttpClient.newCall(request)
|
||||
@@ -1021,7 +1085,7 @@ object EhEngine {
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Throwable::class)
|
||||
fun getGalleryPage(
|
||||
suspend fun getGalleryPage(
|
||||
url: String?,
|
||||
gid: Long,
|
||||
token: String?
|
||||
@@ -1035,7 +1099,7 @@ object EhEngine {
|
||||
var headers: Headers? = null
|
||||
var code = -1
|
||||
try {
|
||||
call.execute().use { response ->
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
@@ -1050,7 +1114,7 @@ object EhEngine {
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Throwable::class)
|
||||
fun getGalleryPageApi(
|
||||
suspend fun getGalleryPageApi(
|
||||
gid: Long,
|
||||
index: Int,
|
||||
pToken: String?,
|
||||
@@ -1080,7 +1144,7 @@ object EhEngine {
|
||||
var headers: Headers? = null
|
||||
var code = -1
|
||||
try {
|
||||
call.execute().use { response ->
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.client;
|
||||
|
||||
import com.hippo.okhttp.ChromeRequestBuilder;
|
||||
|
||||
public class EhRequestBuilder extends ChromeRequestBuilder {
|
||||
|
||||
public EhRequestBuilder(String url, String referer) {
|
||||
this(url, referer, null);
|
||||
}
|
||||
|
||||
public EhRequestBuilder(String url, String referer, String origin) {
|
||||
super(url);
|
||||
if (referer != null) {
|
||||
addHeader("Referer", referer);
|
||||
}
|
||||
if (origin != null) {
|
||||
addHeader("Origin", origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -13,14 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
package com.hippo.util;
|
||||
import com.hippo.okhttp.ChromeRequestBuilder
|
||||
|
||||
public class MutableBoolean {
|
||||
|
||||
public boolean value;
|
||||
|
||||
public MutableBoolean(boolean value) {
|
||||
this.value = value;
|
||||
class EhRequestBuilder @JvmOverloads constructor(
|
||||
url: String,
|
||||
referer: String?,
|
||||
origin: String? = null
|
||||
) : ChromeRequestBuilder(url) {
|
||||
init {
|
||||
referer?.let { addHeader("Referer", it) }
|
||||
origin?.let { addHeader("Origin", it) }
|
||||
}
|
||||
}
|
||||
@@ -43,35 +43,62 @@ import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
private typealias TagGroup = Map<String, String>
|
||||
private typealias TagGroups = Map<String, TagGroup>
|
||||
|
||||
object EhTagDatabase {
|
||||
private lateinit var tags: JSONObject
|
||||
private lateinit var tagList: ArrayList<Pair<String, String>>
|
||||
private const val NAMESPACE_PREFIX = "n"
|
||||
private lateinit var tagGroups: TagGroups
|
||||
|
||||
fun isInitialized(): Boolean {
|
||||
return this::tags.isInitialized && this::tagList.isInitialized
|
||||
return this::tagGroups.isInitialized
|
||||
}
|
||||
|
||||
private fun JSONObject.toTagGroups(): TagGroups =
|
||||
keys().asSequence().associateWith { getJSONObject(it).toTagGroup() }
|
||||
|
||||
private fun JSONObject.toTagGroup(): TagGroup =
|
||||
keys().asSequence().associateWith { getString(it) }
|
||||
|
||||
private fun updateData(source: BufferedSource) {
|
||||
try {
|
||||
val tmpTags = JSONObject(source.readString(StandardCharsets.UTF_8))
|
||||
val tmpTagList = arrayListOf<Pair<String, String>>()
|
||||
tmpTags.keys().forEachRemaining { prefix: String ->
|
||||
val values = tmpTags.optJSONObject(prefix)
|
||||
values?.let {
|
||||
it.keys().forEachRemaining { tag: String ->
|
||||
tmpTagList.add(Pair("$prefix:$tag", values.optString(tag)))
|
||||
}
|
||||
}
|
||||
}
|
||||
tags = tmpTags
|
||||
tagList = tmpTagList
|
||||
tagGroups = JSONObject(source.readString(StandardCharsets.UTF_8)).toTagGroups()
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun getTranslation(prefix: String?, tag: String?): String? {
|
||||
return tags.optJSONObject(prefix)?.optString(tag)?.trim()?.takeIf { it.isNotEmpty() }
|
||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
|
||||
return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun internalSuggestFlow(
|
||||
tags: Map<String, String>,
|
||||
keyword: String,
|
||||
translate: Boolean,
|
||||
exactly: Boolean
|
||||
): Flow<Pair<String?, String>> = flow {
|
||||
if (exactly) {
|
||||
tags[keyword]?.let {
|
||||
emit(Pair(it.takeIf { translate }, keyword))
|
||||
}
|
||||
} else {
|
||||
if (translate) {
|
||||
tags.forEach { (tag, hint) ->
|
||||
if (tag != keyword &&
|
||||
(tag.containsIgnoreSpace(keyword) || hint.containsIgnoreSpace(keyword))
|
||||
) {
|
||||
emit(Pair(hint, tag))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tags.keys.forEach { tag ->
|
||||
if (tag != keyword && tag.containsIgnoreSpace(keyword)) {
|
||||
emit(Pair(null, tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Construct a cold flow for tag database suggestions */
|
||||
@@ -80,32 +107,32 @@ object EhTagDatabase {
|
||||
translate: Boolean,
|
||||
exactly: Boolean = false
|
||||
): Flow<Pair<String?, String>> = flow {
|
||||
tagList.forEach {
|
||||
val tag = it.first
|
||||
val hint = if (translate) it.second else null
|
||||
val index = tag.indexOf(':')
|
||||
if (tag.substring(index + 1).trim() == keyword) {
|
||||
if (exactly)
|
||||
emit(Pair(hint, tag))
|
||||
return@forEach
|
||||
val keywordPrefix = keyword.substringBefore(':')
|
||||
val keywordTag = keyword.drop(keywordPrefix.length + 1)
|
||||
val prefix = namespaceToPrefix(keywordPrefix) ?: keywordPrefix
|
||||
val tags = tagGroups[prefix.takeIf { keywordTag.isNotEmpty() && it != NAMESPACE_PREFIX }]
|
||||
tags?.let {
|
||||
internalSuggestFlow(it, keywordTag, translate, exactly).collect { (hint, tag) ->
|
||||
emit(Pair(hint, "$prefix:$tag"))
|
||||
}
|
||||
if (exactly)
|
||||
return@forEach
|
||||
val keywordMatches: Boolean =
|
||||
if (index == -1 || index >= tag.length - 1 || keyword.length > 2) {
|
||||
containsIgnoreSpace(tag, keyword)
|
||||
} else {
|
||||
containsIgnoreSpace(tag.substring(index + 1), keyword)
|
||||
} ?: tagGroups.forEach { (prefix, tags) ->
|
||||
if (prefix != NAMESPACE_PREFIX) {
|
||||
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
|
||||
emit(Pair(hint, "$prefix:$tag"))
|
||||
}
|
||||
} else {
|
||||
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
|
||||
emit(Pair(hint, "$tag:"))
|
||||
}
|
||||
if (keywordMatches || containsIgnoreSpace(hint, keyword)) {
|
||||
emit(Pair(hint, tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun containsIgnoreSpace(text: String?, key: String): Boolean {
|
||||
return text != null && text.replace(" ", "").contains(key.replace(" ", ""))
|
||||
}
|
||||
private fun String.removeSpace(): String = replace(" ", "")
|
||||
|
||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
||||
removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||
|
||||
|
||||
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
|
||||
it["artist"] = "a"
|
||||
|
||||
@@ -44,7 +44,8 @@ object EhUrl {
|
||||
const val URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login"
|
||||
const val URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00"
|
||||
const val URL_FORUMS = "https://forums.e-hentai.org/"
|
||||
const val URL_FUNDS = "https://e-hentai.org/exchange.php?t=gp"
|
||||
const val URL_FUNDS = HOST_E + "exchange.php?t=gp"
|
||||
const val URL_HOME = HOST_E + "home.php"
|
||||
const val REFERER_EX = "https://$DOMAIN_EX"
|
||||
const val ORIGIN_EX = REFERER_EX
|
||||
const val REFERER_E = "https://$DOMAIN_E"
|
||||
|
||||
@@ -66,6 +66,9 @@ object EhUtils {
|
||||
)
|
||||
private val CATEGORY_STRINGS = CATEGORY_VALUES.entries.map { (k, v) -> v to k }
|
||||
|
||||
val isExHentai: Boolean
|
||||
get() = Settings.getGallerySite() == EhUrl.SITE_EX
|
||||
|
||||
@JvmStatic
|
||||
fun getCategory(type: String?): Int {
|
||||
for (entry in CATEGORY_STRINGS) {
|
||||
@@ -102,6 +105,7 @@ object EhUtils {
|
||||
Settings.putDisplayName(null)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
Settings.putNeedSignIn(true)
|
||||
Settings.putSelectSite(true)
|
||||
}
|
||||
|
||||
fun needSignedIn(): Boolean {
|
||||
@@ -109,12 +113,12 @@ object EhUtils {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getSuitableTitle(gi: GalleryInfo): String? {
|
||||
fun getSuitableTitle(gi: GalleryInfo): String {
|
||||
return if (Settings.getShowJpnTitle()) {
|
||||
if (TextUtils.isEmpty(gi.titleJpn)) gi.title else gi.titleJpn
|
||||
} else {
|
||||
if (TextUtils.isEmpty(gi.title)) gi.titleJpn else gi.title
|
||||
}
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
fun extractTitle(fullTitle: String?): String? {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
object EhX509TrustManager : X509TrustManager {
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
}
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate?> {
|
||||
return arrayOfNulls(0)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANGS
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -70,6 +72,33 @@ class GalleryDetail(
|
||||
@JvmField
|
||||
var previewPages: Int = 0,
|
||||
|
||||
@IgnoredOnParcel
|
||||
@JvmField
|
||||
var previewSet: PreviewSet? = null,
|
||||
) : AbstractGalleryInfo by galleryInfo, GalleryInfo
|
||||
) : AbstractGalleryInfo by galleryInfo, GalleryInfo {
|
||||
override fun generateSLang() {
|
||||
val index = LANGUAGES.indexOf(language)
|
||||
if (index != -1) {
|
||||
simpleLanguage = S_LANGS[index]
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LANGUAGES = arrayOf(
|
||||
"English",
|
||||
"Chinese",
|
||||
"Spanish",
|
||||
"Korean",
|
||||
"Russian",
|
||||
"French",
|
||||
"Portuguese",
|
||||
"Thai",
|
||||
"German",
|
||||
"Italian",
|
||||
"Vietnamese",
|
||||
"Polish",
|
||||
"Hungarian",
|
||||
"Dutch"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,6 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class GalleryPreview(
|
||||
@JvmField
|
||||
var imageKey: String? = null,
|
||||
@JvmField
|
||||
var imageUrl: String? = null,
|
||||
@JvmField
|
||||
@@ -40,6 +38,6 @@ class GalleryPreview(
|
||||
) : Parcelable {
|
||||
fun load(view: LoadImageView) {
|
||||
view.setClip(offsetX, offsetY, clipWidth, clipHeight)
|
||||
view.load(imageKey!!, imageUrl!!)
|
||||
view.load(imageUrl!!)
|
||||
}
|
||||
}
|
||||
@@ -23,16 +23,4 @@ class GalleryTagGroup(
|
||||
private val mTagList: ArrayList<String> = arrayListOf(),
|
||||
@JvmField
|
||||
var groupName: String? = null
|
||||
) : Parcelable {
|
||||
fun addTag(tag: String) {
|
||||
mTagList.add(tag)
|
||||
}
|
||||
|
||||
fun size(): Int {
|
||||
return mTagList.size
|
||||
}
|
||||
|
||||
fun getTagAt(position: Int): String {
|
||||
return mTagList[position]
|
||||
}
|
||||
}
|
||||
) : Parcelable, MutableList<String> by mTagList
|
||||
@@ -15,12 +15,9 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory
|
||||
import com.hippo.widget.LoadImageView
|
||||
import com.hippo.yorozuya.collect.IntList
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class LargePreviewSet(
|
||||
private val mPositionList: IntList = IntList(),
|
||||
private val mImageUrlList: ArrayList<String> = ArrayList(),
|
||||
@@ -48,7 +45,6 @@ class LargePreviewSet(
|
||||
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
|
||||
val galleryPreview = GalleryPreview()
|
||||
galleryPreview.position = mPositionList[index]
|
||||
galleryPreview.imageKey = EhCacheKeyFactory.getLargePreviewKey(gid, galleryPreview.position)
|
||||
galleryPreview.imageUrl = mImageUrlList[index]
|
||||
galleryPreview.pageUrl = mPageUrlList[index]
|
||||
return galleryPreview
|
||||
@@ -57,7 +53,6 @@ class LargePreviewSet(
|
||||
override fun load(view: LoadImageView, gid: Long, index: Int) {
|
||||
view.resetClip()
|
||||
view.load(
|
||||
EhCacheKeyFactory.getLargePreviewKey(gid, mPositionList[index]),
|
||||
mImageUrlList[index]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -283,13 +283,6 @@ data class ListUrlBuilder(
|
||||
if (this.category != EhUtils.NONE) {
|
||||
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
|
||||
}
|
||||
// Search key
|
||||
mKeyword?.run {
|
||||
val keyword = trim { it <= ' ' }
|
||||
if (keyword.isNotEmpty()) {
|
||||
ub.addQuery("f_search", encodeUTF8(this))
|
||||
}
|
||||
}
|
||||
mSHash?.let {
|
||||
ub.addQuery("f_shash", it)
|
||||
}
|
||||
@@ -302,6 +295,19 @@ data class ListUrlBuilder(
|
||||
mNext?.let {
|
||||
ub.addQuery("next", it)
|
||||
}
|
||||
// Search key
|
||||
// the settings of ub:UrlBuilder may be overwritten by following Advance search
|
||||
StringUtils.split(mKeyword, '|')?.forEachIndexed { idx, keyword ->
|
||||
val keyword = keyword.trim { it <= ' ' }
|
||||
when (idx) {
|
||||
0 -> keyword.takeIf { it.isNotEmpty() }?.let { ub.addQuery("f_search", encodeUTF8(it)) }
|
||||
else -> keyword.indexOf(':').takeIf { it >= 0 }?.run {
|
||||
val key = keyword.substring(0, this).trim { it <= ' ' }
|
||||
val value = keyword.substring(this + 1).trim { it <= ' ' }
|
||||
ub.addQuery(key, encodeUTF8(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Advance search
|
||||
if (advanceSearch != -1) {
|
||||
ub.addQuery("advsearch", "1")
|
||||
|
||||
@@ -17,12 +17,9 @@ package com.hippo.ehviewer.client.data
|
||||
|
||||
import com.hippo.widget.LoadImageView
|
||||
import com.hippo.yorozuya.collect.IntList
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class NormalPreviewSet(
|
||||
private var mPositionList: IntList = IntList(),
|
||||
private var mImageKeyList: ArrayList<String> = ArrayList(),
|
||||
private var mImageUrlList: ArrayList<String> = ArrayList(),
|
||||
private var mOffsetXList: IntList = IntList(),
|
||||
private var mOffsetYList: IntList = IntList(),
|
||||
@@ -31,21 +28,11 @@ class NormalPreviewSet(
|
||||
private var mPageUrlList: ArrayList<String> = ArrayList()
|
||||
) : PreviewSet() {
|
||||
|
||||
private fun getImageKey(imageUrl: String): String {
|
||||
val index = imageUrl.indexOf('/')
|
||||
return if (index >= 0) {
|
||||
imageUrl.substring(index + 1)
|
||||
} else {
|
||||
imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
fun addItem(
|
||||
position: Int, imageUrl: String, xOffset: Int, yOffset: Int, width: Int,
|
||||
height: Int, pageUrl: String
|
||||
) {
|
||||
mPositionList.add(position)
|
||||
mImageKeyList.add(getImageKey(imageUrl))
|
||||
mImageUrlList.add(imageUrl)
|
||||
mOffsetXList.add(xOffset)
|
||||
mOffsetYList.add(yOffset)
|
||||
@@ -55,7 +42,7 @@ class NormalPreviewSet(
|
||||
}
|
||||
|
||||
override fun size(): Int {
|
||||
return mPositionList.size()
|
||||
return mPositionList.size
|
||||
}
|
||||
|
||||
override fun getPosition(index: Int): Int {
|
||||
@@ -69,7 +56,6 @@ class NormalPreviewSet(
|
||||
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
|
||||
val galleryPreview = GalleryPreview()
|
||||
galleryPreview.position = mPositionList[index]
|
||||
galleryPreview.imageKey = mImageKeyList[index]
|
||||
galleryPreview.imageUrl = mImageUrlList[index]
|
||||
galleryPreview.pageUrl = mPageUrlList[index]
|
||||
galleryPreview.offsetX = mOffsetXList[index]
|
||||
@@ -84,6 +70,6 @@ class NormalPreviewSet(
|
||||
mOffsetXList[index], mOffsetYList[index],
|
||||
mClipWidthList[index], mClipHeightList[index]
|
||||
)
|
||||
view.load(mImageKeyList[index], mImageUrlList[index])
|
||||
view.load(mImageUrlList[index])
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,9 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.hippo.widget.LoadImageView
|
||||
|
||||
abstract class PreviewSet : Parcelable {
|
||||
abstract class PreviewSet {
|
||||
abstract fun size(): Int
|
||||
abstract fun getPosition(index: Int): Int
|
||||
abstract fun getPageUrlAt(index: Int): String
|
||||
|
||||
@@ -15,113 +15,71 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.function.IntFunction
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object ArchiveParser {
|
||||
private val PATTERN_ARCHIVE_URL =
|
||||
Pattern.compile("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
|
||||
Regex("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
|
||||
private val PATTERN_CURRENT_FUNDS =
|
||||
Pattern.compile("<p>([\\d,]+) GP \\[[^]]*] ([\\d,]+) Credits \\[[^]]*]</p>")
|
||||
private val PATTERN_FUNDS =
|
||||
Pattern.compile("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", Pattern.DOTALL)
|
||||
Regex("<p>([\\d,]+) GP \\[[^]]*] ([\\d,]+) Credits \\[[^]]*]</p>")
|
||||
private val PATTERN_HATH_FORM =
|
||||
Pattern.compile("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
|
||||
Regex("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
|
||||
private val PATTERN_HATH_ARCHIVE =
|
||||
Pattern.compile("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
|
||||
Regex("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
|
||||
private val PATTERN_NEED_HATH_CLIENT =
|
||||
Pattern.compile("You must have a H@H client assigned to your account to use this feature\\.")
|
||||
Regex("You must have a H@H client assigned to your account to use this feature\\.")
|
||||
|
||||
fun parse(body: String): Result? {
|
||||
var m = PATTERN_HATH_FORM.matcher(body)
|
||||
if (!m.find()) {
|
||||
return null
|
||||
}
|
||||
val paramOr = m.group(1)
|
||||
val m = PATTERN_HATH_FORM.find(body) ?: return null
|
||||
val paramOr = m.groupValues[1]
|
||||
val archiveList = ArrayList<Archive>()
|
||||
val d = Jsoup.parse(body)
|
||||
val es = d.select("#db>div>div")
|
||||
for (e in es) {
|
||||
if (e.childrenSize() > 0 && !e.attr("style").contains("color:#CCCCCC")) {
|
||||
try {
|
||||
val res = e.selectFirst("form>input")!!.attr("value")
|
||||
val name = e.selectFirst("form>div>input")!!.attr("value")
|
||||
val size = e.selectFirst("p>strong")!!.text()
|
||||
val cost = e.selectFirst("div>strong")!!.text().replace(",", "")
|
||||
val item = Archive(res, name, size, cost, false)
|
||||
archiveList.add(item)
|
||||
} catch (ex: NullPointerException) {
|
||||
ex.printStackTrace()
|
||||
Jsoup.parse(body).select("#db>div>div").forEach { element ->
|
||||
if (element.childrenSize() > 0 && !element.attr("style").contains("color:#CCCCCC")) {
|
||||
runCatching {
|
||||
val res = element.selectFirst("form>input")!!.attr("value")
|
||||
val name = element.selectFirst("form>div>input")!!.attr("value")
|
||||
val size = element.selectFirst("p>strong")!!.text()
|
||||
val cost = element.selectFirst("div>strong")!!.text().replace(",", "")
|
||||
Archive(res, name, size, cost, false)
|
||||
}.onSuccess {
|
||||
archiveList.add(it)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
m = PATTERN_HATH_ARCHIVE.matcher(body)
|
||||
while (m.find()) {
|
||||
val res = ParserUtils.trim(m.group(1))
|
||||
val name = ParserUtils.trim(m.group(2))
|
||||
val size = ParserUtils.trim(m.group(3))
|
||||
val cost = ParserUtils.trim(m.group(4))
|
||||
PATTERN_HATH_ARCHIVE.findAll(body).forEach { matchResult ->
|
||||
val (res, name, size, cost) = matchResult.groupValues.slice(1..4)
|
||||
.map { ParserUtils.trim(it) }
|
||||
val item = Archive(res, name, size, cost, true)
|
||||
archiveList.add(item)
|
||||
}
|
||||
var funds: Funds? = null
|
||||
m = PATTERN_CURRENT_FUNDS.matcher(body)
|
||||
if (m.find()) {
|
||||
val fundsGP = ParserUtils.parseInt(m.group(1), 0)
|
||||
val fundsC = ParserUtils.parseInt(m.group(2), 0)
|
||||
funds = Funds(fundsGP, fundsC)
|
||||
val result = Result(paramOr, archiveList, null)
|
||||
PATTERN_CURRENT_FUNDS.find(body)?.groupValues?.run {
|
||||
val fundsGP = ParserUtils.parseInt(get(1), 0)
|
||||
val fundsC = ParserUtils.parseInt(get(2), 0)
|
||||
val funds = HomeParser.Funds(fundsGP, fundsC)
|
||||
result.funds = funds
|
||||
}
|
||||
return Result(paramOr, archiveList, funds)
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(NoHAtHClientException::class)
|
||||
fun parseArchiveUrl(body: String): String? {
|
||||
val m = PATTERN_NEED_HATH_CLIENT.matcher(body)
|
||||
if (m.find()) {
|
||||
throw NoHAtHClientException("No H@H client")
|
||||
}
|
||||
val d = Jsoup.parse(body)
|
||||
val a = d.selectFirst("#continue>a[href]")
|
||||
return if (a != null) {
|
||||
a.attr("href") + "?start=1"
|
||||
} else null
|
||||
|
||||
if (PATTERN_NEED_HATH_CLIENT.containsMatchIn(body)) throw NoHAtHClientException("No H@H client")
|
||||
return Jsoup.parse(body).selectFirst("#continue>a[href]")
|
||||
?.let { it.attr("href") + "?start=1" }
|
||||
// TODO: Check more errors
|
||||
}
|
||||
|
||||
fun parseFunds(body: String): Funds? {
|
||||
val m = PATTERN_FUNDS.matcher(body)
|
||||
return if (m.find()) {
|
||||
val fundsC = ParserUtils.parseInt(m.group(1), 0)
|
||||
val fundsGP = ParserUtils.parseInt(m.group(2), 0) * 1000
|
||||
return Funds(fundsGP, fundsC)
|
||||
} else null
|
||||
}
|
||||
|
||||
class Archive(
|
||||
val res: String,
|
||||
val name: String,
|
||||
val size: String,
|
||||
val cost: String,
|
||||
val isHAtH: Boolean
|
||||
) {
|
||||
fun format(getString: IntFunction<String?>): String {
|
||||
return if (isHAtH) {
|
||||
val costStr = if (cost == "Free") getString.apply(R.string.archive_free) else cost
|
||||
"[H@H] $name [$size] [$costStr]"
|
||||
} else {
|
||||
val nameStr =
|
||||
getString.apply(if (res == "org") R.string.archive_original else R.string.archive_resample)
|
||||
val costStr = if (cost == "Free!") getString.apply(R.string.archive_free) else cost
|
||||
"$nameStr [$size] [$costStr]"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
class Funds(var fundsGP: Int, var fundsC: Int)
|
||||
|
||||
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: Funds?)
|
||||
}
|
||||
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: HomeParser.Funds?)
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
|
||||
import com.hippo.ehviewer.client.EhUrl;
|
||||
import com.hippo.ehviewer.client.exception.ParseException;
|
||||
import com.hippo.ehviewer.client.exception.EhException;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
@@ -26,7 +26,7 @@ import org.jsoup.nodes.Element;
|
||||
|
||||
public class ForumsParser {
|
||||
|
||||
public static String parse(String body) throws ParseException {
|
||||
public static String parse(String body) throws EhException {
|
||||
try {
|
||||
Document d = Jsoup.parse(body, EhUrl.URL_FORUMS);
|
||||
Element userlinks = d.getElementById("userlinks");
|
||||
@@ -34,7 +34,7 @@ public class ForumsParser {
|
||||
return child.attr("href");
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
throw new ParseException("Parse forums error", body);
|
||||
throw new EhException("Not logged in");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,695 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.client.parser;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.EhDB;
|
||||
import com.hippo.ehviewer.client.EhFilter;
|
||||
import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo;
|
||||
import com.hippo.ehviewer.client.data.GalleryComment;
|
||||
import com.hippo.ehviewer.client.data.GalleryCommentList;
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.client.data.GalleryTagGroup;
|
||||
import com.hippo.ehviewer.client.data.LargePreviewSet;
|
||||
import com.hippo.ehviewer.client.data.NormalPreviewSet;
|
||||
import com.hippo.ehviewer.client.data.PreviewSet;
|
||||
import com.hippo.ehviewer.client.exception.EhException;
|
||||
import com.hippo.ehviewer.client.exception.OffensiveException;
|
||||
import com.hippo.ehviewer.client.exception.ParseException;
|
||||
import com.hippo.ehviewer.client.exception.PiningException;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.util.JsoupUtils;
|
||||
import com.hippo.util.MutableBoolean;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
import com.hippo.yorozuya.StringUtils;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.jsoup.select.NodeTraversor;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class GalleryDetailParser {
|
||||
|
||||
private static final Pattern PATTERN_ERROR = Pattern.compile("<div class=\"d\">\n<p>([^<]+)</p>");
|
||||
private static final Pattern PATTERN_DETAIL = Pattern.compile("var gid = (\\d+);.+?var token = \"([a-f0-9]+)\";.+?var apiuid = ([\\-\\d]+);.+?var apikey = \"([a-f0-9]+)\";", Pattern.DOTALL);
|
||||
private static final Pattern PATTERN_TORRENT = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a");
|
||||
private static final Pattern PATTERN_ARCHIVE = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>");
|
||||
private static final Pattern PATTERN_COVER = Pattern.compile("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)");
|
||||
private static final Pattern PATTERN_PAGES = Pattern.compile("<tr><td[^<>]*>Length:</td><td[^<>]*>([\\d,]+) pages</td></tr>");
|
||||
private static final Pattern PATTERN_PREVIEW_PAGES = Pattern.compile("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?>(?:</a>)?</td>");
|
||||
private static final Pattern PATTERN_NORMAL_PREVIEW = Pattern.compile("<div class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*><img alt=\"([\\d,]+)\"");
|
||||
private static final Pattern PATTERN_LARGE_PREVIEW = Pattern.compile("<div class=\"gdtl\".+?<a href=\"(.+?)\"><img alt=\"([\\d,]+)\".+?src=\"(.+?)\"");
|
||||
private static final Pattern PATTERN_NEWER_DATE = Pattern.compile(", added (.+?)<br />");
|
||||
private static final Pattern PATTERN_FAVORITE_SLOT = Pattern.compile("/fav.png\\); background-position:0px -(\\d+)px");
|
||||
|
||||
private static final GalleryTagGroup[] EMPTY_GALLERY_TAG_GROUP_ARRAY = new GalleryTagGroup[0];
|
||||
private static final GalleryCommentList EMPTY_GALLERY_COMMENT_ARRAY = new GalleryCommentList(new GalleryComment[0], false);
|
||||
|
||||
private static final DateTimeFormatter WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
|
||||
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC);
|
||||
private static final String OFFENSIVE_STRING =
|
||||
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>";
|
||||
private static final String PINING_STRING =
|
||||
"<p>This gallery is pining for the fjords.</p>";
|
||||
|
||||
private static final String[] LANGUAGES = {
|
||||
"English",
|
||||
"Chinese",
|
||||
"Spanish",
|
||||
"Korean",
|
||||
"Russian",
|
||||
"French",
|
||||
"Portuguese",
|
||||
"Thai",
|
||||
"German",
|
||||
"Italian",
|
||||
"Vietnamese",
|
||||
"Polish",
|
||||
"Hungarian",
|
||||
"Dutch",
|
||||
};
|
||||
|
||||
public static GalleryDetail parse(String body) throws EhException {
|
||||
if (body.contains(OFFENSIVE_STRING)) {
|
||||
throw new OffensiveException();
|
||||
}
|
||||
|
||||
if (body.contains(PINING_STRING)) {
|
||||
throw new PiningException();
|
||||
}
|
||||
|
||||
// Error info
|
||||
Matcher m = PATTERN_ERROR.matcher(body);
|
||||
if (m.find()) {
|
||||
throw new EhException(m.group(1));
|
||||
}
|
||||
|
||||
GalleryDetail galleryDetail = new GalleryDetail();
|
||||
Document document = Jsoup.parse(body);
|
||||
parseDetail(galleryDetail, document, body);
|
||||
galleryDetail.tags = parseTagGroups(document);
|
||||
galleryDetail.comments = parseComments(document);
|
||||
galleryDetail.previewPages = parsePreviewPages(document, body);
|
||||
galleryDetail.previewSet = parsePreviewSet(document, body);
|
||||
|
||||
// Generate simpleLanguage for local favorites
|
||||
for (int i = 0; i < LANGUAGES.length; i++) {
|
||||
if (galleryDetail.language.equals(LANGUAGES[i])) {
|
||||
galleryDetail.setSimpleLanguage(GalleryInfo.Companion.getS_LANGS()[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return galleryDetail;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private static void parseDetail(GalleryDetail gd, Document d, String body) throws ParseException {
|
||||
Matcher matcher = PATTERN_DETAIL.matcher(body);
|
||||
if (matcher.find()) {
|
||||
gd.setGid(NumberUtils.parseLongSafely(matcher.group(1), -1L));
|
||||
gd.setToken(matcher.group(2));
|
||||
gd.apiUid = NumberUtils.parseLongSafely(matcher.group(3), -1L);
|
||||
gd.apiKey = matcher.group(4);
|
||||
} else {
|
||||
throw new ParseException("Can't parse gallery detail", body);
|
||||
}
|
||||
if (gd.getGid() == -1L) {
|
||||
throw new ParseException("Can't parse gallery detail", body);
|
||||
}
|
||||
|
||||
matcher = PATTERN_TORRENT.matcher(body);
|
||||
if (matcher.find()) {
|
||||
gd.torrentUrl = StringUtils.unescapeXml(StringUtils.trim(matcher.group(1)));
|
||||
gd.torrentCount = NumberUtils.parseIntSafely(matcher.group(2), 0);
|
||||
} else {
|
||||
gd.torrentCount = 0;
|
||||
gd.torrentUrl = "";
|
||||
}
|
||||
|
||||
matcher = PATTERN_ARCHIVE.matcher(body);
|
||||
if (matcher.find()) {
|
||||
gd.archiveUrl = StringUtils.unescapeXml(StringUtils.trim(matcher.group(1)));
|
||||
} else {
|
||||
gd.archiveUrl = "";
|
||||
}
|
||||
|
||||
try {
|
||||
Element gm = JsoupUtils.getElementByClass(d, "gm");
|
||||
|
||||
// Thumb url
|
||||
Element gd1 = gm.getElementById("gd1");
|
||||
try {
|
||||
gd.setThumb(parseCoverStyle(StringUtils.trim(gd1.child(0).attr("style"))));
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
gd.setThumb("");
|
||||
}
|
||||
|
||||
// Title
|
||||
Element gn = gm.getElementById("gn");
|
||||
if (null != gn) {
|
||||
gd.setTitle(StringUtils.trim(gn.text()));
|
||||
} else {
|
||||
gd.setTitle("");
|
||||
}
|
||||
|
||||
// Jpn title
|
||||
Element gj = gm.getElementById("gj");
|
||||
if (null != gj) {
|
||||
gd.setTitleJpn(StringUtils.trim(gj.text()));
|
||||
} else {
|
||||
gd.setTitleJpn("");
|
||||
}
|
||||
|
||||
// Category
|
||||
Element gdc = gm.getElementById("gdc");
|
||||
try {
|
||||
Element ce = JsoupUtils.getElementByClass(gdc, "cn");
|
||||
if (ce == null) {
|
||||
ce = JsoupUtils.getElementByClass(gdc, "cs");
|
||||
}
|
||||
gd.setCategory(EhUtils.getCategory(ce.text()));
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
gd.setCategory(EhUtils.UNKNOWN);
|
||||
}
|
||||
|
||||
// Uploader
|
||||
Element gdn = gm.getElementById("gdn");
|
||||
if (null != gdn) {
|
||||
gd.setDisowned(gdn.attr("style").contains("opacity:0.5"));
|
||||
gd.setUploader(StringUtils.trim(gdn.text()));
|
||||
} else {
|
||||
gd.setUploader("");
|
||||
}
|
||||
|
||||
Element gdd = gm.getElementById("gdd");
|
||||
gd.setPosted("");
|
||||
gd.parent = "";
|
||||
gd.visible = "";
|
||||
gd.size = "";
|
||||
gd.setPages(0);
|
||||
gd.favoriteCount = 0;
|
||||
Elements es = gdd.child(0).child(0).children();
|
||||
for (int i = 0, n = es.size(); i < n; i++) {
|
||||
parseDetailInfo(gd, es.get(i));
|
||||
}
|
||||
|
||||
// Rating count
|
||||
Element rating_count = gm.getElementById("rating_count");
|
||||
if (null != rating_count) {
|
||||
gd.ratingCount = NumberUtils.parseIntSafely(
|
||||
StringUtils.trim(rating_count.text()), 0);
|
||||
} else {
|
||||
gd.ratingCount = 0;
|
||||
}
|
||||
|
||||
// Rating
|
||||
Element rating_label = gm.getElementById("rating_label");
|
||||
if (null != rating_label) {
|
||||
String ratingStr = StringUtils.trim(rating_label.text());
|
||||
if ("Not Yet Rated".equals(ratingStr)) {
|
||||
gd.setRating(-1.0f);
|
||||
} else {
|
||||
int index = ratingStr.indexOf(' ');
|
||||
if (index == -1 || index >= ratingStr.length()) {
|
||||
gd.setRating(0f);
|
||||
} else {
|
||||
gd.setRating(NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gd.setRating(-1.0f);
|
||||
}
|
||||
|
||||
// isFavorited
|
||||
Element gdf = gm.getElementById("gdf");
|
||||
gd.isFavorited = false;
|
||||
if (gdf != null) {
|
||||
final String favoriteName = StringUtils.trim(gdf.text());
|
||||
if (favoriteName.equals("Add to Favorites")) {
|
||||
gd.setFavoriteName(null);
|
||||
} else {
|
||||
gd.isFavorited = true;
|
||||
gd.setFavoriteName(StringUtils.trim(gdf.text()));
|
||||
|
||||
matcher = PATTERN_FAVORITE_SLOT.matcher(body);
|
||||
if (matcher.find()) {
|
||||
gd.setFavoriteSlot((NumberUtils.parseIntSafely(matcher.group(1), 2) - 2) / 19);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (gd.getFavoriteSlot() == -2 && EhDB.containLocalFavorites(gd.getGid())) {
|
||||
gd.setFavoriteSlot(-1);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
throw new ParseException("Can't parse gallery detail", body);
|
||||
}
|
||||
|
||||
// newer version
|
||||
try {
|
||||
Element gnd = d.getElementById("gnd");
|
||||
if (gnd != null) {
|
||||
matcher = PATTERN_NEWER_DATE.matcher(body);
|
||||
ArrayList<String> dates = new ArrayList<>();
|
||||
while (matcher.find()) {
|
||||
dates.add(matcher.group(1));
|
||||
}
|
||||
Elements elements = gnd.select("a");
|
||||
for (int i = 0; i < elements.size(); i++) {
|
||||
Element element = elements.get(i);
|
||||
GalleryInfo gi = new BaseGalleryInfo();
|
||||
GalleryDetailUrlParser.Result result = GalleryDetailUrlParser.parse(element.attr("href"));
|
||||
if (result != null) {
|
||||
gi.setGid(result.gid);
|
||||
gi.setToken(result.token);
|
||||
gi.setTitle(StringUtils.trim(element.text()));
|
||||
gi.setPosted(dates.get(i));
|
||||
gd.newerVersions.add(gi);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// width:250px; height:356px; background:transparent url(https://exhentai.org/t/fe/1f/fe1fcfa9bf8fba2f03982eda0aa347cc9d6a6372-145921-1050-1492-jpg_250.jpg) 0 0 no-repeat
|
||||
private static String parseCoverStyle(String str) {
|
||||
Matcher matcher = PATTERN_COVER.matcher(str);
|
||||
if (matcher.find()) {
|
||||
return EhUtils.handleThumbUrlResolution(matcher.group(3));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseDetailInfo(GalleryDetail gd, Element e) {
|
||||
Elements es = e.children();
|
||||
if (es.size() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
String key = StringUtils.trim(es.get(0).text());
|
||||
String value = StringUtils.trim(es.get(1).ownText());
|
||||
if (key.startsWith("Posted")) {
|
||||
gd.setPosted(value);
|
||||
} else if (key.startsWith("Parent")) {
|
||||
Element a = es.get(1).children().first();
|
||||
if (a != null) {
|
||||
gd.parent = a.attr("href");
|
||||
}
|
||||
} else if (key.startsWith("Visible")) {
|
||||
gd.visible = value;
|
||||
} else if (key.startsWith("Language")) {
|
||||
gd.language = value;
|
||||
} else if (key.startsWith("File Size")) {
|
||||
gd.size = value;
|
||||
} else if (key.startsWith("Length")) {
|
||||
int index = value.indexOf(' ');
|
||||
if (index >= 0) {
|
||||
gd.setPages(NumberUtils.parseIntSafely(value.substring(0, index), 1));
|
||||
} else {
|
||||
gd.setPages(1);
|
||||
}
|
||||
} else if (key.startsWith("Favorited")) {
|
||||
switch (value) {
|
||||
case "Never" -> gd.favoriteCount = 0;
|
||||
case "Once" -> gd.favoriteCount = 1;
|
||||
default -> {
|
||||
int index = value.indexOf(' ');
|
||||
if (index == -1) {
|
||||
gd.favoriteCount = 0;
|
||||
} else {
|
||||
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static GalleryTagGroup parseTagGroup(Element element) {
|
||||
try {
|
||||
GalleryTagGroup group = new GalleryTagGroup();
|
||||
|
||||
String nameSpace = element.child(0).text();
|
||||
// Remove last ':'
|
||||
nameSpace = nameSpace.substring(0, nameSpace.length() - 1);
|
||||
group.groupName = nameSpace;
|
||||
|
||||
Elements tags = element.child(1).children();
|
||||
for (int i = 0, n = tags.size(); i < n; i++) {
|
||||
String tag = tags.get(i).text();
|
||||
// Sometimes parody tag is followed with '|' and english translate, just remove them
|
||||
int index = tag.indexOf('|');
|
||||
if (index >= 0) {
|
||||
tag = tag.substring(0, index).trim();
|
||||
}
|
||||
group.addTag(tag);
|
||||
}
|
||||
|
||||
return group.size() > 0 ? group : null;
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tag groups with html parser
|
||||
*/
|
||||
@NonNull
|
||||
public static GalleryTagGroup[] parseTagGroups(Document document) {
|
||||
try {
|
||||
Element taglist = document.getElementById("taglist");
|
||||
assert taglist != null;
|
||||
Elements tagGroups = taglist.child(0).child(0).children();
|
||||
return parseTagGroups(tagGroups);
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
e.printStackTrace();
|
||||
return EMPTY_GALLERY_TAG_GROUP_ARRAY;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static GalleryTagGroup[] parseTagGroups(Elements trs) {
|
||||
try {
|
||||
List<GalleryTagGroup> list = new ArrayList<>(trs.size());
|
||||
for (int i = 0, n = trs.size(); i < n; i++) {
|
||||
GalleryTagGroup group = parseTagGroup(trs.get(i));
|
||||
if (null != group) {
|
||||
list.add(group);
|
||||
}
|
||||
}
|
||||
return list.toArray(new GalleryTagGroup[0]);
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
e.printStackTrace();
|
||||
return EMPTY_GALLERY_TAG_GROUP_ARRAY;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public static GalleryComment parseComment(Element element) {
|
||||
try {
|
||||
GalleryComment comment = new GalleryComment();
|
||||
// Id
|
||||
Element a = element.previousElementSibling();
|
||||
String name = a.attr("name");
|
||||
comment.id = Integer.parseInt(StringUtils.trim(name).substring(1));
|
||||
// Editable, vote up and vote down
|
||||
Element c4 = JsoupUtils.getElementByClass(element, "c4");
|
||||
if (null != c4) {
|
||||
if ("Uploader Comment".equals(c4.text())) {
|
||||
comment.uploader = true;
|
||||
}
|
||||
for (Element e : c4.children()) {
|
||||
switch (e.text()) {
|
||||
case "Vote+" -> {
|
||||
comment.voteUpAble = true;
|
||||
comment.voteUpEd = !StringUtils.trim(e.attr("style")).isEmpty();
|
||||
}
|
||||
case "Vote-" -> {
|
||||
comment.voteDownAble = true;
|
||||
comment.voteDownEd = !StringUtils.trim(e.attr("style")).isEmpty();
|
||||
}
|
||||
case "Edit" -> comment.editable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Vote state
|
||||
Element c7 = JsoupUtils.getElementByClass(element, "c7");
|
||||
if (null != c7) {
|
||||
comment.voteState = StringUtils.trim(c7.text());
|
||||
}
|
||||
// Score
|
||||
Element c5 = JsoupUtils.getElementByClass(element, "c5");
|
||||
if (null != c5) {
|
||||
Elements es = c5.children();
|
||||
if (!es.isEmpty()) {
|
||||
comment.score = NumberUtils.parseIntSafely(StringUtils.trim(es.get(0).text()), 0);
|
||||
}
|
||||
}
|
||||
// time
|
||||
Element c3 = JsoupUtils.getElementByClass(element, "c3");
|
||||
String temp = c3.ownText();
|
||||
temp = temp.substring("Posted on ".length(), temp.length() - " by:".length());
|
||||
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
|
||||
// user
|
||||
comment.user = c3.child(0).text();
|
||||
// comment
|
||||
comment.comment = JsoupUtils.getElementByClass(element, "c6").html();
|
||||
// filter comment
|
||||
if (!comment.uploader) {
|
||||
EhFilter sEhFilter = EhFilter.INSTANCE;
|
||||
if (!sEhFilter.filterCommenter(comment.user) || !sEhFilter.filterComment(comment.comment)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// last edited
|
||||
Element c8 = JsoupUtils.getElementByClass(element, "c8");
|
||||
if (c8 != null) {
|
||||
Element e = c8.children().first();
|
||||
if (e != null) {
|
||||
comment.lastEdited = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
|
||||
}
|
||||
}
|
||||
return comment;
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse comments with html parser
|
||||
*/
|
||||
@NonNull
|
||||
public static GalleryCommentList parseComments(Document document) {
|
||||
try {
|
||||
Element cdiv = document.getElementById("cdiv");
|
||||
assert cdiv != null;
|
||||
Elements c1s = cdiv.getElementsByClass("c1");
|
||||
|
||||
List<GalleryComment> list = new ArrayList<>(c1s.size());
|
||||
for (int i = 0, n = c1s.size(); i < n; i++) {
|
||||
GalleryComment comment = parseComment(c1s.get(i));
|
||||
if (null != comment) {
|
||||
list.add(comment);
|
||||
}
|
||||
}
|
||||
|
||||
Element chd = cdiv.getElementById("chd");
|
||||
MutableBoolean hasMore = new MutableBoolean(false);
|
||||
assert chd != null;
|
||||
NodeTraversor.traverse(new NodeVisitor() {
|
||||
@Override
|
||||
public void head(@NonNull Node node, int depth) {
|
||||
if (node instanceof Element && ((Element) node).text().equals("click to show all")) {
|
||||
hasMore.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tail(@NonNull Node node, int depth) {
|
||||
}
|
||||
}, chd);
|
||||
|
||||
return new GalleryCommentList(list.toArray(new GalleryComment[0]), hasMore.value);
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
e.printStackTrace();
|
||||
return EMPTY_GALLERY_COMMENT_ARRAY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse preview pages with html parser
|
||||
*/
|
||||
public static int parsePreviewPages(Document document, String body) throws ParseException {
|
||||
try {
|
||||
Element ptt = document.getElementsByClass("ptt").first();
|
||||
assert ptt != null;
|
||||
Elements elements = ptt.child(0).child(0).children();
|
||||
return Integer.parseInt(elements.get(elements.size() - 2).text());
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
e.printStackTrace();
|
||||
throw new ParseException("Can't parse preview pages", body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse preview pages with regular expressions
|
||||
*/
|
||||
public static int parsePreviewPages(String body) throws ParseException {
|
||||
Matcher m = PATTERN_PREVIEW_PAGES.matcher(body);
|
||||
int previewPages = -1;
|
||||
if (m.find()) {
|
||||
previewPages = ParserUtils.parseInt(m.group(1), -1);
|
||||
}
|
||||
|
||||
if (previewPages <= 0) {
|
||||
throw new ParseException("Parse preview page count error", body);
|
||||
}
|
||||
|
||||
return previewPages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse pages with regular expressions
|
||||
*/
|
||||
public static int parsePages(String body) throws ParseException {
|
||||
int pages = -1;
|
||||
|
||||
Matcher m = PATTERN_PAGES.matcher(body);
|
||||
if (m.find()) {
|
||||
pages = ParserUtils.parseInt(m.group(1), -1);
|
||||
}
|
||||
|
||||
if (pages < 0) {
|
||||
throw new ParseException("Parse pages error", body);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
public static PreviewSet parsePreviewSet(Document d, String body) throws ParseException {
|
||||
try {
|
||||
return parseLargePreviewSet(d, body);
|
||||
} catch (ParseException e) {
|
||||
return parseNormalPreviewSet(body);
|
||||
}
|
||||
}
|
||||
|
||||
public static PreviewSet parsePreviewSet(String body) throws ParseException {
|
||||
try {
|
||||
return parseLargePreviewSet(body);
|
||||
} catch (ParseException e) {
|
||||
return parseNormalPreviewSet(body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse large previews with regular expressions
|
||||
*/
|
||||
private static LargePreviewSet parseLargePreviewSet(Document d, String body) throws ParseException {
|
||||
try {
|
||||
LargePreviewSet largePreviewSet = new LargePreviewSet();
|
||||
Element gdt = d.getElementById("gdt");
|
||||
assert gdt != null;
|
||||
Elements gdtls = gdt.getElementsByClass("gdtl");
|
||||
int n = gdtls.size();
|
||||
if (n == 0) {
|
||||
throw new ParseException("Can't parse large preview", body);
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
Element element = gdtls.get(i).child(0);
|
||||
String pageUrl = element.attr("href");
|
||||
element = element.child(0);
|
||||
String imageUrl = element.attr("src");
|
||||
int index = Integer.parseInt(element.attr("alt")) - 1;
|
||||
largePreviewSet.addItem(index, imageUrl, pageUrl);
|
||||
}
|
||||
return largePreviewSet;
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
e.printStackTrace();
|
||||
throw new ParseException("Can't parse large preview", body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse large previews with regular expressions
|
||||
*/
|
||||
private static LargePreviewSet parseLargePreviewSet(String body) throws ParseException {
|
||||
Matcher m = PATTERN_LARGE_PREVIEW.matcher(body);
|
||||
LargePreviewSet largePreviewSet = new LargePreviewSet();
|
||||
|
||||
while (m.find()) {
|
||||
int index = ParserUtils.parseInt(m.group(2), 0) - 1;
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
String imageUrl = ParserUtils.trim(m.group(3));
|
||||
String pageUrl = ParserUtils.trim(m.group(1));
|
||||
largePreviewSet.addItem(index, imageUrl, pageUrl);
|
||||
}
|
||||
|
||||
if (largePreviewSet.size() == 0) {
|
||||
throw new ParseException("Can't parse large preview", body);
|
||||
}
|
||||
|
||||
return largePreviewSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse normal previews with regular expressions
|
||||
*/
|
||||
private static NormalPreviewSet parseNormalPreviewSet(String body) throws ParseException {
|
||||
Matcher m = PATTERN_NORMAL_PREVIEW.matcher(body);
|
||||
NormalPreviewSet normalPreviewSet = new NormalPreviewSet();
|
||||
while (m.find()) {
|
||||
int position = ParserUtils.parseInt(m.group(6), 0) - 1;
|
||||
if (position < 0) {
|
||||
continue;
|
||||
}
|
||||
String imageUrl = ParserUtils.trim(m.group(3));
|
||||
int xOffset = ParserUtils.parseInt(m.group(4), 0);
|
||||
int yOffset = 0;
|
||||
int width = ParserUtils.parseInt(m.group(1), 0);
|
||||
if (width <= 0) {
|
||||
continue;
|
||||
}
|
||||
int height = ParserUtils.parseInt(m.group(2), 0);
|
||||
if (height <= 0) {
|
||||
continue;
|
||||
}
|
||||
String pageUrl = ParserUtils.trim(m.group(5));
|
||||
normalPreviewSet.addItem(position, imageUrl, xOffset, yOffset, width, height, pageUrl);
|
||||
}
|
||||
|
||||
if (normalPreviewSet.size() == 0) {
|
||||
throw new ParseException("Can't parse normal preview", body);
|
||||
}
|
||||
|
||||
return normalPreviewSet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.client.EhFilter
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.EhUtils.getCategory
|
||||
import com.hippo.ehviewer.client.EhUtils.handleThumbUrlResolution
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryComment
|
||||
import com.hippo.ehviewer.client.data.GalleryCommentList
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.data.GalleryTagGroup
|
||||
import com.hippo.ehviewer.client.data.LargePreviewSet
|
||||
import com.hippo.ehviewer.client.data.NormalPreviewSet
|
||||
import com.hippo.ehviewer.client.data.PreviewSet
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.OffensiveException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.exception.PiningException
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.JsoupUtils
|
||||
import com.hippo.yorozuya.NumberUtils
|
||||
import com.hippo.yorozuya.StringUtils
|
||||
import com.hippo.yorozuya.unescapeXml
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.Node
|
||||
import org.jsoup.select.Elements
|
||||
import org.jsoup.select.NodeTraversor
|
||||
import org.jsoup.select.NodeVisitor
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
object GalleryDetailParser {
|
||||
private val PATTERN_ERROR = Regex("<div class=\"d\">\n<p>([^<]+)</p>")
|
||||
private val PATTERN_DETAIL = Regex(
|
||||
"var gid = (\\d+);.+?var token = \"([a-f0-9]+)\";.+?var apiuid = ([\\-\\d]+);.+?var apikey = \"([a-f0-9]+)\";",
|
||||
RegexOption.DOT_MATCHES_ALL
|
||||
)
|
||||
private val PATTERN_TORRENT =
|
||||
Regex("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a")
|
||||
private val PATTERN_ARCHIVE =
|
||||
Regex("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>")
|
||||
private val PATTERN_COVER = Regex("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)")
|
||||
private val PATTERN_PAGES =
|
||||
Regex("<tr><td[^<>]*>Length:</td><td[^<>]*>([\\d,]+) pages</td></tr>")
|
||||
private val PATTERN_PREVIEW_PAGES =
|
||||
Regex("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?>(?:</a>)?</td>")
|
||||
private val PATTERN_NORMAL_PREVIEW =
|
||||
Regex("<div class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*><img alt=\"([\\d,]+)\"")
|
||||
private val PATTERN_LARGE_PREVIEW =
|
||||
Regex("<div class=\"gdtl\".+?<a href=\"(.+?)\"><img alt=\"([\\d,]+)\".+?src=\"(.+?)\"")
|
||||
private val PATTERN_NEWER_DATE = Regex(", added (.+?)<br />")
|
||||
private val PATTERN_FAVORITE_SLOT =
|
||||
Regex("/fav.png\\); background-position:0px -(\\d+)px")
|
||||
private val EMPTY_GALLERY_TAG_GROUP_ARRAY = arrayOf<GalleryTagGroup>()
|
||||
private val EMPTY_GALLERY_COMMENT_ARRAY = GalleryCommentList(arrayOf(), false)
|
||||
private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
|
||||
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC)
|
||||
private const val OFFENSIVE_STRING =
|
||||
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>"
|
||||
private const val PINING_STRING = "<p>This gallery is pining for the fjords.</p>"
|
||||
|
||||
@Throws(EhException::class)
|
||||
fun parse(body: String): GalleryDetail {
|
||||
if (body.contains(OFFENSIVE_STRING)) {
|
||||
throw OffensiveException()
|
||||
}
|
||||
if (body.contains(PINING_STRING)) {
|
||||
throw PiningException()
|
||||
}
|
||||
|
||||
// Error info
|
||||
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
|
||||
val galleryDetail = GalleryDetail()
|
||||
val document = Jsoup.parse(body)
|
||||
parseDetail(galleryDetail, document, body)
|
||||
galleryDetail.tags = parseTagGroups(document)
|
||||
galleryDetail.comments = parseComments(document)
|
||||
galleryDetail.previewPages = parsePreviewPages(document, body)
|
||||
galleryDetail.previewSet = parsePreviewSet(document, body)
|
||||
|
||||
// Generate simpleLanguage for local favorites
|
||||
galleryDetail.generateSLang()
|
||||
return galleryDetail
|
||||
}
|
||||
|
||||
@Throws(ParseException::class)
|
||||
private fun parseDetail(gd: GalleryDetail, d: Document, body: String) {
|
||||
PATTERN_DETAIL.find(body)?.apply {
|
||||
gd.gid = groupValues[1].toLongOrNull() ?: -1L
|
||||
gd.token = groupValues[2]
|
||||
gd.apiUid = groupValues[3].toLongOrNull() ?: -1L
|
||||
gd.apiKey = groupValues[4]
|
||||
} ?: throw ParseException("Can't parse gallery detail", body)
|
||||
if (gd.gid == -1L) {
|
||||
throw ParseException("Can't parse gallery detail", body)
|
||||
}
|
||||
PATTERN_TORRENT.find(body)?.run {
|
||||
gd.torrentUrl = groupValues[1].trim().unescapeXml()
|
||||
gd.torrentCount = groupValues[2].toIntOrNull() ?: 0
|
||||
}
|
||||
PATTERN_ARCHIVE.find(body)?.run {
|
||||
gd.archiveUrl = groupValues[1].trim().unescapeXml()
|
||||
}
|
||||
try {
|
||||
val gm = d.getElementsByClass("gm")[0]
|
||||
// Thumb url
|
||||
gm.getElementById("gd1")?.child(0)?.attr("style")?.trim()?.let {
|
||||
gd.thumb = PATTERN_COVER.find(it)?.run {
|
||||
handleThumbUrlResolution(groupValues[3])
|
||||
}
|
||||
}
|
||||
|
||||
gd.title = gm.getElementById("gn")?.text()?.trim()
|
||||
gd.titleJpn = gm.getElementById("gj")?.text()?.trim()
|
||||
|
||||
// Category
|
||||
val gdc = gm.getElementById("gdc")
|
||||
try {
|
||||
var ce = JsoupUtils.getElementByClass(gdc, "cn")
|
||||
if (ce == null) {
|
||||
ce = JsoupUtils.getElementByClass(gdc, "cs")
|
||||
}
|
||||
gd.category = getCategory(ce!!.text())
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
gd.category = EhUtils.UNKNOWN
|
||||
}
|
||||
|
||||
// Uploader
|
||||
val gdn = gm.getElementById("gdn")
|
||||
if (null != gdn) {
|
||||
gd.disowned = gdn.attr("style").contains("opacity:0.5")
|
||||
gd.uploader = StringUtils.trim(gdn.text())
|
||||
} else {
|
||||
gd.uploader = ""
|
||||
}
|
||||
val gdd = gm.getElementById("gdd")
|
||||
gd.posted = ""
|
||||
gd.parent = ""
|
||||
gd.visible = ""
|
||||
gd.size = ""
|
||||
gd.pages = 0
|
||||
gd.favoriteCount = 0
|
||||
val es = gdd!!.child(0).child(0).children()
|
||||
es.forEach {
|
||||
parseDetailInfo(gd, it)
|
||||
}
|
||||
|
||||
// Rating count
|
||||
val ratingCount = gm.getElementById("rating_count")
|
||||
if (null != ratingCount) {
|
||||
gd.ratingCount = NumberUtils.parseIntSafely(
|
||||
StringUtils.trim(ratingCount.text()), 0
|
||||
)
|
||||
} else {
|
||||
gd.ratingCount = 0
|
||||
}
|
||||
|
||||
// Rating
|
||||
val ratingLabel = gm.getElementById("rating_label")
|
||||
if (null != ratingLabel) {
|
||||
val ratingStr = StringUtils.trim(ratingLabel.text())
|
||||
if ("Not Yet Rated" == ratingStr) {
|
||||
gd.rating = -1.0f
|
||||
} else {
|
||||
val index = ratingStr.indexOf(' ')
|
||||
if (index == -1 || index >= ratingStr.length) {
|
||||
gd.rating = 0f
|
||||
} else {
|
||||
gd.rating = NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gd.rating = -1.0f
|
||||
}
|
||||
|
||||
// isFavorited
|
||||
val gdf = gm.getElementById("gdf")
|
||||
gd.isFavorited = false
|
||||
if (gdf != null) {
|
||||
val favoriteName = StringUtils.trim(gdf.text())
|
||||
if (favoriteName == "Add to Favorites") {
|
||||
gd.favoriteName = null
|
||||
} else {
|
||||
gd.isFavorited = true
|
||||
gd.favoriteName = StringUtils.trim(gdf.text())
|
||||
PATTERN_FAVORITE_SLOT.find(body)?.run {
|
||||
gd.favoriteSlot = ((groupValues[1].toIntOrNull() ?: 2) - 2) / 19
|
||||
}
|
||||
}
|
||||
}
|
||||
if (gd.favoriteSlot == -2 && EhDB.containLocalFavorites(gd.gid)) {
|
||||
gd.favoriteSlot = -1
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
throw ParseException("Can't parse gallery detail", body)
|
||||
}
|
||||
|
||||
// newer version
|
||||
d.getElementById("gnd")?.run {
|
||||
val dates = PATTERN_NEWER_DATE.findAll(body).map { it.groupValues[1] }.toList()
|
||||
select("a").forEachIndexed { index, element ->
|
||||
val gi = BaseGalleryInfo()
|
||||
val result = GalleryDetailUrlParser.parse(element.attr("href"))
|
||||
if (result != null) {
|
||||
gi.gid = result.gid
|
||||
gi.token = result.token
|
||||
gi.title = StringUtils.trim(element.text())
|
||||
gi.posted = dates[index]
|
||||
gd.newerVersions.add(gi)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDetailInfo(gd: GalleryDetail, e: Element) {
|
||||
val es = e.children()
|
||||
if (es.size < 2) {
|
||||
return
|
||||
}
|
||||
val key = StringUtils.trim(es[0].text())
|
||||
val value = StringUtils.trim(es[1].ownText())
|
||||
if (key.startsWith("Posted")) {
|
||||
gd.posted = value
|
||||
} else if (key.startsWith("Parent")) {
|
||||
val a = es[1].children().first()
|
||||
if (a != null) {
|
||||
gd.parent = a.attr("href")
|
||||
}
|
||||
} else if (key.startsWith("Visible")) {
|
||||
gd.visible = value
|
||||
} else if (key.startsWith("Language")) {
|
||||
gd.language = value
|
||||
} else if (key.startsWith("File Size")) {
|
||||
gd.size = value
|
||||
} else if (key.startsWith("Length")) {
|
||||
val index = value.indexOf(' ')
|
||||
if (index >= 0) {
|
||||
gd.pages = NumberUtils.parseIntSafely(value.substring(0, index), 1)
|
||||
} else {
|
||||
gd.pages = 1
|
||||
}
|
||||
} else if (key.startsWith("Favorited")) {
|
||||
when (value) {
|
||||
"Never" -> gd.favoriteCount = 0
|
||||
"Once" -> gd.favoriteCount = 1
|
||||
else -> {
|
||||
val index = value.indexOf(' ')
|
||||
if (index == -1) {
|
||||
gd.favoriteCount = 0
|
||||
} else {
|
||||
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTagGroup(element: Element): GalleryTagGroup? {
|
||||
return try {
|
||||
val group = GalleryTagGroup()
|
||||
var nameSpace = element.child(0).text()
|
||||
// Remove last ':'
|
||||
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
|
||||
group.groupName = nameSpace
|
||||
val tags = element.child(1).children()
|
||||
tags.forEach {
|
||||
var tag = it.text()
|
||||
// Sometimes parody tag is followed with '|' and english translate, just remove them
|
||||
val index = tag.indexOf('|')
|
||||
if (index >= 0) {
|
||||
tag = tag.substring(0, index).trim()
|
||||
}
|
||||
if (it.className() == "gtw") {
|
||||
tag = "_$tag" // weak tag
|
||||
}
|
||||
group.add(tag)
|
||||
}
|
||||
if (group.size > 0) group else null
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tag groups with html parser
|
||||
*/
|
||||
private fun parseTagGroups(document: Document): Array<GalleryTagGroup> {
|
||||
return try {
|
||||
val taglist = document.getElementById("taglist")!!
|
||||
val tagGroups = taglist.child(0).child(0).children()
|
||||
parseTagGroups(tagGroups)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
EMPTY_GALLERY_TAG_GROUP_ARRAY
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTagGroups(trs: Elements): Array<GalleryTagGroup> {
|
||||
return try {
|
||||
trs.mapNotNull { parseTagGroup(it) }.toTypedArray()
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
EMPTY_GALLERY_TAG_GROUP_ARRAY
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseComment(element: Element): GalleryComment? {
|
||||
return try {
|
||||
val comment = GalleryComment()
|
||||
// Id
|
||||
val a = element.previousElementSibling()
|
||||
val name = a!!.attr("name")
|
||||
comment.id = StringUtils.trim(name).substring(1).toInt().toLong()
|
||||
// Editable, vote up and vote down
|
||||
val c4 = JsoupUtils.getElementByClass(element, "c4")
|
||||
if (null != c4) {
|
||||
if ("Uploader Comment" == c4.text()) {
|
||||
comment.uploader = true
|
||||
}
|
||||
for (e in c4.children()) {
|
||||
when (e.text()) {
|
||||
"Vote+" -> {
|
||||
comment.voteUpAble = true
|
||||
comment.voteUpEd = StringUtils.trim(e.attr("style")).isNotEmpty()
|
||||
}
|
||||
|
||||
"Vote-" -> {
|
||||
comment.voteDownAble = true
|
||||
comment.voteDownEd = StringUtils.trim(e.attr("style")).isNotEmpty()
|
||||
}
|
||||
|
||||
"Edit" -> comment.editable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Vote state
|
||||
val c7 = JsoupUtils.getElementByClass(element, "c7")
|
||||
if (null != c7) {
|
||||
comment.voteState = StringUtils.trim(c7.text())
|
||||
}
|
||||
// Score
|
||||
val c5 = JsoupUtils.getElementByClass(element, "c5")
|
||||
if (null != c5) {
|
||||
val es = c5.children()
|
||||
if (!es.isEmpty()) {
|
||||
comment.score = NumberUtils.parseIntSafely(
|
||||
StringUtils.trim(es[0].text()), 0
|
||||
)
|
||||
}
|
||||
}
|
||||
// time
|
||||
val c3 = JsoupUtils.getElementByClass(element, "c3")
|
||||
var temp = c3!!.ownText()
|
||||
if (temp.endsWith(':')) {
|
||||
temp = temp.substring("Posted on ".length, temp.length - " by:".length)
|
||||
// user
|
||||
comment.user = c3.child(0).text()
|
||||
} else {
|
||||
temp = temp.substring("Posted on ".length)
|
||||
}
|
||||
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli()
|
||||
// comment
|
||||
val c6 = JsoupUtils.getElementByClass(element, "c6")
|
||||
// fix underline support
|
||||
for (e in c6!!.children()) {
|
||||
if ("span" == e.tagName() && "text-decoration:underline;" == e.attr("style")) {
|
||||
e.tagName("u")
|
||||
}
|
||||
}
|
||||
comment.comment = c6.html()
|
||||
// filter comment
|
||||
if (!comment.uploader) {
|
||||
val sEhFilter = EhFilter
|
||||
if (!sEhFilter.filterCommenter(comment.user) || !sEhFilter.filterComment(comment.comment)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// last edited
|
||||
val c8 = JsoupUtils.getElementByClass(element, "c8")
|
||||
if (c8 != null) {
|
||||
val e = c8.children().first()
|
||||
if (e != null) {
|
||||
comment.lastEdited =
|
||||
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli()
|
||||
}
|
||||
}
|
||||
comment
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse comments with html parser
|
||||
*/
|
||||
fun parseComments(document: Document): GalleryCommentList {
|
||||
return try {
|
||||
val cdiv = document.getElementById("cdiv")!!
|
||||
val c1s = cdiv.getElementsByClass("c1")
|
||||
val list = c1s.mapNotNull { parseComment(it) }
|
||||
val chd = cdiv.getElementById("chd")
|
||||
var hasMore = false
|
||||
NodeTraversor.traverse(object : NodeVisitor {
|
||||
override fun head(node: Node, depth: Int) {
|
||||
if (node is Element && node.text() == "click to show all") {
|
||||
hasMore = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun tail(node: Node, depth: Int) {}
|
||||
}, chd!!)
|
||||
GalleryCommentList(list.toTypedArray(), hasMore)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
EMPTY_GALLERY_COMMENT_ARRAY
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse preview pages with html parser
|
||||
*/
|
||||
@Throws(ParseException::class)
|
||||
fun parsePreviewPages(document: Document, body: String?): Int {
|
||||
return try {
|
||||
val ptt = document.getElementsByClass("ptt").first()!!
|
||||
val elements = ptt.child(0).child(0).children()
|
||||
elements[elements.size - 2].text().toInt()
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
throw ParseException("Can't parse preview pages", body!!)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse preview pages with regular expressions
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(ParseException::class)
|
||||
fun parsePreviewPages(body: String): Int {
|
||||
return PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||
?: throw ParseException("Parse preview page count error", body)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse pages with regular expressions
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(ParseException::class)
|
||||
fun parsePages(body: String): Int {
|
||||
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||
?: throw ParseException("Parse pages error", body)
|
||||
}
|
||||
|
||||
@Throws(ParseException::class)
|
||||
fun parsePreviewSet(d: Document, body: String): PreviewSet {
|
||||
return try {
|
||||
parseLargePreviewSet(d, body)
|
||||
} catch (e: ParseException) {
|
||||
parseNormalPreviewSet(body)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(ParseException::class)
|
||||
fun parsePreviewSet(body: String): PreviewSet {
|
||||
return try {
|
||||
parseLargePreviewSet(body)
|
||||
} catch (e: ParseException) {
|
||||
parseNormalPreviewSet(body)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse large previews with regular expressions
|
||||
*/
|
||||
@Throws(ParseException::class)
|
||||
private fun parseLargePreviewSet(d: Document, body: String): LargePreviewSet {
|
||||
return try {
|
||||
val largePreviewSet = LargePreviewSet()
|
||||
val gdt = d.getElementById("gdt")!!
|
||||
val gdtls = gdt.getElementsByClass("gdtl")
|
||||
val n = gdtls.size
|
||||
if (n == 0) {
|
||||
throw ParseException("Can't parse large preview", body)
|
||||
}
|
||||
for (i in 0 until n) {
|
||||
var element = gdtls[i].child(0)
|
||||
val pageUrl = element.attr("href")
|
||||
element = element.child(0)
|
||||
val imageUrl = element.attr("src")
|
||||
val index = element.attr("alt").toInt() - 1
|
||||
largePreviewSet.addItem(index, imageUrl, pageUrl)
|
||||
}
|
||||
largePreviewSet
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
throw ParseException("Can't parse large preview", body)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse large previews with regular expressions
|
||||
*/
|
||||
@Throws(ParseException::class)
|
||||
private fun parseLargePreviewSet(body: String): LargePreviewSet {
|
||||
val largePreviewSet = LargePreviewSet()
|
||||
PATTERN_LARGE_PREVIEW.findAll(body).forEach {
|
||||
val index = (it.groupValues[2].toIntOrNull() ?: return@forEach) - 1
|
||||
val imageUrl = it.groupValues[3].trim()
|
||||
val pageUrl = it.groupValues[1].trim()
|
||||
largePreviewSet.addItem(index, imageUrl, pageUrl)
|
||||
}
|
||||
if (largePreviewSet.size() == 0) {
|
||||
throw ParseException("Can't parse large preview", body)
|
||||
}
|
||||
return largePreviewSet
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse normal previews with regular expressions
|
||||
*/
|
||||
@Throws(ParseException::class)
|
||||
private fun parseNormalPreviewSet(body: String): NormalPreviewSet {
|
||||
val normalPreviewSet = NormalPreviewSet()
|
||||
PATTERN_NORMAL_PREVIEW.findAll(body).forEach {
|
||||
val position = (it.groupValues[6].toIntOrNull() ?: return@forEach) - 1
|
||||
val imageUrl = it.groupValues[3].trim()
|
||||
val xOffset = it.groupValues[4].toIntOrNull() ?: 0
|
||||
val yOffset = 0
|
||||
val width = it.groupValues[1].toIntOrNull() ?: return@forEach
|
||||
val height = it.groupValues[2].toIntOrNull() ?: return@forEach
|
||||
val pageUrl = it.groupValues[5].trim()
|
||||
normalPreviewSet.addItem(position, imageUrl, xOffset, yOffset, width, height, pageUrl)
|
||||
}
|
||||
if (normalPreviewSet.size() == 0) {
|
||||
throw ParseException("Can't parse normal preview", body)
|
||||
}
|
||||
return normalPreviewSet
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object HomeParser {
|
||||
private val PATTERN_FUNDS =
|
||||
Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
|
||||
private const val RESET_SUCCEED = "Image limit was successfully reset."
|
||||
|
||||
fun parse(body: String): Limits {
|
||||
Jsoup.parse(body).selectFirst("div.homebox")?.let {
|
||||
val es = it.select("p > strong")
|
||||
if (es.size == 3) {
|
||||
val current = ParserUtils.parseInt(es[0].text(), 0)
|
||||
val maximum = ParserUtils.parseInt(es[1].text(), 0)
|
||||
val resetCost = ParserUtils.parseInt(es[2].text(), 0)
|
||||
return Limits(current, maximum, resetCost)
|
||||
}
|
||||
}
|
||||
throw ParseException("Parse image limits error", body)
|
||||
}
|
||||
|
||||
fun parseResetLimits(body: String): Limits? {
|
||||
return if (body.contains(RESET_SUCCEED)){
|
||||
null
|
||||
} else {
|
||||
parse(body)
|
||||
}
|
||||
}
|
||||
|
||||
fun parseFunds(body: String): Funds {
|
||||
PATTERN_FUNDS.find(body)?.groupValues?.run {
|
||||
val fundsC = ParserUtils.parseInt(get(1), 0)
|
||||
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
|
||||
return Funds(fundsGP, fundsC)
|
||||
}
|
||||
throw ParseException("Parse funds error", body)
|
||||
}
|
||||
|
||||
data class Limits(val current: Int = 0, val maximum: Int, val resetCost: Int = 0)
|
||||
data class Funds(val fundsGP: Int, val fundsC: Int)
|
||||
class Result(val limits: Limits, val funds: Funds)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -25,6 +25,7 @@ import android.os.IBinder
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
@@ -37,11 +38,10 @@ import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
import com.hippo.ehviewer.ui.scene.DownloadsScene
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.util.getParcelableExtraCompat
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.SimpleHandler
|
||||
import com.hippo.yorozuya.collect.LongList
|
||||
import com.hippo.yorozuya.collect.SparseJBArray
|
||||
import com.hippo.yorozuya.collect.SparseJLArray
|
||||
|
||||
class DownloadService : Service(), DownloadManager.DownloadListener {
|
||||
private var mNotifyManager: NotificationManagerCompat? = null
|
||||
@@ -99,13 +99,13 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
|
||||
action = intent.action
|
||||
}
|
||||
if (ACTION_START == action) {
|
||||
val gi = intent!!.getParcelableExtra<GalleryInfo>(KEY_GALLERY_INFO)
|
||||
val gi = intent!!.getParcelableExtraCompat<GalleryInfo>(KEY_GALLERY_INFO)
|
||||
val label = intent.getStringExtra(KEY_LABEL)
|
||||
if (gi != null && mDownloadManager != null) {
|
||||
mDownloadManager!!.startDownload(gi, label)
|
||||
}
|
||||
} else if (ACTION_START_RANGE == action) {
|
||||
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
|
||||
val gidList = intent!!.getParcelableExtraCompat<LongList>(KEY_GID_LIST)
|
||||
if (gidList != null && mDownloadManager != null) {
|
||||
mDownloadManager!!.startRangeDownload(gidList)
|
||||
}
|
||||
@@ -123,7 +123,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
|
||||
mDownloadManager!!.stopCurrentDownload()
|
||||
}
|
||||
} else if (ACTION_STOP_RANGE == action) {
|
||||
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
|
||||
val gidList = intent!!.getParcelableExtraCompat<LongList>(KEY_GID_LIST)
|
||||
if (gidList != null && mDownloadManager != null) {
|
||||
mDownloadManager!!.stopRangeDownload(gidList)
|
||||
}
|
||||
@@ -137,7 +137,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
|
||||
mDownloadManager!!.deleteDownload(gid)
|
||||
}
|
||||
} else if (ACTION_DELETE_RANGE == action) {
|
||||
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
|
||||
val gidList = intent!!.getParcelableExtraCompat<LongList>(KEY_GID_LIST)
|
||||
if (gidList != null && mDownloadManager != null) {
|
||||
mDownloadManager!!.deleteRangeDownload(gidList)
|
||||
}
|
||||
@@ -533,8 +533,8 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
|
||||
private const val ID_DOWNLOADING = 1
|
||||
private const val ID_DOWNLOADED = 2
|
||||
private const val ID_509 = 3
|
||||
private val sItemStateArray = SparseJBArray()
|
||||
private val sItemTitleArray = SparseJLArray<String>()
|
||||
private val sItemStateArray = LongSparseArray<Boolean>()
|
||||
private val sItemTitleArray = LongSparseArray<String>()
|
||||
private var sFailedCount = 0
|
||||
private var sFinishedCount = 0
|
||||
private var sDownloadedCount = 0
|
||||
|
||||
@@ -46,6 +46,7 @@ import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
import kotlin.Pair;
|
||||
|
||||
public class ArchivePageLoader extends PageLoader2 {
|
||||
private static final AtomicInteger sIdGenerator = new AtomicInteger();
|
||||
@@ -54,7 +55,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
public static PVLock pv = new PVLock(0);
|
||||
private final UriArchiveAccessor archiveAccessor;
|
||||
private final Stack<Integer> requests = new Stack<>();
|
||||
private final LinkedHashMap<Integer, Image.ByteBufferSource> streams = new LinkedHashMap<>();
|
||||
private final LinkedHashMap<Integer, Image.CloseableSource> streams = new LinkedHashMap<>();
|
||||
private final Thread[] decodeThread = new Thread[]{
|
||||
new Thread(new DecodeTask()),
|
||||
new Thread(new DecodeTask()),
|
||||
@@ -187,7 +188,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preloadPages(@NonNull List<Integer> pages) {
|
||||
protected void preloadPages(@NonNull List<Integer> pages, @NonNull Pair<Integer, Integer> pair) {
|
||||
|
||||
}
|
||||
|
||||
@@ -293,7 +294,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
|
||||
// Check index valid
|
||||
if (index < 0 || index >= size) {
|
||||
notifyPageFailed(index);
|
||||
notifyPageFailed(index, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -303,7 +304,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
}
|
||||
}
|
||||
|
||||
Image.ByteBufferSource src = archiveAccessor.getImageSource(index);
|
||||
Image.CloseableSource src = archiveAccessor.getImageSource(index);
|
||||
|
||||
synchronized (streams) {
|
||||
streams.put(index, src);
|
||||
@@ -318,7 +319,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int index;
|
||||
Image.ByteBufferSource src;
|
||||
Image.CloseableSource src;
|
||||
synchronized (streams) {
|
||||
if (streams.isEmpty()) {
|
||||
try {
|
||||
@@ -329,8 +330,8 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
Iterator<Map.Entry<Integer, Image.ByteBufferSource>> iterator = streams.entrySet().iterator();
|
||||
Map.Entry<Integer, Image.ByteBufferSource> entry = iterator.next();
|
||||
Iterator<Map.Entry<Integer, Image.CloseableSource>> iterator = streams.entrySet().iterator();
|
||||
Map.Entry<Integer, Image.CloseableSource> entry = iterator.next();
|
||||
iterator.remove();
|
||||
index = entry.getKey();
|
||||
src = entry.getValue();
|
||||
@@ -343,7 +344,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
if (image != null) {
|
||||
notifyPageSucceed(index, image);
|
||||
} else {
|
||||
notifyPageFailed(index);
|
||||
notifyPageFailed(index, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
import kotlin.Pair;
|
||||
|
||||
public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderListener {
|
||||
|
||||
@@ -124,28 +125,14 @@ public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderLis
|
||||
@Override
|
||||
protected void onRequest(int index) {
|
||||
if (mSpiderQueen != null) {
|
||||
Object object = mSpiderQueen.request(index, true);
|
||||
if (object instanceof Float) {
|
||||
notifyPagePercent(index, (Float) object);
|
||||
} else if (object instanceof String) {
|
||||
notifyPageFailed(index);
|
||||
} else if (object == null) {
|
||||
notifyPageWait(index);
|
||||
}
|
||||
mSpiderQueen.request(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onForceRequest(int index) {
|
||||
if (mSpiderQueen != null) {
|
||||
Object object = mSpiderQueen.forceRequest(index);
|
||||
if (object instanceof Float) {
|
||||
notifyPagePercent(index, (Float) object);
|
||||
} else if (object instanceof String) {
|
||||
notifyPageFailed(index);
|
||||
} else if (object == null) {
|
||||
notifyPageWait(index);
|
||||
}
|
||||
mSpiderQueen.forceRequest(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,15 +170,11 @@ public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderLis
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSuccess(int index, int finished, int downloaded, int total) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.request(index, false);
|
||||
}
|
||||
}
|
||||
public void onPageSuccess(int index, int finished, int downloaded, int total) {}
|
||||
|
||||
@Override
|
||||
public void onPageFailure(int index, String error, int finished, int downloaded, int total) {
|
||||
notifyPageFailed(index);
|
||||
notifyPageFailed(index, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -205,13 +188,13 @@ public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderLis
|
||||
|
||||
@Override
|
||||
public void onGetImageFailure(int index, String error) {
|
||||
notifyPageFailed(index);
|
||||
notifyPageFailed(index, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preloadPages(@NonNull List<Integer> pages) {
|
||||
protected void preloadPages(@NonNull List<Integer> pages, @NonNull Pair<Integer, Integer> pair) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.preloadPages(pages);
|
||||
mSpiderQueen.preloadPages(pages, pair);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ public abstract class PageLoader2 extends PageLoader {
|
||||
// With dot
|
||||
public static final String[] SUPPORT_IMAGE_EXTENSIONS = {
|
||||
".jpg", // Joint Photographic Experts Group
|
||||
".jpeg",
|
||||
".png", // Portable Network Graphics
|
||||
".gif", // Graphics Interchange Format
|
||||
};
|
||||
|
||||
@@ -53,15 +53,15 @@ class AccountPreference @JvmOverloads constructor(
|
||||
while (i < n) {
|
||||
val cookie = cookies[i]
|
||||
when (cookie.name) {
|
||||
EhCookieStore.KEY_IPD_MEMBER_ID -> ipbMemberId = cookie.value
|
||||
EhCookieStore.KEY_IPD_PASS_HASH -> ipbPassHash = cookie.value
|
||||
EhCookieStore.KEY_IPB_MEMBER_ID -> ipbMemberId = cookie.value
|
||||
EhCookieStore.KEY_IPB_PASS_HASH -> ipbPassHash = cookie.value
|
||||
EhCookieStore.KEY_IGNEOUS -> igneous = cookie.value
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (ipbMemberId != null || ipbPassHash != null || igneous != null) {
|
||||
message = (EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "<br>"
|
||||
+ EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "<br>"
|
||||
message = (EhCookieStore.KEY_IPB_MEMBER_ID + ": " + ipbMemberId + "<br>"
|
||||
+ EhCookieStore.KEY_IPB_PASS_HASH + ": " + ipbPassHash + "<br>"
|
||||
+ EhCookieStore.KEY_IGNEOUS + ": " + igneous)
|
||||
setDialogMessage(
|
||||
Html.fromHtml(
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.ui.SettingsActivity
|
||||
import com.hippo.preference.DialogPreference
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
|
||||
class ImageLimitsPreference(context: Context, attrs: AttributeSet) :
|
||||
DialogPreference(context, attrs), View.OnClickListener {
|
||||
private val mActivity = context as SettingsActivity
|
||||
private val placeholder = context.getString(R.string.please_wait)
|
||||
private lateinit var resetButton: Button
|
||||
private lateinit var mDialog: AlertDialog
|
||||
|
||||
init {
|
||||
updateSummary()
|
||||
}
|
||||
|
||||
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
super.onPrepareDialogBuilder(builder)
|
||||
builder.setMessage(placeholder)
|
||||
}
|
||||
|
||||
override fun onDialogCreated(dialog: AlertDialog) {
|
||||
super.onDialogCreated(dialog)
|
||||
mDialog = dialog
|
||||
resetButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
resetButton.setOnClickListener(this)
|
||||
resetButton.isEnabled = false
|
||||
launchIOCatching {
|
||||
mActivity.getImageLimits()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
super.onDialogClosed(positiveResult)
|
||||
updateSummary()
|
||||
}
|
||||
|
||||
private fun updateSummary() {
|
||||
summary = mActivity.mLimits?.run {
|
||||
mActivity.getString(R.string.image_limits_summary, current, maximum)
|
||||
} ?: mActivity.getString(R.string.image_limits_summary, 0, 0)
|
||||
}
|
||||
|
||||
private fun launchIOCatching(block: suspend () -> Unit) = mActivity.lifecycleScope.launchIO {
|
||||
runCatching {
|
||||
block()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
withUIContext {
|
||||
mDialog.setMessage(it.message)
|
||||
}
|
||||
}.onSuccess {
|
||||
withUIContext {
|
||||
bind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bind() {
|
||||
val (current, maximum, resetCost) = mActivity.mLimits!!
|
||||
val (fundsGP, fundsC) = mActivity.mFunds!!
|
||||
val cost = if (fundsGP >= resetCost) "$resetCost GP" else "$resetCost Credits"
|
||||
val message = mActivity.getString(R.string.current_limits, "$current / $maximum", cost) +
|
||||
"\n" + mActivity.getString(R.string.current_funds, "$fundsGP+", fundsC)
|
||||
mDialog.setMessage(message)
|
||||
resetButton.isEnabled = resetCost in 1..maxOf(fundsGP, fundsC)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
resetButton.isEnabled = false
|
||||
mDialog.setMessage(placeholder)
|
||||
launchIOCatching {
|
||||
mActivity.resetImageLimits()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,15 +27,13 @@ import com.hippo.ehviewer.client.EhEngine.fillGalleryListByApi
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.spider.SpiderInfo
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.readCompatFromUniFile
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.Collections
|
||||
|
||||
class RestoreDownloadPreference constructor(
|
||||
@@ -53,10 +51,8 @@ class RestoreDownloadPreference constructor(
|
||||
return null
|
||||
}
|
||||
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
|
||||
var `is`: InputStream? = null
|
||||
return try {
|
||||
`is` = siFile.openInputStream()
|
||||
val spiderInfo = SpiderInfo.read(`is`) ?: return null
|
||||
val spiderInfo = readCompatFromUniFile(siFile) ?: return null
|
||||
val gid = spiderInfo.gid
|
||||
val dirname = file.name
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
@@ -76,8 +72,6 @@ class RestoreDownloadPreference constructor(
|
||||
restoreItem
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
} finally {
|
||||
IOUtils.closeQuietly(`is`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.preference;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.client.EhCookieStore;
|
||||
import com.hippo.ehviewer.client.EhUrl;
|
||||
import com.hippo.ehviewer.ui.SettingsActivity;
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class SignInRequiredPreference extends Preference implements Preference.OnPreferenceClickListener {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private SettingsActivity mActivity;
|
||||
|
||||
public SignInRequiredPreference(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public SignInRequiredPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public SignInRequiredPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
mActivity = (SettingsActivity) context;
|
||||
setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
EhCookieStore store = EhApplication.getEhCookieStore();
|
||||
HttpUrl e = HttpUrl.get(EhUrl.HOST_E);
|
||||
HttpUrl ex = HttpUrl.get(EhUrl.HOST_EX);
|
||||
|
||||
if (store.contains(e, EhCookieStore.KEY_IPD_MEMBER_ID) ||
|
||||
store.contains(e, EhCookieStore.KEY_IPD_PASS_HASH) ||
|
||||
store.contains(ex, EhCookieStore.KEY_IPD_MEMBER_ID) ||
|
||||
store.contains(ex, EhCookieStore.KEY_IPD_PASS_HASH)) {
|
||||
return false;
|
||||
} else {
|
||||
mActivity.showTip(R.string.error_please_login_first, BaseScene.LENGTH_SHORT);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.spider;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.Native;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.EhDB;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory;
|
||||
import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.gallery.PageLoader2;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.io.UniFileInputStreamPipe;
|
||||
import com.hippo.io.UniFileOutputStreamPipe;
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
import com.hippo.unifile.RawFile;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
import com.hippo.yorozuya.Utilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Locale;
|
||||
|
||||
import coil.disk.DiskCache;
|
||||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
|
||||
public final class SpiderDen {
|
||||
|
||||
@Nullable
|
||||
private static DiskCache sCache; // We use data to store image file, and metadata for image type
|
||||
private final GalleryInfo mGalleryInfo;
|
||||
private final long mGid;
|
||||
@Nullable
|
||||
private UniFile mDownloadDir;
|
||||
private volatile int mMode = SpiderQueen.MODE_READ;
|
||||
|
||||
public SpiderDen(GalleryInfo galleryInfo) {
|
||||
mGalleryInfo = galleryInfo;
|
||||
mGid = galleryInfo.getGid();
|
||||
mDownloadDir = getGalleryDownloadDir(galleryInfo.getGid());
|
||||
}
|
||||
|
||||
public static void initialize(Context context) {
|
||||
sCache = new DiskCache.Builder().directory(new File(context.getCacheDir(), "gallery_image"))
|
||||
.maxSizeBytes((long) MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280) * 1024 * 1024).build();
|
||||
}
|
||||
|
||||
public static UniFile getGalleryDownloadDir(long gid) {
|
||||
UniFile dir = Settings.getDownloadLocation();
|
||||
// Read from DB
|
||||
String dirname = EhDB.getDownloadDirname(gid);
|
||||
if (dir != null && dirname != null) {
|
||||
// Some dirname may be invalid in some version
|
||||
dirname = FileUtils.sanitizeFilename(dirname);
|
||||
EhDB.putDownloadDirname(gid, dirname);
|
||||
return dir.subFile(dirname);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extension with dot
|
||||
*/
|
||||
public static String generateImageFilename(int index, String extension) {
|
||||
return String.format(Locale.US, "%08d%s", index + 1, extension);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static UniFile findImageFile(UniFile dir, int index) {
|
||||
for (String extension : PageLoader2.SUPPORT_IMAGE_EXTENSIONS) {
|
||||
String filename = generateImageFilename(index, extension);
|
||||
UniFile file = dir.findFile(filename);
|
||||
if (file != null) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setMode(@SpiderQueen.Mode int mode) {
|
||||
mMode = mode;
|
||||
|
||||
if (mode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
ensureDownloadDir();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean ensureDownloadDir() {
|
||||
if (mDownloadDir == null) {
|
||||
var dirname = FileUtils.sanitizeFilename(mGid + "-" + EhUtils.getSuitableTitle(mGalleryInfo));
|
||||
EhDB.putDownloadDirname(mGid, dirname);
|
||||
mDownloadDir = getGalleryDownloadDir(mGid);
|
||||
}
|
||||
return mDownloadDir != null && mDownloadDir.ensureDir();
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return switch (mMode) {
|
||||
case SpiderQueen.MODE_READ -> sCache != null;
|
||||
case SpiderQueen.MODE_DOWNLOAD -> mDownloadDir != null && mDownloadDir.isDirectory();
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UniFile getDownloadDir() {
|
||||
if (mDownloadDir == null) {
|
||||
mDownloadDir = getGalleryDownloadDir(mGid);
|
||||
}
|
||||
return mDownloadDir != null && mDownloadDir.isDirectory() ? mDownloadDir : null;
|
||||
}
|
||||
|
||||
private boolean containInCache(int index) {
|
||||
if (sCache == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
var snapshot = sCache.get(key);
|
||||
var exist = snapshot != null;
|
||||
if (exist) snapshot.close();
|
||||
return exist;
|
||||
}
|
||||
|
||||
private boolean containInDownloadDir(int index) {
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find image file in download dir
|
||||
return findImageFile(dir, index) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extension with dot
|
||||
*/
|
||||
private String fixExtension(String extension) {
|
||||
if (Utilities.contain(PageLoader2.SUPPORT_IMAGE_EXTENSIONS, extension)) {
|
||||
return extension;
|
||||
} else {
|
||||
return PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0];
|
||||
}
|
||||
}
|
||||
|
||||
private boolean copyFromCacheToDownloadDir(int index) {
|
||||
if (sCache == null) {
|
||||
return false;
|
||||
}
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return false;
|
||||
}
|
||||
// Find image file in cache
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
var snapshot = sCache.get(key);
|
||||
if (snapshot == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OutputStream os = null;
|
||||
try (snapshot; FileInputStream is = new FileInputStream(snapshot.getData().toFile()); BufferedSource buf = Okio.buffer(Okio.source(snapshot.getMetadata().toFile()))) {
|
||||
// Get extension
|
||||
String extension = fixExtension("." + buf.readUtf8Line());
|
||||
// Copy from cache to download dir
|
||||
UniFile file = dir.createFile(generateImageFilename(index, extension));
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
os = file.openOutputStream();
|
||||
IOUtils.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contain(int index) {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
return containInCache(index) || containInDownloadDir(index);
|
||||
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
return containInDownloadDir(index) || copyFromCacheToDownloadDir(index);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean removeFromCache(int index) {
|
||||
if (sCache == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
return sCache.remove(key);
|
||||
}
|
||||
|
||||
private boolean removeFromDownloadDir(int index) {
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result = false;
|
||||
for (int i = 0, n = PageLoader2.SUPPORT_IMAGE_EXTENSIONS.length; i < n; i++) {
|
||||
String filename = generateImageFilename(index, PageLoader2.SUPPORT_IMAGE_EXTENSIONS[i]);
|
||||
UniFile file = dir.subFile(filename);
|
||||
if (file != null) {
|
||||
result |= file.delete();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean remove(int index) {
|
||||
boolean result = removeFromCache(index);
|
||||
result |= removeFromDownloadDir(index);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private OutputStreamPipe openCacheOutputStreamPipe(int index, @Nullable String extension) {
|
||||
if (sCache == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
var editor = sCache.edit(key);
|
||||
if (editor == null) return null;
|
||||
try (var sink = Okio.buffer(Okio.sink(editor.getMetadata().toFile()))) {
|
||||
sink.writeUtf8(extension);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new OutputStreamPipe() {
|
||||
private OutputStream os = null;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public OutputStream open() throws IOException {
|
||||
if (os != null) IOUtils.closeQuietly(os);
|
||||
os = new FileOutputStream(editor.getData().toFile());
|
||||
return os;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (os != null) IOUtils.closeQuietly(os);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extension without dot
|
||||
*/
|
||||
@Nullable
|
||||
private OutputStreamPipe openDownloadOutputStreamPipe(int index, @Nullable String extension) {
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
extension = fixExtension('.' + extension);
|
||||
UniFile file = dir.createFile(generateImageFilename(index, extension));
|
||||
if (file != null) {
|
||||
return new UniFileOutputStreamPipe(file);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public OutputStreamPipe openOutputStreamPipe(int index, @Nullable String extension) {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
// Return the download pipe is the gallery has been downloaded
|
||||
OutputStreamPipe pipe = openDownloadOutputStreamPipe(index, extension);
|
||||
if (pipe == null) {
|
||||
pipe = openCacheOutputStreamPipe(index, extension);
|
||||
}
|
||||
return pipe;
|
||||
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
return openDownloadOutputStreamPipe(index, extension);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private InputStreamPipe openCacheInputStreamPipe(int index) {
|
||||
if (sCache == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
var snapshot = sCache.get(key);
|
||||
if (snapshot == null) return null;
|
||||
return new InputStreamPipe() {
|
||||
private InputStream is = null;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
snapshot.close();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InputStream open() throws IOException {
|
||||
if (is != null) IOUtils.closeQuietly(is);
|
||||
is = new FileInputStream(snapshot.getData().toFile());
|
||||
return is;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (is != null) IOUtils.closeQuietly(is);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InputStreamPipe openDownloadInputStreamPipe(int index) {
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
UniFile file = findImageFile(dir, index);
|
||||
if (file != null) {
|
||||
return new UniFileInputStreamPipe(file);
|
||||
} else if (!copyFromCacheToDownloadDir(index)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InputStreamPipe openInputStreamPipe(int index) {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
InputStreamPipe pipe = openCacheInputStreamPipe(index);
|
||||
if (pipe == null) {
|
||||
pipe = openDownloadInputStreamPipe(index);
|
||||
}
|
||||
return pipe;
|
||||
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
return openDownloadInputStreamPipe(index);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Image.ByteBufferSource getImageSource(int index) {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
if (sCache != null) {
|
||||
var snapshot = sCache.get(key);
|
||||
if (snapshot != null) {
|
||||
try {
|
||||
var file = new RandomAccessFile(snapshot.getData().toFile(), "rw");
|
||||
var chan = file.getChannel();
|
||||
var map = chan.map(FileChannel.MapMode.PRIVATE, 0, file.length());
|
||||
return new Image.ByteBufferSource() {
|
||||
@NonNull
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
chan.close();
|
||||
file.close();
|
||||
snapshot.close();
|
||||
}
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return null;
|
||||
}
|
||||
UniFile file = findImageFile(dir, index);
|
||||
if (file != null && mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
if (copyFromCacheToDownloadDir(index)) {
|
||||
file = findImageFile(dir, index);
|
||||
}
|
||||
}
|
||||
if (file instanceof RawFile rawFile) {
|
||||
try {
|
||||
var file2 = new RandomAccessFile(rawFile.mFile, "rw");
|
||||
var chan = file2.getChannel();
|
||||
var map = chan.map(FileChannel.MapMode.PRIVATE, 0, file2.length());
|
||||
return new Image.ByteBufferSource() {
|
||||
@NonNull
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
chan.close();
|
||||
file2.close();
|
||||
}
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (file != null) {
|
||||
try {
|
||||
var pfd = EhApplication.getApplication().getContentResolver().openFileDescriptor(file.getUri(), "rw");
|
||||
var map = Native.mapFd(pfd.getFd(), pfd.getStatSize());
|
||||
if (map != null) {
|
||||
return new Image.ByteBufferSource() {
|
||||
@NonNull
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
Native.unmapDirectByteBuffer(map);
|
||||
pfd.close();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
pfd.close();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.spider
|
||||
|
||||
import android.graphics.ImageDecoder
|
||||
import coil.decode.DecodeUtils
|
||||
import coil.decode.FrameDelayRewritingSource
|
||||
import coil.decode.isGif
|
||||
import coil.disk.DiskCache
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhApplication.Companion.application
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory
|
||||
import com.hippo.ehviewer.client.EhRequestBuilder
|
||||
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.image.Image.CloseableSource
|
||||
import com.hippo.unifile.RawFile
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.MathUtils
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import okhttp3.executeAsync
|
||||
import okio.buffer
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.channels.FileChannel
|
||||
import java.util.Locale
|
||||
import kotlin.io.path.readText
|
||||
|
||||
private val client = EhApplication.okHttpClient
|
||||
private val contentResolver = application.contentResolver
|
||||
|
||||
class SpiderDen(private val mGalleryInfo: GalleryInfo) {
|
||||
private val mGid: Long = mGalleryInfo.gid
|
||||
private var mDownloadDir: UniFile? = getGalleryDownloadDir(mGid)
|
||||
|
||||
@Volatile
|
||||
private var mMode = SpiderQueen.MODE_READ
|
||||
|
||||
fun setMode(@SpiderQueen.Mode mode: Int) {
|
||||
mMode = mode
|
||||
if (mode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
ensureDownloadDir()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureDownloadDir(): Boolean {
|
||||
mDownloadDir?.let { return it.ensureDir() }
|
||||
val title = getSuitableTitle(mGalleryInfo)
|
||||
val dirname = FileUtils.sanitizeFilename("$mGid-$title")
|
||||
EhDB.putDownloadDirname(mGid, dirname)
|
||||
mDownloadDir = getGalleryDownloadDir(mGid)
|
||||
return mDownloadDir?.ensureDir() ?: false
|
||||
}
|
||||
|
||||
val isReady: Boolean
|
||||
get() = when (mMode) {
|
||||
SpiderQueen.MODE_READ -> true
|
||||
SpiderQueen.MODE_DOWNLOAD -> mDownloadDir?.isDirectory ?: false
|
||||
else -> false
|
||||
}
|
||||
val downloadDir: UniFile?
|
||||
get() {
|
||||
if (mDownloadDir == null) {
|
||||
mDownloadDir = getGalleryDownloadDir(mGid)
|
||||
}
|
||||
return mDownloadDir?.takeIf { it.isDirectory }
|
||||
}
|
||||
|
||||
private fun containInCache(index: Int): Boolean {
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
return sCache[key]?.apply { close() } != null
|
||||
}
|
||||
|
||||
private fun containInDownloadDir(index: Int): Boolean {
|
||||
val dir = downloadDir ?: return false
|
||||
return findImageFile(dir, index) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extension with dot
|
||||
*/
|
||||
private fun fixExtension(extension: String): String {
|
||||
return extension.takeIf { PageLoader2.SUPPORT_IMAGE_EXTENSIONS.contains(it) }
|
||||
?: PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0]
|
||||
}
|
||||
|
||||
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
|
||||
val dir = downloadDir ?: return false
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
runCatching {
|
||||
(sCache[key] ?: return false).use { snapshot ->
|
||||
val extension = fixExtension("." + snapshot.metadata.toFile().readText())
|
||||
val file = dir.createFile(generateImageFilename(index, extension)) ?: return false
|
||||
(file.openOutputStream() as FileOutputStream).use { outputStream ->
|
||||
outputStream.channel.use { outChannel ->
|
||||
snapshot.data.toFile().inputStream().use { inputStream ->
|
||||
inputStream.channel.use {
|
||||
outChannel.transferFrom(it, 0, it.size())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
operator fun contains(index: Int): Boolean {
|
||||
return when (mMode) {
|
||||
SpiderQueen.MODE_READ -> {
|
||||
containInCache(index) || containInDownloadDir(index)
|
||||
}
|
||||
|
||||
SpiderQueen.MODE_DOWNLOAD -> {
|
||||
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFromCache(index: Int): Boolean {
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
return sCache.remove(key)
|
||||
}
|
||||
|
||||
private fun removeFromDownloadDir(index: Int): Boolean {
|
||||
return downloadDir?.let { findImageFile(it, index)?.delete() } ?: false
|
||||
}
|
||||
|
||||
fun remove(index: Int): Boolean {
|
||||
return removeFromCache(index) or removeFromDownloadDir(index)
|
||||
}
|
||||
|
||||
private fun findDownloadFileForIndex(index: Int, extension: String): UniFile? {
|
||||
val dir = downloadDir ?: return null
|
||||
val ext = fixExtension(".$extension")
|
||||
return dir.createFile(generateImageFilename(index, ext))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
suspend fun makeHttpCallAndSaveImage(
|
||||
index: Int,
|
||||
url: String,
|
||||
referer: String?,
|
||||
notifyProgress: (Long, Long, Int) -> Unit
|
||||
): Boolean {
|
||||
val call = client.newCall(EhRequestBuilder(url, referer).build())
|
||||
call.executeAsync().use { response ->
|
||||
if (response.code >= 400) return false
|
||||
val body = response.body
|
||||
val extension = body.contentType()?.subtype ?: "jpg"
|
||||
val length = body.contentLength()
|
||||
|
||||
suspend fun doSave(chan: FileChannel): Long {
|
||||
var receivedSize: Long = 0
|
||||
body.source().use { responseSource ->
|
||||
val source = if (DecodeUtils.isGif(responseSource)) FrameDelayRewritingSource(
|
||||
responseSource
|
||||
).buffer() else responseSource
|
||||
source.use {
|
||||
while (true) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
val bytesRead = chan.transferFrom(it, receivedSize, TRANSFER_BLOCK)
|
||||
receivedSize += bytesRead
|
||||
notifyProgress(body.contentLength(), receivedSize, bytesRead.toInt())
|
||||
if (bytesRead.toInt() == 0) break
|
||||
}
|
||||
}
|
||||
}
|
||||
return receivedSize
|
||||
}
|
||||
|
||||
findDownloadFileForIndex(index, extension)?.runCatching {
|
||||
openOutputStream().use { outputStream ->
|
||||
(outputStream as FileOutputStream).channel.use { return doSave(it) == length }
|
||||
}
|
||||
}?.onFailure {
|
||||
it.printStackTrace()
|
||||
return false
|
||||
}
|
||||
|
||||
// Read Mode, allow save to cache
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
val editor = sCache.edit(key) ?: return false
|
||||
var received: Long = 0
|
||||
runCatching {
|
||||
editor.metadata.toFile().writeText(extension)
|
||||
editor.data.toFile().outputStream().use { outputStream ->
|
||||
outputStream.channel.use { received = doSave(it) }
|
||||
}
|
||||
}.onFailure {
|
||||
editor.abort()
|
||||
it.printStackTrace()
|
||||
return false
|
||||
}.onSuccess {
|
||||
editor.commit()
|
||||
return received == length
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun saveToUniFile(index: Int, file: UniFile): Boolean {
|
||||
(file.openOutputStream() as FileOutputStream).use { outputStream ->
|
||||
outputStream.channel.use { outChannel ->
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
|
||||
fun copy(inputStream: FileInputStream) {
|
||||
inputStream.channel.use {
|
||||
outChannel.transferFrom(it, 0, it.size())
|
||||
}
|
||||
}
|
||||
|
||||
// Read from diskCache first
|
||||
sCache[key]?.use { snapshot ->
|
||||
runCatching {
|
||||
snapshot.data.toFile().inputStream().use { copy(it) }
|
||||
}.onSuccess {
|
||||
return true
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
downloadDir?.let { uniFile ->
|
||||
runCatching {
|
||||
findImageFile(uniFile, index)?.openInputStream()?.use {
|
||||
copy(it as FileInputStream)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
return false
|
||||
}.onSuccess {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun checkPlainText(index: Int): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
fun getExtension(index: Int): String? {
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
return sCache[key]?.use { it.metadata.toNioPath().readText() }
|
||||
?: downloadDir?.let { findImageFile(it, index) }
|
||||
?.name.let { FileUtils.getExtensionFromFilename(it) }
|
||||
}
|
||||
|
||||
fun getImageSource(index: Int): CloseableSource? {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
val snapshot: DiskCache.Snapshot? = sCache[key]
|
||||
if (snapshot != null) {
|
||||
val source = ImageDecoder.createSource(snapshot.data.toFile())
|
||||
return object : CloseableSource {
|
||||
override val source: ImageDecoder.Source
|
||||
get() = source
|
||||
|
||||
override fun close() {
|
||||
snapshot.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val dir = downloadDir ?: return null
|
||||
var file = findImageFile(dir, index)
|
||||
if (file != null && mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
if (copyFromCacheToDownloadDir(index)) {
|
||||
file = findImageFile(dir, index)
|
||||
}
|
||||
}
|
||||
if (file is RawFile) {
|
||||
val source = ImageDecoder.createSource(file.mFile)
|
||||
return object : CloseableSource {
|
||||
override val source: ImageDecoder.Source
|
||||
get() = source
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
}
|
||||
if (file != null) {
|
||||
val source = ImageDecoder.createSource(contentResolver, file.uri)
|
||||
return object : CloseableSource {
|
||||
override val source: ImageDecoder.Source
|
||||
get() = source
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TRANSFER_BLOCK: Long = 8192
|
||||
|
||||
// We use data to store image file, and metadata for image type
|
||||
private val sCache by lazy {
|
||||
DiskCache.Builder().directory(File(application.cacheDir, "gallery_image"))
|
||||
.maxSizeBytes(
|
||||
MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280).toLong() * 1024 * 1024
|
||||
).build()
|
||||
}
|
||||
|
||||
fun getGalleryDownloadDir(gid: Long): UniFile? {
|
||||
val dir = Settings.getDownloadLocation()
|
||||
// Read from DB
|
||||
var dirname = EhDB.getDownloadDirname(gid)
|
||||
return if (dir != null && dirname != null) {
|
||||
// Some dirname may be invalid in some version
|
||||
dirname = FileUtils.sanitizeFilename(dirname)
|
||||
EhDB.putDownloadDirname(gid, dirname)
|
||||
dir.subFile(dirname)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extension with dot
|
||||
*/
|
||||
fun generateImageFilename(index: Int, extension: String?): String {
|
||||
return String.format(Locale.US, "%08d%s", index + 1, extension)
|
||||
}
|
||||
|
||||
private fun findImageFile(dir: UniFile, index: Int): UniFile? {
|
||||
for (extension in PageLoader2.SUPPORT_IMAGE_EXTENSIONS) {
|
||||
val filename = generateImageFilename(index, extension)
|
||||
val file = dir.findFile(filename)
|
||||
if (file != null) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.spider;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
public class SpiderInfo {
|
||||
|
||||
static final String TOKEN_FAILED = "failed";
|
||||
private static final String TAG = SpiderInfo.class.getSimpleName();
|
||||
private static final String VERSION_STR = "VERSION";
|
||||
private static final int VERSION = 2;
|
||||
public int startPage = 0;
|
||||
public long gid = -1;
|
||||
public String token = null;
|
||||
public int pages = -1;
|
||||
public int previewPages = -1;
|
||||
public int previewPerPage = -1;
|
||||
public SparseArray<String> pTokenMap = null;
|
||||
|
||||
public static SpiderInfo read(@Nullable UniFile file) {
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = file.openInputStream();
|
||||
return read(is);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getStartPage(String str) {
|
||||
if (null == str) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int startPage = 0;
|
||||
for (int i = 0, n = str.length(); i < n; i++) {
|
||||
startPage *= 16;
|
||||
char ch = str.charAt(i);
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
startPage += ch - '0';
|
||||
} else if (ch >= 'a' && ch <= 'f') {
|
||||
startPage += ch - 'a' + 10;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.max(startPage, 0);
|
||||
}
|
||||
|
||||
private static int getVersion(String str) {
|
||||
if (null == str) {
|
||||
return -1;
|
||||
}
|
||||
if (str.startsWith(VERSION_STR)) {
|
||||
return NumberUtils.parseIntSafely(str.substring(VERSION_STR.length()), -1);
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("InfiniteLoopStatement")
|
||||
public static SpiderInfo read(@Nullable InputStream is) {
|
||||
if (null == is) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SpiderInfo spiderInfo = null;
|
||||
try {
|
||||
spiderInfo = new SpiderInfo();
|
||||
// Get version
|
||||
String line = IOUtils.readAsciiLine(is);
|
||||
int version = getVersion(line);
|
||||
if (version == VERSION) {
|
||||
// Read next line
|
||||
line = IOUtils.readAsciiLine(is);
|
||||
} else if (version == 1) {
|
||||
// pass
|
||||
} else {
|
||||
// Invalid version
|
||||
return null;
|
||||
}
|
||||
// Start page
|
||||
spiderInfo.startPage = getStartPage(line);
|
||||
// Gid
|
||||
spiderInfo.gid = Long.parseLong(IOUtils.readAsciiLine(is));
|
||||
// Token
|
||||
spiderInfo.token = IOUtils.readAsciiLine(is);
|
||||
// Deprecated, mode, skip it
|
||||
IOUtils.readAsciiLine(is);
|
||||
// Preview pages
|
||||
spiderInfo.previewPages = Integer.parseInt(IOUtils.readAsciiLine(is));
|
||||
// Preview pre page
|
||||
line = IOUtils.readAsciiLine(is);
|
||||
if (version == 1) {
|
||||
// Skip it
|
||||
} else {
|
||||
spiderInfo.previewPerPage = Integer.parseInt(line);
|
||||
}
|
||||
// Pages
|
||||
spiderInfo.pages = Integer.parseInt(IOUtils.readAsciiLine(is));
|
||||
// Check pages
|
||||
if (spiderInfo.pages <= 0) {
|
||||
return null;
|
||||
}
|
||||
// PToken
|
||||
spiderInfo.pTokenMap = new SparseArray<>(spiderInfo.pages);
|
||||
while (true) { // EOFException will raise
|
||||
line = IOUtils.readAsciiLine(is);
|
||||
int pos = line.indexOf(" ");
|
||||
if (pos > 0) {
|
||||
int index = Integer.parseInt(line.substring(0, pos));
|
||||
String pToken = line.substring(pos + 1);
|
||||
if (!TextUtils.isEmpty(pToken)) {
|
||||
spiderInfo.pTokenMap.put(index, pToken);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Can't parse index and pToken, index = " + pos);
|
||||
}
|
||||
}
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
if (spiderInfo == null || spiderInfo.gid == -1 || spiderInfo.token == null ||
|
||||
spiderInfo.pages == -1 || spiderInfo.pTokenMap == null) {
|
||||
return null;
|
||||
} else {
|
||||
return spiderInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public void write(@NonNull OutputStream os) {
|
||||
OutputStreamWriter writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(os);
|
||||
writer.write(VERSION_STR);
|
||||
writer.write(Integer.toString(VERSION));
|
||||
writer.write("\n");
|
||||
writer.write(String.format("%08x", Math.max(startPage, 0))); // Avoid negative
|
||||
writer.write("\n");
|
||||
writer.write(Long.toString(gid));
|
||||
writer.write("\n");
|
||||
writer.write(token);
|
||||
writer.write("\n");
|
||||
writer.write("1");
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(previewPages));
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(previewPerPage));
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(pages));
|
||||
writer.write("\n");
|
||||
for (int i = 0; i < pTokenMap.size(); i++) {
|
||||
Integer key = pTokenMap.keyAt(i);
|
||||
String value = pTokenMap.valueAt(i);
|
||||
if (TOKEN_FAILED.equals(value) || TextUtils.isEmpty(value)) {
|
||||
continue;
|
||||
}
|
||||
writer.write(Integer.toString(key));
|
||||
writer.write(" ");
|
||||
writer.write(value);
|
||||
writer.write("\n");
|
||||
}
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
} finally {
|
||||
IOUtils.closeQuietly(writer);
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class SpiderInfo @JvmOverloads constructor(
|
||||
val gid: Long,
|
||||
|
||||
val pages: Int,
|
||||
|
||||
val pTokenMap: MutableMap<Int, String> = hashMapOf(),
|
||||
|
||||
var startPage: Int = 0,
|
||||
|
||||
var token: String? = null,
|
||||
|
||||
var previewPages: Int = -1,
|
||||
|
||||
var previewPerPage: Int = -1
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import coil.disk.DiskCache
|
||||
import com.hippo.compat.readLegacySpiderInfo
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.unifile.UniFile
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import java.io.File
|
||||
|
||||
fun SpiderInfo.write(file: UniFile) {
|
||||
file.openOutputStream().use {
|
||||
it.write(Cbor.encodeToByteArray(this))
|
||||
}
|
||||
}
|
||||
|
||||
fun SpiderInfo.saveToCache() {
|
||||
val entry = spiderInfoCache.edit(gid.toString()) ?: return
|
||||
runCatching {
|
||||
entry.data.toFile().writeBytes(Cbor.encodeToByteArray(this))
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
entry.abort()
|
||||
}.onSuccess {
|
||||
entry.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private val spiderInfoCache by lazy {
|
||||
DiskCache.Builder()
|
||||
.directory(File(EhApplication.application.cacheDir, "spider_info_v2_1"))
|
||||
.maxSizeBytes(20 * 1024 * 1024).build()
|
||||
}
|
||||
|
||||
fun readFromCache(gid: Long): SpiderInfo? {
|
||||
val snapshot = spiderInfoCache[gid.toString()] ?: return null
|
||||
return runCatching {
|
||||
snapshot.use {
|
||||
return Cbor.decodeFromByteArray(it.data.toFile().readBytes())
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
|
||||
return runCatching {
|
||||
file.openInputStream().use {
|
||||
Cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
|
||||
}
|
||||
}.getOrNull() ?: runCatching {
|
||||
file.openInputStream().use { readLegacySpiderInfo(it) }
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
const val TOKEN_FAILED = "failed"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,331 @@
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import android.util.Log
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine.getGalleryPage
|
||||
import com.hippo.ehviewer.client.EhEngine.getGalleryPageApi
|
||||
import com.hippo.ehviewer.client.EhUrl.getPageUrl
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.DECODE_ERROR
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.ERROR_509
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.NETWORK_ERROR
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.PTOKEN_FAILED_MESSAGE
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.STATE_FAILED
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.STATE_FINISHED
|
||||
import com.hippo.image.Image.Companion.decode
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.yorozuya.StringUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
|
||||
private val spiderDen
|
||||
get() = queen.mSpiderDen
|
||||
private val spiderInfo by lazy { queen.mSpiderInfo.get() }
|
||||
private val mFetcherJobMap = hashMapOf<Int, Job>()
|
||||
private val mSemaphore = Semaphore(Settings.getMultiThreadDownload())
|
||||
private val pTokenLock = Mutex()
|
||||
private var showKey: String? = null
|
||||
private val showKeyLock = Mutex()
|
||||
private val mDownloadDelay = Settings.getDownloadDelay()
|
||||
private var isDownloadMode = false
|
||||
private val size
|
||||
get() = queen.size()
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.IO + Job()
|
||||
|
||||
fun cancelDecode(index: Int) {
|
||||
decoder.cancel(index)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun enterDownloadMode() {
|
||||
if (isDownloadMode) return
|
||||
updateRAList((0 until size).toList())
|
||||
isDownloadMode = true
|
||||
}
|
||||
|
||||
fun updateRAList(list: List<Int>, cancelBounds: Pair<Int, Int> = 0 to Int.MAX_VALUE) {
|
||||
if (isDownloadMode) return
|
||||
synchronized(mFetcherJobMap) {
|
||||
mFetcherJobMap.forEach { (i, job) ->
|
||||
if (i < cancelBounds.first || i > cancelBounds.second)
|
||||
job.cancel()
|
||||
}
|
||||
list.forEach {
|
||||
if (mFetcherJobMap[it]?.isActive != true)
|
||||
doLaunchDownloadJob(it, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doLaunchDownloadJob(index: Int, force: Boolean) {
|
||||
val state = queen.mPageStateArray[index]
|
||||
if (!force && state == STATE_FINISHED) return
|
||||
val currentJob = mFetcherJobMap[index]
|
||||
if (force) currentJob?.cancel()
|
||||
if (currentJob?.isActive != true) {
|
||||
mFetcherJobMap[index] = launch {
|
||||
mSemaphore.withPermit {
|
||||
doInJob(index, force)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun launch(index: Int, force: Boolean = false) {
|
||||
check(index in 0 until size)
|
||||
if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }
|
||||
decoder.launch(index)
|
||||
}
|
||||
|
||||
private suspend fun doInJob(index: Int, force: Boolean) {
|
||||
fun getPToken(index: Int): String? {
|
||||
if (index !in 0 until size) return null
|
||||
return spiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
|
||||
?: queen.getPTokenFromInternet(index)
|
||||
?: queen.getPTokenFromInternet(index)
|
||||
?: queen.getPTokenFromMultiPageViewer(index)
|
||||
}
|
||||
queen.updatePageState(index, SpiderQueen.STATE_DOWNLOADING)
|
||||
if (!force && index in spiderDen) {
|
||||
return queen.updatePageState(index, STATE_FINISHED)
|
||||
}
|
||||
if (force) {
|
||||
pTokenLock.withLock {
|
||||
val pToken = spiderInfo.pTokenMap[index]
|
||||
if (pToken == TOKEN_FAILED) spiderInfo.pTokenMap.remove(index)
|
||||
}
|
||||
}
|
||||
val previousPToken: String?
|
||||
val pToken: String
|
||||
|
||||
pTokenLock.withLock {
|
||||
pToken = getPToken(index) ?: return queen.updatePageState(
|
||||
index,
|
||||
STATE_FAILED,
|
||||
PTOKEN_FAILED_MESSAGE
|
||||
).also {
|
||||
spiderInfo.pTokenMap[index] = TOKEN_FAILED
|
||||
}
|
||||
previousPToken = getPToken(index - 1)
|
||||
}
|
||||
|
||||
var skipHathKey: String? = null
|
||||
val skipHathKeys = mutableListOf<String>()
|
||||
var originImageUrl: String? = null
|
||||
var error: String? = null
|
||||
var forceHtml = false
|
||||
var leakSkipHathKey = false
|
||||
repeat(3) {
|
||||
var imageUrl: String? = null
|
||||
var localShowKey: String?
|
||||
|
||||
showKeyLock.withLock {
|
||||
localShowKey = showKey
|
||||
if (localShowKey == null || forceHtml) {
|
||||
if (leakSkipHathKey) return@repeat
|
||||
var pageUrl = getPageUrl(spiderInfo.gid, index, pToken)
|
||||
// Add skipHathKey
|
||||
if (skipHathKey != null) {
|
||||
pageUrl += if ("?" in pageUrl) {
|
||||
"&nl=$skipHathKey"
|
||||
} else {
|
||||
"?nl=$skipHathKey"
|
||||
}
|
||||
}
|
||||
runSuspendCatching {
|
||||
getGalleryPage(pageUrl, spiderInfo.gid, spiderInfo.token).also {
|
||||
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
|
||||
// Get 509
|
||||
queen.notifyGet509(index)
|
||||
error = ERROR_509
|
||||
return@repeat
|
||||
}
|
||||
}
|
||||
}.onSuccess { result ->
|
||||
imageUrl = result.imageUrl
|
||||
skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() }
|
||||
originImageUrl = result.originImageUrl
|
||||
localShowKey = result.showKey
|
||||
|
||||
if (skipHathKey != null) {
|
||||
if (skipHathKey in skipHathKeys) {
|
||||
// Duplicate skip hath key
|
||||
leakSkipHathKey = true
|
||||
} else {
|
||||
skipHathKeys.add(skipHathKey!!)
|
||||
}
|
||||
} else {
|
||||
leakSkipHathKey = true
|
||||
}
|
||||
|
||||
showKey = result.showKey
|
||||
}.onFailure {
|
||||
if (it is ParseException && "Key mismatch" == it.message) {
|
||||
// Show key is wrong, enter a new loop to get the new show key
|
||||
if (showKey == localShowKey) showKey = null
|
||||
return@repeat
|
||||
} else {
|
||||
error = ExceptionUtils.getReadableString(it)
|
||||
return@repeat
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl == null) {
|
||||
if (localShowKey == null) {
|
||||
error = "ShowKey error"
|
||||
return@repeat
|
||||
}
|
||||
runSuspendCatching {
|
||||
getGalleryPageApi(
|
||||
spiderInfo.gid,
|
||||
index,
|
||||
pToken,
|
||||
localShowKey,
|
||||
previousPToken
|
||||
).also {
|
||||
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
|
||||
// Get 509
|
||||
queen.notifyGet509(index)
|
||||
error = ERROR_509
|
||||
return@repeat
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
if (it is ParseException && "Key mismatch" == it.message) {
|
||||
// Show key is wrong, enter a new loop to get the new show key
|
||||
if (showKey == localShowKey) showKey = null
|
||||
} else {
|
||||
error = ExceptionUtils.getReadableString(it)
|
||||
}
|
||||
return@repeat
|
||||
}.onSuccess {
|
||||
imageUrl = it.imageUrl
|
||||
skipHathKey = it.skipHathKey
|
||||
originImageUrl = it.originImageUrl
|
||||
}
|
||||
}
|
||||
|
||||
val targetImageUrl: String?
|
||||
val referer: String?
|
||||
|
||||
if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) {
|
||||
targetImageUrl = originImageUrl
|
||||
referer = getPageUrl(spiderInfo.gid, index, pToken)
|
||||
} else {
|
||||
targetImageUrl = imageUrl
|
||||
referer = null
|
||||
}
|
||||
if (targetImageUrl == null) {
|
||||
error = "TargetImageUrl error"
|
||||
return@repeat
|
||||
}
|
||||
Log.d(DEBUG_TAG, targetImageUrl)
|
||||
|
||||
runSuspendCatching {
|
||||
Log.d(DEBUG_TAG, "Start download image $index")
|
||||
val success: Boolean = spiderDen.makeHttpCallAndSaveImage(
|
||||
index,
|
||||
targetImageUrl,
|
||||
referer
|
||||
) { contentLength: Long, receivedSize: Long, bytesRead: Int ->
|
||||
queen.mPagePercentMap[index] = receivedSize.toFloat() / contentLength
|
||||
queen.notifyPageDownload(index, contentLength, receivedSize, bytesRead)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Log.e(DEBUG_TAG, "Can't download all of image data")
|
||||
error = "Incomplete"
|
||||
forceHtml = true
|
||||
return@repeat
|
||||
}
|
||||
|
||||
if (spiderDen.checkPlainText(index)) {
|
||||
error = ""
|
||||
forceHtml = true
|
||||
return@repeat
|
||||
}
|
||||
|
||||
Log.d(DEBUG_TAG, "Download image succeed $index")
|
||||
|
||||
queen.updatePageState(index, STATE_FINISHED)
|
||||
delay(mDownloadDelay.toLong())
|
||||
return
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
error = NETWORK_ERROR
|
||||
}
|
||||
Log.d(DEBUG_TAG, "End download image $index")
|
||||
}
|
||||
spiderDen.remove(index)
|
||||
queen.updatePageState(index, STATE_FAILED, error)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val URL_509_SUFFIX_ARRAY = arrayOf(
|
||||
"/509.gif",
|
||||
"/509s.gif"
|
||||
)
|
||||
|
||||
private const val DEBUG_TAG = "SpiderQueenWorker"
|
||||
}
|
||||
|
||||
private val decoder = Decoder()
|
||||
|
||||
inner class Decoder {
|
||||
private val mSemaphore = Semaphore(4)
|
||||
private val mDecodeJobMap = hashMapOf<Int, Job>()
|
||||
|
||||
fun cancel(index: Int) {
|
||||
synchronized(mDecodeJobMap) {
|
||||
mDecodeJobMap.remove(index)?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(index: Int) {
|
||||
synchronized(mDecodeJobMap) {
|
||||
val currentJob = mDecodeJobMap[index]
|
||||
if (currentJob?.isActive != true) {
|
||||
mDecodeJobMap[index] = launch {
|
||||
doInJob(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doInJob(index: Int) {
|
||||
val src = spiderDen.getImageSource(index) ?: run {
|
||||
delay(1000)
|
||||
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
|
||||
spiderDen.getImageSource(index)
|
||||
} ?: return
|
||||
val image = mSemaphore.withPermit { decode(src) }
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image?.recycle()
|
||||
throw it
|
||||
}
|
||||
if (image == null) {
|
||||
queen.notifyGetImageFailure(index, DECODE_ERROR)
|
||||
} else {
|
||||
queen.notifyGetImageSuccess(index, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.hippo.app.ListCheckBoxDialogBuilder;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.EhDB;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhClient;
|
||||
import com.hippo.ehviewer.client.EhRequest;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.dao.DownloadLabel;
|
||||
import com.hippo.ehviewer.download.DownloadManager;
|
||||
import com.hippo.ehviewer.download.DownloadService;
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.collect.LongList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class CommonOperations {
|
||||
|
||||
private static void doAddToFavorites(Activity activity, GalleryInfo galleryInfo,
|
||||
int slot, EhClient.Callback<Void> listener) {
|
||||
if (slot == -1) {
|
||||
EhDB.putLocalFavorites(galleryInfo);
|
||||
listener.onSuccess(null);
|
||||
} else if (slot >= 0 && slot <= 9) {
|
||||
EhClient client = EhClient.INSTANCE;
|
||||
EhRequest request = new EhRequest();
|
||||
request.setMethod(EhClient.METHOD_ADD_FAVORITES);
|
||||
request.setArgs(galleryInfo.getGid(), galleryInfo.getToken(), slot, "");
|
||||
request.setCallback(listener);
|
||||
request.enqueue(activity);
|
||||
} else {
|
||||
listener.onFailure(new Exception()); // TODO Add text
|
||||
}
|
||||
}
|
||||
|
||||
public static void addToFavorites(final Activity activity, final GalleryInfo galleryInfo,
|
||||
final EhClient.Callback<Void> listener) {
|
||||
addToFavorites(activity, galleryInfo, listener, false);
|
||||
}
|
||||
|
||||
public static void addToFavorites(final Activity activity, final GalleryInfo galleryInfo,
|
||||
final EhClient.Callback<Void> listener, boolean select) {
|
||||
int slot = Settings.getDefaultFavSlot();
|
||||
String[] items = new String[11];
|
||||
items[0] = activity.getString(R.string.local_favorites);
|
||||
String[] favCat = Settings.getFavCat();
|
||||
System.arraycopy(favCat, 0, items, 1, 10);
|
||||
if (!select && slot >= -1 && slot <= 9) {
|
||||
String newFavoriteName = slot >= 0 ? items[slot + 1] : null;
|
||||
doAddToFavorites(activity, galleryInfo, slot, new DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot));
|
||||
} else {
|
||||
new ListCheckBoxDialogBuilder(activity, items,
|
||||
(builder, dialog, position) -> {
|
||||
int slot1 = position - 1;
|
||||
String newFavoriteName = (slot1 >= 0 && slot1 <= 9) ? items[slot1 + 1] : null;
|
||||
doAddToFavorites(activity, galleryInfo, slot1, new DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot1));
|
||||
if (builder.isChecked()) {
|
||||
Settings.putDefaultFavSlot(slot1);
|
||||
} else {
|
||||
Settings.putDefaultFavSlot(Settings.INVALID_DEFAULT_FAV_SLOT);
|
||||
}
|
||||
}, activity.getString(R.string.remember_favorite_collection), false)
|
||||
.setTitle(R.string.add_favorites_dialog_title)
|
||||
.setOnCancelListener(dialog -> listener.onCancel())
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeFromFavorites(Activity activity, GalleryInfo galleryInfo,
|
||||
final EhClient.Callback<Void> listener) {
|
||||
EhDB.removeLocalFavorites(galleryInfo.getGid());
|
||||
EhClient client = EhClient.INSTANCE;
|
||||
EhRequest request = new EhRequest();
|
||||
request.setMethod(EhClient.METHOD_ADD_FAVORITES);
|
||||
request.setArgs(galleryInfo.getGid(), galleryInfo.getToken(), -1, "");
|
||||
request.setCallback(new DelegateFavoriteCallback(listener, galleryInfo, null, -2));
|
||||
request.enqueue(activity);
|
||||
}
|
||||
|
||||
public static void startDownload(final MainActivity activity, final GalleryInfo galleryInfo, boolean forceDefault) {
|
||||
startDownload(activity, Collections.singletonList(galleryInfo), forceDefault);
|
||||
}
|
||||
|
||||
public static void startDownload(final MainActivity activity, final List<GalleryInfo> galleryInfos, boolean forceDefault) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Objects.requireNonNull(EhApplication.getApplication().getTopActivity()).checkAndRequestNotificationPermission();
|
||||
}
|
||||
doStartDownload(activity, galleryInfos, forceDefault);
|
||||
}
|
||||
|
||||
private static void doStartDownload(final MainActivity activity, final List<GalleryInfo> galleryInfos, boolean forceDefault) {
|
||||
final DownloadManager dm = EhApplication.getDownloadManager();
|
||||
|
||||
LongList toStart = new LongList();
|
||||
List<GalleryInfo> toAdd = new ArrayList<>();
|
||||
for (GalleryInfo gi : galleryInfos) {
|
||||
if (dm.containDownloadInfo(gi.getGid())) {
|
||||
toStart.add(gi.getGid());
|
||||
} else {
|
||||
toAdd.add(gi);
|
||||
}
|
||||
}
|
||||
|
||||
if (!toStart.isEmpty()) {
|
||||
Intent intent = new Intent(activity, DownloadService.class);
|
||||
intent.setAction(DownloadService.ACTION_START_RANGE);
|
||||
intent.putExtra(DownloadService.KEY_GID_LIST, toStart);
|
||||
ContextCompat.startForegroundService(activity, intent);
|
||||
}
|
||||
|
||||
if (toAdd.isEmpty()) {
|
||||
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean justStart = forceDefault;
|
||||
String label = null;
|
||||
// Get default download label
|
||||
if (!justStart && Settings.getHasDefaultDownloadLabel()) {
|
||||
label = Settings.getDefaultDownloadLabel();
|
||||
justStart = label == null || dm.containLabel(label);
|
||||
}
|
||||
// If there is no other label, just use null label
|
||||
if (!justStart && 0 == dm.getLabelList().size()) {
|
||||
justStart = true;
|
||||
label = null;
|
||||
}
|
||||
|
||||
if (justStart) {
|
||||
// Got default label
|
||||
for (GalleryInfo gi : toAdd) {
|
||||
Intent intent = new Intent(activity, DownloadService.class);
|
||||
intent.setAction(DownloadService.ACTION_START);
|
||||
intent.putExtra(DownloadService.KEY_LABEL, label);
|
||||
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi);
|
||||
ContextCompat.startForegroundService(activity, intent);
|
||||
}
|
||||
// Notify
|
||||
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT);
|
||||
} else {
|
||||
// Let use chose label
|
||||
List<DownloadLabel> list = dm.getLabelList();
|
||||
final String[] items = new String[list.size() + 1];
|
||||
items[0] = activity.getString(R.string.default_download_label_name);
|
||||
for (int i = 0, n = list.size(); i < n; i++) {
|
||||
items[i + 1] = list.get(i).getLabel();
|
||||
}
|
||||
|
||||
new ListCheckBoxDialogBuilder(activity, items,
|
||||
(builder, dialog, position) -> {
|
||||
String label1;
|
||||
if (position == 0) {
|
||||
label1 = null;
|
||||
} else {
|
||||
label1 = items[position];
|
||||
if (!dm.containLabel(label1)) {
|
||||
label1 = null;
|
||||
}
|
||||
}
|
||||
// Start download
|
||||
for (GalleryInfo gi : toAdd) {
|
||||
Intent intent = new Intent(activity, DownloadService.class);
|
||||
intent.setAction(DownloadService.ACTION_START);
|
||||
intent.putExtra(DownloadService.KEY_LABEL, label1);
|
||||
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi);
|
||||
ContextCompat.startForegroundService(activity, intent);
|
||||
}
|
||||
// Save settings
|
||||
if (builder.isChecked()) {
|
||||
Settings.putHasDefaultDownloadLabel(true);
|
||||
Settings.putDefaultDownloadLabel(label1);
|
||||
} else {
|
||||
Settings.putHasDefaultDownloadLabel(false);
|
||||
}
|
||||
// Notify
|
||||
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT);
|
||||
}, activity.getString(R.string.remember_download_label), false)
|
||||
.setTitle(R.string.download)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ensureNoMediaFile(UniFile file) {
|
||||
if (null == file) {
|
||||
return;
|
||||
}
|
||||
|
||||
UniFile noMedia = file.createFile(".nomedia");
|
||||
if (null == noMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = noMedia.openInputStream();
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeNoMediaFile(UniFile file) {
|
||||
if (null == file) {
|
||||
return;
|
||||
}
|
||||
|
||||
UniFile noMedia = file.subFile(".nomedia");
|
||||
if (null != noMedia && noMedia.isFile()) {
|
||||
noMedia.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DelegateFavoriteCallback implements EhClient.Callback<Void> {
|
||||
|
||||
private final EhClient.Callback<Void> delegate;
|
||||
private final GalleryInfo info;
|
||||
private final String newFavoriteName;
|
||||
private final int slot;
|
||||
|
||||
DelegateFavoriteCallback(EhClient.Callback<Void> delegate, GalleryInfo info,
|
||||
String newFavoriteName, int slot) {
|
||||
this.delegate = delegate;
|
||||
this.info = info;
|
||||
this.newFavoriteName = newFavoriteName;
|
||||
this.slot = slot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
info.setFavoriteName(newFavoriteName);
|
||||
info.setFavoriteSlot(slot);
|
||||
delegate.onSuccess(result);
|
||||
EhApplication.getFavouriteStatusRouter().modifyFavourites(info.getGid(), slot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
delegate.onFailure(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
delegate.onCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.ehviewer.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.hippo.app.ListCheckBoxDialogBuilder
|
||||
import com.hippo.ehviewer.EhApplication.Companion.application
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhClient
|
||||
import com.hippo.ehviewer.client.EhRequest
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.download.DownloadService
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import com.hippo.yorozuya.collect.LongList
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
object CommonOperations {
|
||||
private fun doAddToFavorites(
|
||||
activity: Activity, galleryInfo: GalleryInfo,
|
||||
slot: Int, listener: EhClient.Callback<Void?>
|
||||
) {
|
||||
when (slot) {
|
||||
-1 -> {
|
||||
EhDB.putLocalFavorites(galleryInfo)
|
||||
listener.onSuccess(null)
|
||||
}
|
||||
|
||||
in 0..9 -> {
|
||||
val request = EhRequest()
|
||||
request.setMethod(EhClient.METHOD_ADD_FAVORITES)
|
||||
request.setArgs(galleryInfo.gid, galleryInfo.token, slot, "")
|
||||
request.setCallback(listener)
|
||||
request.enqueue(activity)
|
||||
}
|
||||
|
||||
else -> {
|
||||
listener.onFailure(Exception()) // TODO Add text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun addToFavorites(
|
||||
activity: Activity, galleryInfo: GalleryInfo,
|
||||
listener: EhClient.Callback<Void?>, select: Boolean = false
|
||||
) {
|
||||
val slot = Settings.getDefaultFavSlot()
|
||||
val localFav = activity.getString(R.string.local_favorites)
|
||||
val items = Settings.getFavCat().toMutableList().apply { add(0, localFav) }
|
||||
if (!select && slot >= -1 && slot <= 9) {
|
||||
val newFavoriteName = if (slot >= 0) items[slot + 1] else null
|
||||
doAddToFavorites(
|
||||
activity,
|
||||
galleryInfo,
|
||||
slot,
|
||||
DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot)
|
||||
)
|
||||
} else {
|
||||
ListCheckBoxDialogBuilder(
|
||||
activity, items,
|
||||
{ builder: ListCheckBoxDialogBuilder?, _: AlertDialog?, position: Int ->
|
||||
val slot1 = position - 1
|
||||
val newFavoriteName = if (slot1 in 0..9) items[slot1 + 1] else null
|
||||
doAddToFavorites(
|
||||
activity,
|
||||
galleryInfo,
|
||||
slot1,
|
||||
DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot1)
|
||||
)
|
||||
if (builder?.isChecked == true) {
|
||||
Settings.putDefaultFavSlot(slot1)
|
||||
} else {
|
||||
Settings.putDefaultFavSlot(Settings.INVALID_DEFAULT_FAV_SLOT)
|
||||
}
|
||||
}, activity.getString(R.string.remember_favorite_collection), false
|
||||
)
|
||||
.setTitle(R.string.add_favorites_dialog_title)
|
||||
.setOnCancelListener { listener.onCancel() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromFavorites(
|
||||
activity: Activity?, galleryInfo: GalleryInfo,
|
||||
listener: EhClient.Callback<Void?>
|
||||
) {
|
||||
EhDB.removeLocalFavorites(galleryInfo.gid)
|
||||
val request = EhRequest()
|
||||
request.setMethod(EhClient.METHOD_ADD_FAVORITES)
|
||||
request.setArgs(galleryInfo.gid, galleryInfo.token, -1, "")
|
||||
request.setCallback(DelegateFavoriteCallback(listener, galleryInfo, null, -2))
|
||||
request.enqueue(activity!!)
|
||||
}
|
||||
|
||||
fun startDownload(activity: MainActivity?, galleryInfo: GalleryInfo, forceDefault: Boolean) {
|
||||
startDownload(activity!!, listOf(galleryInfo), forceDefault)
|
||||
}
|
||||
|
||||
fun startDownload(
|
||||
activity: MainActivity,
|
||||
galleryInfos: List<GalleryInfo>,
|
||||
forceDefault: Boolean
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
application.topActivity?.checkAndRequestNotificationPermission()
|
||||
}
|
||||
doStartDownload(activity, galleryInfos, forceDefault)
|
||||
}
|
||||
|
||||
private fun doStartDownload(
|
||||
activity: MainActivity,
|
||||
galleryInfos: List<GalleryInfo>,
|
||||
forceDefault: Boolean
|
||||
) {
|
||||
val dm = downloadManager
|
||||
val toStart = LongList()
|
||||
val toAdd: MutableList<GalleryInfo> = ArrayList()
|
||||
for (gi in galleryInfos) {
|
||||
if (dm.containDownloadInfo(gi.gid)) {
|
||||
toStart.add(gi.gid)
|
||||
} else {
|
||||
toAdd.add(gi)
|
||||
}
|
||||
}
|
||||
if (!toStart.isEmpty()) {
|
||||
val intent = Intent(activity, DownloadService::class.java)
|
||||
intent.action = DownloadService.ACTION_START_RANGE
|
||||
intent.putExtra(DownloadService.KEY_GID_LIST, toStart)
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
}
|
||||
if (toAdd.isEmpty()) {
|
||||
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)
|
||||
return
|
||||
}
|
||||
var justStart = forceDefault
|
||||
var label: String? = null
|
||||
// Get default download label
|
||||
if (!justStart && Settings.getHasDefaultDownloadLabel()) {
|
||||
label = Settings.getDefaultDownloadLabel()
|
||||
justStart = label == null || dm.containLabel(label)
|
||||
}
|
||||
// If there is no other label, just use null label
|
||||
if (!justStart && dm.labelList.isEmpty()) {
|
||||
justStart = true
|
||||
label = null
|
||||
}
|
||||
if (justStart) {
|
||||
// Got default label
|
||||
for (gi in toAdd) {
|
||||
val intent = Intent(activity, DownloadService::class.java)
|
||||
intent.action = DownloadService.ACTION_START
|
||||
intent.putExtra(DownloadService.KEY_LABEL, label)
|
||||
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
}
|
||||
// Notify
|
||||
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)
|
||||
} else {
|
||||
// Let use chose label
|
||||
val list = dm.labelList
|
||||
val items = mutableListOf<String>()
|
||||
items.add(activity.getString(R.string.default_download_label_name))
|
||||
items.addAll(list.mapNotNull { it.label })
|
||||
ListCheckBoxDialogBuilder(
|
||||
activity, items,
|
||||
{ builder: ListCheckBoxDialogBuilder?, _: AlertDialog?, position: Int ->
|
||||
var label1: String?
|
||||
if (position == 0) {
|
||||
label1 = null
|
||||
} else {
|
||||
label1 = items[position]
|
||||
if (!dm.containLabel(label1)) {
|
||||
label1 = null
|
||||
}
|
||||
}
|
||||
// Start download
|
||||
for (gi in toAdd) {
|
||||
val intent = Intent(activity, DownloadService::class.java)
|
||||
intent.action = DownloadService.ACTION_START
|
||||
intent.putExtra(DownloadService.KEY_LABEL, label1)
|
||||
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
}
|
||||
// Save settings
|
||||
if (builder?.isChecked == true) {
|
||||
Settings.putHasDefaultDownloadLabel(true)
|
||||
Settings.putDefaultDownloadLabel(label1)
|
||||
} else {
|
||||
Settings.putHasDefaultDownloadLabel(false)
|
||||
}
|
||||
// Notify
|
||||
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)
|
||||
}, activity.getString(R.string.remember_download_label), false
|
||||
)
|
||||
.setTitle(R.string.download)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun ensureNoMediaFile(file: UniFile?) {
|
||||
if (null == file) {
|
||||
return
|
||||
}
|
||||
val noMedia = file.createFile(".nomedia") ?: return
|
||||
var `is`: InputStream? = null
|
||||
try {
|
||||
`is` = noMedia.openInputStream()
|
||||
} catch (e: IOException) {
|
||||
// Ignore
|
||||
} finally {
|
||||
IOUtils.closeQuietly(`is`)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun removeNoMediaFile(file: UniFile?) {
|
||||
if (null == file) {
|
||||
return
|
||||
}
|
||||
val noMedia = file.subFile(".nomedia")
|
||||
if (null != noMedia && noMedia.isFile) {
|
||||
noMedia.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private class DelegateFavoriteCallback(
|
||||
private val delegate: EhClient.Callback<Void?>, private val info: GalleryInfo,
|
||||
private val newFavoriteName: String?, private val slot: Int
|
||||
) : EhClient.Callback<Void?> {
|
||||
override fun onSuccess(result: Void?) {
|
||||
info.favoriteName = newFavoriteName
|
||||
info.favoriteSlot = slot
|
||||
delegate.onSuccess(result)
|
||||
favouriteStatusRouter.modifyFavourites(info.gid, slot)
|
||||
}
|
||||
|
||||
override fun onFailure(e: Exception) {
|
||||
delegate.onFailure(e)
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
delegate.onCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.content.pm.PackageManager
|
||||
import android.content.pm.verify.domain.DomainVerificationManager
|
||||
import android.content.pm.verify.domain.DomainVerificationUserState
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
@@ -35,6 +36,7 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
@@ -69,11 +71,10 @@ import com.hippo.ehviewer.ui.scene.GalleryDetailScene
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
|
||||
import com.hippo.ehviewer.ui.scene.ProgressScene
|
||||
import com.hippo.io.UniFileInputStreamPipe
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.util.BitmapUtils
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.util.addTextToClipboard
|
||||
import com.hippo.util.getClipboardManager
|
||||
import com.hippo.util.getParcelableExtraCompat
|
||||
import com.hippo.util.getUrlFromClipboard
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
@@ -91,25 +92,11 @@ class MainActivity : EhActivity() {
|
||||
private lateinit var connectivityManager: ConnectivityManager
|
||||
private val availableNetworks: MutableList<Network> = mutableListOf()
|
||||
|
||||
private fun saveImageToTempFile(file: UniFile?): File? {
|
||||
file ?: return null
|
||||
var bitmap: Bitmap? = null
|
||||
try {
|
||||
bitmap = BitmapUtils.decodeStream(
|
||||
UniFileInputStreamPipe(file),
|
||||
-1,
|
||||
-1,
|
||||
500 * 500,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
// Ignore
|
||||
}
|
||||
if (null == bitmap) {
|
||||
return null
|
||||
}
|
||||
private fun saveImageToTempFile(uri: Uri): File? {
|
||||
val src = ImageDecoder.createSource(contentResolver, uri)
|
||||
val bitmap = runCatching {
|
||||
ImageDecoder.decodeBitmap(src, Image.imageSearchDecoderSampleListener)
|
||||
}.getOrNull() ?: return null
|
||||
val temp = AppConfig.createTempFile() ?: return null
|
||||
var os: OutputStream? = null
|
||||
return try {
|
||||
@@ -198,10 +185,9 @@ class MainActivity : EhActivity() {
|
||||
)
|
||||
return true
|
||||
} else if (type != null && type.startsWith("image/")) {
|
||||
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
||||
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
||||
if (null != uri) {
|
||||
val file = UniFile.fromUri(this, uri)
|
||||
val temp = saveImageToTempFile(file)
|
||||
val temp = saveImageToTempFile(uri)
|
||||
if (null != temp) {
|
||||
val builder = ListUrlBuilder()
|
||||
builder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
|
||||
@@ -223,19 +209,7 @@ class MainActivity : EhActivity() {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
connectivityManager = getSystemService()!!
|
||||
if (Settings.getDF() && Settings.getBypassVPN()) {
|
||||
bypassVPN()
|
||||
}
|
||||
if (EhUtils.needSignedIn())
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||
navController = navHostFragment.navController
|
||||
private fun setNavGraph() {
|
||||
navController.apply {
|
||||
graph = navInflater.inflate(R.navigation.nav_graph).apply {
|
||||
when (Settings.getLaunchPageGalleryListSceneAction()) {
|
||||
@@ -246,6 +220,26 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
connectivityManager = getSystemService()!!
|
||||
if (Settings.getDF() && Settings.getBypassVpn()) {
|
||||
bypassVpn()
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||
navController = navHostFragment.navController
|
||||
if (EhUtils.needSignedIn()) {
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
setNavGraph()
|
||||
}.launch(Intent(this, LoginActivity::class.java))
|
||||
} else {
|
||||
setNavGraph()
|
||||
}
|
||||
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
|
||||
binding.navView.setupWithNavController(navController)
|
||||
|
||||
@@ -287,7 +281,7 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun bypassVPN() {
|
||||
private fun bypassVpn() {
|
||||
val network = connectivityManager.activeNetwork
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
capabilities?.let {
|
||||
|
||||
@@ -20,20 +20,38 @@ import android.view.MenuItem
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.ui.fragment.SettingsFragment
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
|
||||
class SettingsActivity : EhActivity() {
|
||||
private var mFab: FloatingActionButton? = null
|
||||
private var mContentText: TextView? = null
|
||||
private var mAppbarLayout: AppBarLayout? = null
|
||||
var mLimits: HomeParser.Limits? = null
|
||||
private set
|
||||
var mFunds: HomeParser.Funds? = null
|
||||
private set
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (EhCookieStore.hasSignedIn()) {
|
||||
lifecycleScope.launchIO {
|
||||
runCatching {
|
||||
getImageLimits()
|
||||
}.exceptionOrNull()?.printStackTrace()
|
||||
}
|
||||
}
|
||||
setContentView(R.layout.activity_preference)
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
mContentText = findViewById(R.id.tip)
|
||||
@@ -49,6 +67,20 @@ class SettingsActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getImageLimits() = EhEngine.getImageLimits().run {
|
||||
mLimits = limits
|
||||
mFunds = funds
|
||||
}
|
||||
|
||||
suspend fun resetImageLimits() {
|
||||
mLimits =
|
||||
EhEngine.resetImageLimits() ?: HomeParser.Limits(maximum = mLimits!!.maximum).also {
|
||||
withUIContext {
|
||||
showTip(R.string.reset_limits_succeed, BaseScene.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showTip(@StringRes id: Int, length: Int) {
|
||||
showTip(getString(id), length)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ import java.io.FileOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.math.ceil
|
||||
import com.hippo.ehviewer.Settings as AppSettings
|
||||
|
||||
class AdvancedFragment : BasePreferenceFragment() {
|
||||
private var exportLauncher = registerForActivityResult<String, Uri>(
|
||||
@@ -235,6 +236,8 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
val exportData = findPreference<Preference>(KEY_EXPORT_DATA)
|
||||
val backupFavorite = findPreference<Preference>(KEY_BACKUP_FAVORITE)
|
||||
val openByDefault = findPreference<Preference>(KEY_OPEN_BY_DEFAULT)
|
||||
val domainFronting = findPreference<Preference>(AppSettings.KEY_DOMAIN_FRONTING)
|
||||
val bypassVpn = findPreference<Preference>(AppSettings.KEY_BYPASS_VPN)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
openByDefault!!.isVisible = false
|
||||
} else {
|
||||
@@ -245,6 +248,11 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
exportData!!.onPreferenceClickListener = this
|
||||
backupFavorite!!.onPreferenceClickListener = this
|
||||
appLanguage!!.onPreferenceChangeListener = this
|
||||
bypassVpn!!.isVisible = AppSettings.getDF()
|
||||
domainFronting!!.setOnPreferenceChangeListener { _, newValue ->
|
||||
bypassVpn.isVisible = newValue as Boolean
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference): Boolean {
|
||||
@@ -336,7 +344,7 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
if (result.next != null) {
|
||||
try {
|
||||
runBlocking {
|
||||
delay(com.hippo.ehviewer.Settings.getDownloadDelay().toLong())
|
||||
delay(AppSettings.getDownloadDelay().toLong())
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
|
||||
@@ -20,14 +20,16 @@ import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
|
||||
class EhFragment : BasePreferenceFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@@ -42,7 +44,7 @@ class EhFragment : BasePreferenceFragment() {
|
||||
val thumbSize = findPreference<Preference>(Settings.KEY_THUMB_SIZE)
|
||||
val thumbResolution = findPreference<Preference>(Settings.KEY_THUMB_RESOLUTION)
|
||||
val showTagTranslations = findPreference<Preference>(Settings.KEY_SHOW_TAG_TRANSLATIONS)
|
||||
val tagTranslationsSource = findPreference<Preference>("tag_translations_source")
|
||||
val tagTranslationsSource = findPreference<Preference>(Settings.KEY_TAG_TRANSLATIONS_SOURCE)
|
||||
Settings.getDisplayName()?.let { account?.summary = it }
|
||||
theme!!.onPreferenceChangeListener = this
|
||||
gallerySite!!.onPreferenceChangeListener = this
|
||||
@@ -59,6 +61,12 @@ class EhFragment : BasePreferenceFragment() {
|
||||
preferenceScreen.removePreference(showTagTranslations)
|
||||
preferenceScreen.removePreference(tagTranslationsSource!!)
|
||||
}
|
||||
if (!EhCookieStore.hasSignedIn()) {
|
||||
Settings.SIGN_IN_REQUIRED.forEach {
|
||||
val preference = findPreference<Preference>(it)
|
||||
preferenceScreen.removePreference(preference!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
@@ -69,7 +77,13 @@ class EhFragment : BasePreferenceFragment() {
|
||||
return true
|
||||
} else if (Settings.KEY_GALLERY_SITE == key) {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
launchIO { runCatching { EhEngine.getUConfig() } }
|
||||
lifecycleScope.launchNonCancellable {
|
||||
runCatching {
|
||||
EhEngine.getUConfig()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else if (Settings.KEY_LIST_MODE == key) {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
|
||||
@@ -378,7 +378,7 @@ class FilterFragment : BaseFragment() {
|
||||
}
|
||||
size = mTagNamespaceFilterList.size
|
||||
if (0 != size) {
|
||||
if (index1 == 0) {
|
||||
index1 -= if (index1 == 0) {
|
||||
return tagNamespaceHeader
|
||||
} else if (index1 <= size) {
|
||||
return mTagNamespaceFilterList[index1 - 1]
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -46,7 +45,6 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
@@ -55,15 +53,16 @@ import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Cookie
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CookieSignInScene(navController: NavController) {
|
||||
fun CookieSignInScene() {
|
||||
val navController = LocalNavController.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
@@ -92,12 +91,11 @@ fun CookieSignInScene(navController: NavController) {
|
||||
fun storeCookie(id: String, hash: String, igneous: String) {
|
||||
EhUtils.signOut()
|
||||
val store = EhApplication.ehCookieStore
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_EX))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_EX))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_EX))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_EX))
|
||||
if (igneous.isBlank()) return
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX))
|
||||
}
|
||||
|
||||
@@ -122,6 +120,9 @@ fun CookieSignInScene(navController: NavController) {
|
||||
runCatching {
|
||||
EhEngine.getProfile()
|
||||
}.onSuccess {
|
||||
withNonCancellableContext {
|
||||
postLogin()
|
||||
}
|
||||
withUIContext {
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,24 @@ package com.hippo.ehviewer.ui.login
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.ui.EhActivity
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LoginActivity : EhActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -16,22 +28,29 @@ class LoginActivity : EhActivity() {
|
||||
Mdc3Theme {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = SIGN_IN_ROUTE_NAME) {
|
||||
composable(SIGN_IN_ROUTE_NAME) {
|
||||
SignInScreen(navController)
|
||||
}
|
||||
CompositionLocalProvider(LocalNavController provides navController) {
|
||||
NavHost(navController = navController, startDestination = SIGN_IN_ROUTE_NAME) {
|
||||
composable(SIGN_IN_ROUTE_NAME) {
|
||||
SignInScreen()
|
||||
}
|
||||
|
||||
composable(WEBVIEW_SIGN_IN_ROUTE_NAME) {
|
||||
WebviewSignInScreen(navController)
|
||||
}
|
||||
composable(WEBVIEW_SIGN_IN_ROUTE_NAME) {
|
||||
WebviewSignInScreen()
|
||||
}
|
||||
|
||||
composable(COOKIE_SIGN_IN_ROUTE_NAME) {
|
||||
CookieSignInScene(navController)
|
||||
}
|
||||
composable(COOKIE_SIGN_IN_ROUTE_NAME) {
|
||||
CookieSignInScene()
|
||||
}
|
||||
|
||||
composable(SELECT_SITE_ROUTE_NAME) {
|
||||
SelectSiteScreen {
|
||||
finish()
|
||||
composable(SELECT_SITE_ROUTE_NAME) {
|
||||
val selectSite by remember { mutableStateOf(Settings.getSelectSite()) }
|
||||
if (selectSite) {
|
||||
SelectSiteScreen()
|
||||
} else {
|
||||
SideEffect {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,10 +60,43 @@ class LoginActivity : EhActivity() {
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
Settings.putNeedSignIn(false)
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun postLogin() = coroutineScope {
|
||||
launch {
|
||||
runCatching {
|
||||
EhEngine.getProfile().run {
|
||||
Settings.putDisplayName(displayName)
|
||||
Settings.putAvatar(avatar)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
runCatching {
|
||||
Settings.putGallerySite(EhUrl.SITE_EX)
|
||||
EhEngine.getUConfig()
|
||||
}.onFailure {
|
||||
Settings.putSelectSite(false)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
launch {
|
||||
runCatching {
|
||||
EhEngine.getUConfig()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.join()
|
||||
}
|
||||
|
||||
val LocalNavController =
|
||||
compositionLocalOf<NavController> { error("CompositionLocal LocalNavController not present!") }
|
||||
|
||||
const val SIGN_IN_ROUTE_NAME = "SignIn"
|
||||
const val WEBVIEW_SIGN_IN_ROUTE_NAME = "WebViewSignIn"
|
||||
const val COOKIE_SIGN_IN_ROUTE_NAME = "CookieSignIn"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -24,24 +25,26 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
|
||||
@Composable
|
||||
fun SelectSiteScreen(finish: () -> Unit) {
|
||||
var site by remember { mutableStateOf(EhApplication.ehCookieStore.hasSignedIn()) } // false for e, true for ex
|
||||
fun SelectSiteScreen() {
|
||||
val activity = LocalContext.current as Activity
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var siteEx by remember { mutableStateOf(true) }
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
@@ -70,8 +73,8 @@ fun SelectSiteScreen(finish: () -> Unit) {
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(if (!site) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.clickable { site = false }
|
||||
.background(if (!siteEx) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.clickable { siteEx = false }
|
||||
.size(100.dp, 40.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -89,8 +92,8 @@ fun SelectSiteScreen(finish: () -> Unit) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(if (site) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.clickable { site = true }
|
||||
.background(if (siteEx) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.clickable { siteEx = true }
|
||||
.size(100.dp, 40.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -116,28 +119,23 @@ fun SelectSiteScreen(finish: () -> Unit) {
|
||||
) {
|
||||
Button(onClick = {
|
||||
Settings.putSelectSite(false)
|
||||
Settings.putNeedSignIn(false)
|
||||
if (!site)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
else
|
||||
Settings.putGallerySite(EhUrl.SITE_EX)
|
||||
GlobalScope.launchIO {
|
||||
getProfile()
|
||||
coroutineScope.launchNonCancellable {
|
||||
runCatching {
|
||||
if (!siteEx) {
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
EhEngine.getUConfig()
|
||||
} else {
|
||||
// Get cookies for image limits
|
||||
EhEngine.getUConfig(EhUrl.URL_UCONFIG_E)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
finish()
|
||||
activity.finish()
|
||||
}, Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getProfile() {
|
||||
runCatching {
|
||||
EhEngine.getProfile().run {
|
||||
Settings.putDisplayName(displayName)
|
||||
Settings.putAvatar(avatar)
|
||||
}
|
||||
EhEngine.getUConfig()
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -51,21 +50,22 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.UrlOpener
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import rikka.core.util.ContextUtils.requireActivity
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SignInScreen(navController: NavController) {
|
||||
fun SignInScreen() {
|
||||
val navController = LocalNavController.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
var isProgressIndicatorVisible by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -110,6 +110,9 @@ fun SignInScreen(navController: NavController) {
|
||||
showErrorDialog = true
|
||||
}
|
||||
}.onSuccess {
|
||||
withNonCancellableContext {
|
||||
postLogin()
|
||||
}
|
||||
withUIContext {
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
}
|
||||
@@ -248,7 +251,11 @@ fun SignInScreen(navController: NavController) {
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = { navController.navigate(SELECT_SITE_ROUTE_NAME) },
|
||||
onClick = {
|
||||
Settings.putSelectSite(false)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
|
||||
@@ -7,22 +7,27 @@ import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.navigation.NavController
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.widget.DialogWebChromeClient
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun WebviewSignInScreen(navController: NavController) {
|
||||
fun WebviewSignInScreen() {
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
AndroidView(factory = {
|
||||
EhUtils.signOut()
|
||||
|
||||
@@ -86,9 +91,9 @@ fun WebviewSignInScreen(navController: NavController) {
|
||||
var getId = false
|
||||
var getHash = false
|
||||
for (cookie in cookies) {
|
||||
if (EhCookieStore.KEY_IPD_MEMBER_ID == cookie.name) {
|
||||
if (EhCookieStore.KEY_IPB_MEMBER_ID == cookie.name) {
|
||||
getId = true
|
||||
} else if (EhCookieStore.KEY_IPD_PASS_HASH == cookie.name) {
|
||||
} else if (EhCookieStore.KEY_IPB_PASS_HASH == cookie.name) {
|
||||
getHash = true
|
||||
}
|
||||
addCookie(EhUrl.DOMAIN_EX, cookie)
|
||||
@@ -96,7 +101,14 @@ fun WebviewSignInScreen(navController: NavController) {
|
||||
}
|
||||
if (getId && getHash) {
|
||||
present = true
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
coroutineScope.launchIO {
|
||||
withNonCancellableContext {
|
||||
postLogin()
|
||||
}
|
||||
withUIContext {
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
@@ -91,7 +90,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
---------------*/
|
||||
private var mDownloadManager: DownloadManager? = null
|
||||
private var mLabel: String? = null
|
||||
private var mList: MutableList<DownloadInfo>? = null
|
||||
private var mList: List<DownloadInfo>? = null
|
||||
|
||||
/*---------------
|
||||
View life cycle
|
||||
@@ -115,8 +114,8 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
// Add "All" and "Default" label names
|
||||
mLabels.add(getString(R.string.download_all))
|
||||
mLabels.add(getString(R.string.default_download_label_name))
|
||||
for ((_, label) in listLabel) {
|
||||
mLabels.add(label!!)
|
||||
listLabel.forEach {
|
||||
mLabels.add(it.label!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,11 +178,12 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
?: Log.e(TAG, "Can't removeDownloadInfoListener")
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun updateForLabel() {
|
||||
if (null == mDownloadManager) {
|
||||
return
|
||||
}
|
||||
var list: MutableList<DownloadInfo>?
|
||||
var list: List<DownloadInfo>?
|
||||
if (mLabel == null) {
|
||||
list = mDownloadManager!!.allDownloadInfoList
|
||||
} else if (mLabel == getString(R.string.default_download_label_name)) {
|
||||
@@ -195,15 +195,10 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
list = mDownloadManager!!.allDownloadInfoList
|
||||
}
|
||||
}
|
||||
if (mType != -1) {
|
||||
mList = ArrayList()
|
||||
for (info in list) {
|
||||
if (info.state == mType) {
|
||||
mList!!.add(info)
|
||||
}
|
||||
}
|
||||
mList = if (mType != -1) {
|
||||
list.filter { it.state == mType }
|
||||
} else {
|
||||
mList = list
|
||||
list
|
||||
}
|
||||
if (mAdapter != null) {
|
||||
mAdapter!!.notifyDataSetChanged()
|
||||
@@ -391,7 +386,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
R.id.action_start_all_reversed -> {
|
||||
val list = mList ?: return true
|
||||
val gidList = LongList()
|
||||
for (i in list.size - 1 downTo -1 + 1) {
|
||||
for (i in list.size - 1 downTo 0) {
|
||||
val info = list[i]
|
||||
if (info.state != DownloadInfo.STATE_FINISH) {
|
||||
gidList.add(info.gid)
|
||||
@@ -410,7 +405,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
|
||||
fun updateView() {
|
||||
if (mViewTransition != null) {
|
||||
if (mList == null || mList!!.size == 0) {
|
||||
if (mList.isNullOrEmpty()) {
|
||||
mViewTransition!!.showView(1)
|
||||
} else {
|
||||
mViewTransition!!.showView(0)
|
||||
@@ -552,9 +547,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
downloadInfoList = LinkedList()
|
||||
}
|
||||
val stateArray = recyclerView.checkedItemPositions
|
||||
var i = 0
|
||||
val n = stateArray.size()
|
||||
while (i < n) {
|
||||
for (i in 0 until stateArray.size()) {
|
||||
if (stateArray.valueAt(i)) {
|
||||
val info = list[stateArray.keyAt(i)]
|
||||
if (collectDownloadInfo) {
|
||||
@@ -564,7 +557,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
gidList!!.add(info.gid)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
when (position) {
|
||||
1 -> {
|
||||
@@ -580,7 +572,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
2 -> {
|
||||
// Stop
|
||||
if (null != mDownloadManager) {
|
||||
mDownloadManager!!.stopRangeDownload(gidList)
|
||||
mDownloadManager!!.stopRangeDownload(gidList!!)
|
||||
}
|
||||
// Cancel check mode
|
||||
recyclerView.outOfCustomChoiceMode()
|
||||
@@ -590,12 +582,12 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
// Delete
|
||||
val builder = CheckBoxDialogBuilder(
|
||||
context,
|
||||
getString(R.string.download_remove_dialog_message_2, gidList!!.size()),
|
||||
getString(R.string.download_remove_dialog_message_2, gidList!!.size),
|
||||
getString(R.string.download_remove_dialog_check_text),
|
||||
Settings.getRemoveImageFiles()
|
||||
)
|
||||
val helper = DeleteRangeDialogHelper(
|
||||
downloadInfoList, gidList, builder
|
||||
downloadInfoList!!, gidList, builder
|
||||
)
|
||||
builder.setTitle(R.string.download_remove_dialog_title)
|
||||
.setPositiveButton(android.R.string.ok, helper)
|
||||
@@ -607,11 +599,8 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
val labelRawList = downloadManager.labelList
|
||||
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
|
||||
labelList.add(getString(R.string.default_download_label_name))
|
||||
var i = 0
|
||||
val n = labelRawList.size
|
||||
while (i < n) {
|
||||
labelRawList[i].label?.let { labelList.add(it) }
|
||||
i++
|
||||
labelRawList.forEach {
|
||||
labelList.add(it.label!!)
|
||||
}
|
||||
val labels = labelList.toTypedArray()
|
||||
val helper = MoveDialogHelper(labels, downloadInfoList!!)
|
||||
@@ -644,12 +633,14 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onUpdateAll() {
|
||||
if (mAdapter != null) {
|
||||
mAdapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onReload() {
|
||||
if (mAdapter != null) {
|
||||
mAdapter!!.notifyDataSetChanged()
|
||||
@@ -867,8 +858,8 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
}
|
||||
|
||||
private inner class DeleteRangeDialogHelper(
|
||||
private val mDownloadInfoList: List<DownloadInfo>?,
|
||||
private val mGidList: LongList?, private val mBuilder: CheckBoxDialogBuilder
|
||||
private val mDownloadInfoList: List<DownloadInfo>,
|
||||
private val mGidList: LongList, private val mBuilder: CheckBoxDialogBuilder
|
||||
) : DialogInterface.OnClickListener {
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
if (which != DialogInterface.BUTTON_POSITIVE) {
|
||||
@@ -889,7 +880,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
val checked = mBuilder.isChecked
|
||||
Settings.putRemoveImageFiles(checked)
|
||||
if (checked) {
|
||||
val files = arrayOfNulls<UniFile>(mDownloadInfoList!!.size)
|
||||
val files = arrayOfNulls<UniFile>(mDownloadInfoList.size)
|
||||
for ((i, info) in mDownloadInfoList.withIndex()) {
|
||||
// Put file
|
||||
files[i] = SpiderDen.getGalleryDownloadDir(info.gid)
|
||||
@@ -908,6 +899,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
private val mLabels: Array<String>,
|
||||
private val mDownloadInfoList: List<DownloadInfo>
|
||||
) : DialogInterface.OnClickListener {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
// Cancel check mode
|
||||
context ?: return
|
||||
@@ -1030,7 +1022,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
}
|
||||
val info = mList!![position]
|
||||
info.thumb?.let {
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), EhUtils.fixThumbUrl(it))
|
||||
holder.thumb.load(EhUtils.fixThumbUrl(it))
|
||||
}
|
||||
holder.title.text = EhUtils.getSuitableTitle(info)
|
||||
holder.uploader.text = info.uploader
|
||||
@@ -1095,6 +1087,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
button.setOnClickListener(this)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onClick(v: View) {
|
||||
context ?: return
|
||||
val text = mBuilder.text
|
||||
@@ -1225,16 +1218,19 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
if (fromPosition == toPosition) {
|
||||
return false
|
||||
}
|
||||
val allInfoList = mDownloadManager!!.allDownloadInfoList
|
||||
val dbFromPosition = allInfoList.indexOf(mList!![fromPosition])
|
||||
val dbToPosition = allInfoList.indexOf(mList!![toPosition])
|
||||
lifecycleScope.launchIO {
|
||||
EhDB.moveDownloadInfo(dbFromPosition, dbToPosition)
|
||||
val item = allInfoList.removeAt(dbFromPosition)
|
||||
allInfoList.add(dbToPosition, item)
|
||||
if (mLabel != null) {
|
||||
val item1 = mList!!.removeAt(fromPosition)
|
||||
mList!!.add(toPosition, item1)
|
||||
when (mLabel) {
|
||||
null -> {
|
||||
mDownloadManager!!.moveDownload(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
getString(R.string.default_download_label_name) -> {
|
||||
mDownloadManager!!.moveDownload(null, fromPosition, toPosition)
|
||||
}
|
||||
|
||||
else -> {
|
||||
mDownloadManager!!.moveDownload(mLabel, fromPosition, toPosition)
|
||||
}
|
||||
}
|
||||
withUIContext {
|
||||
mAdapter!!.notifyItemMoved(fromPosition, toPosition)
|
||||
|
||||
@@ -58,6 +58,7 @@ import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.parser.FavoritesParser
|
||||
import com.hippo.ehviewer.ui.CommonOperations
|
||||
import com.hippo.ehviewer.widget.GalleryInfoContentHelper
|
||||
import com.hippo.util.getParcelableCompat
|
||||
import com.hippo.widget.ContentLayout
|
||||
import com.hippo.widget.FabLayout
|
||||
import com.hippo.widget.FabLayout.OnClickFabListener
|
||||
@@ -149,7 +150,7 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
}
|
||||
|
||||
private fun onRestore(savedInstanceState: Bundle) {
|
||||
mUrlBuilder = savedInstanceState.getParcelable(KEY_URL_BUILDER)
|
||||
mUrlBuilder = savedInstanceState.getParcelableCompat(KEY_URL_BUILDER)
|
||||
if (mUrlBuilder == null) {
|
||||
mUrlBuilder = FavListUrlBuilder()
|
||||
}
|
||||
@@ -529,7 +530,7 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
4 -> { // Download
|
||||
val activity: Activity? = mainActivity
|
||||
if (activity != null) {
|
||||
CommonOperations.startDownload(mainActivity, mModifyGiList, false)
|
||||
CommonOperations.startDownload(mainActivity!!, mModifyGiList, false)
|
||||
}
|
||||
mModifyGiList.clear()
|
||||
if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {
|
||||
|
||||
@@ -29,7 +29,6 @@ import com.hippo.easyrecyclerview.MarginItemDecoration
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||
@@ -175,7 +174,7 @@ internal abstract class GalleryAdapter(
|
||||
}
|
||||
when (mType) {
|
||||
TYPE_LIST -> {
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb!!)
|
||||
holder.thumb.load(gi.thumb!!)
|
||||
holder.title!!.text = EhUtils.getSuitableTitle(gi)
|
||||
holder.uploader!!.text = gi.uploader
|
||||
holder.uploader.alpha = if (gi.disowned) .5f else 1f
|
||||
@@ -209,7 +208,7 @@ internal abstract class GalleryAdapter(
|
||||
|
||||
TYPE_GRID -> {
|
||||
(holder.thumb as TileThumb).setThumbSize(gi.thumbWidth, gi.thumbHeight)
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb!!)
|
||||
holder.thumb.load(gi.thumb!!)
|
||||
val category: View = holder.category
|
||||
var drawable = category.background
|
||||
val color = EhUtils.getCategoryColor(gi.category)
|
||||
|
||||
@@ -25,15 +25,20 @@ import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import android.text.Html
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextUtils
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import android.util.Log
|
||||
import android.view.ActionMode
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewAnimationUtils
|
||||
import android.view.ViewGroup
|
||||
@@ -42,6 +47,9 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.text.set
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsAnimationCompat
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
@@ -51,6 +59,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.hippo.app.BaseDialogBuilder
|
||||
import com.hippo.app.EditTextDialogBuilder
|
||||
import com.hippo.easyrecyclerview.EasyRecyclerView
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.UrlOpener
|
||||
@@ -68,10 +77,12 @@ import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
|
||||
import com.hippo.text.URLImageGetter
|
||||
import com.hippo.util.BBCode.toBBCode
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.util.TextUrl
|
||||
import com.hippo.util.addTextToClipboard
|
||||
import com.hippo.util.getParcelableCompat
|
||||
import com.hippo.view.ViewTransition
|
||||
import com.hippo.widget.FabLayout
|
||||
import com.hippo.widget.LinkifyTextView
|
||||
@@ -116,7 +127,7 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
if (args == null) {
|
||||
return
|
||||
}
|
||||
mGalleryDetail = args.getParcelable(KEY_GALLERY_DETAIL)
|
||||
mGalleryDetail = args.getParcelableCompat(KEY_GALLERY_DETAIL)
|
||||
mShowAllComments =
|
||||
mGalleryDetail != null && mGalleryDetail!!.comments != null && !mGalleryDetail!!.comments!!.hasMore
|
||||
}
|
||||
@@ -126,7 +137,7 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
}
|
||||
|
||||
private fun onRestore(savedInstanceState: Bundle) {
|
||||
mGalleryDetail = savedInstanceState.getParcelable(KEY_GALLERY_DETAIL)
|
||||
mGalleryDetail = savedInstanceState.getParcelableCompat(KEY_GALLERY_DETAIL)
|
||||
mShowAllComments =
|
||||
mGalleryDetail != null && mGalleryDetail!!.comments != null && !mGalleryDetail!!.comments!!.hasMore
|
||||
}
|
||||
@@ -185,6 +196,73 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
itemAnimator.supportsChangeAnimations = false
|
||||
}
|
||||
mSendImage!!.setOnClickListener(this)
|
||||
mEditText!!.customSelectionActionModeCallback = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
requireActivity().menuInflater.inflate(R.menu.context_comment, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
item?.let {
|
||||
val text = mEditText!!.editableText
|
||||
val start = mEditText!!.selectionStart
|
||||
val end = mEditText!!.selectionEnd
|
||||
when (item.itemId) {
|
||||
R.id.action_bold -> text[start, end] = StyleSpan(Typeface.BOLD)
|
||||
|
||||
R.id.action_italic -> text[start, end] = StyleSpan(Typeface.ITALIC)
|
||||
|
||||
R.id.action_underline -> text[start, end] = UnderlineSpan()
|
||||
|
||||
R.id.action_strikethrough -> text[start, end] = StrikethroughSpan()
|
||||
|
||||
R.id.action_url -> {
|
||||
val oldSpans = text.getSpans<URLSpan>(start, end)
|
||||
var oldUrl = "https://"
|
||||
oldSpans.forEach {
|
||||
if (!TextUtils.isEmpty(it.url)) {
|
||||
oldUrl = it.url
|
||||
}
|
||||
}
|
||||
val builder = EditTextDialogBuilder(
|
||||
context, oldUrl, getString(R.string.format_url)
|
||||
)
|
||||
builder.setTitle(getString(R.string.format_url))
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
val dialog = builder.show()
|
||||
val button: View? = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
button?.setOnClickListener(View.OnClickListener {
|
||||
val url = builder.text.trim()
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
builder.setError(getString(R.string.text_is_empty))
|
||||
return@OnClickListener
|
||||
} else {
|
||||
builder.setError(null)
|
||||
}
|
||||
text.clearSpan(start, end, true)
|
||||
text[start, end] = URLSpan(url)
|
||||
dialog.dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
R.id.action_clear -> {
|
||||
text.clearSpan(start, end, false)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
mode?.finish()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
}
|
||||
}
|
||||
mFab!!.setOnClickListener(this)
|
||||
addAboveSnackView(mEditPanel)
|
||||
addAboveSnackView(mFabLayout)
|
||||
@@ -193,6 +271,21 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
return view
|
||||
}
|
||||
|
||||
fun Spannable.clearSpan(start: Int, end: Int, url: Boolean) {
|
||||
val spans = if (url) getSpans<URLSpan>(start, end) else getSpans<CharacterStyle>(start, end)
|
||||
spans.forEach {
|
||||
val spanStart = getSpanStart(it)
|
||||
val spanEnd = getSpanEnd(it)
|
||||
removeSpan(it)
|
||||
if (spanStart < start) {
|
||||
this[spanStart, start] = it
|
||||
}
|
||||
if (spanEnd > end) {
|
||||
this[end, spanEnd] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
mCallback.remove()
|
||||
@@ -324,7 +417,7 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
builder.setView(rv).show()
|
||||
}
|
||||
|
||||
private fun showCommentDialog(position: Int) {
|
||||
private fun showCommentDialog(position: Int, text: CharSequence) {
|
||||
val context = context
|
||||
if (context == null || mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null || position >= mGalleryDetail!!.comments!!.comments!!.size || position < 0) {
|
||||
return
|
||||
@@ -357,12 +450,12 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
}
|
||||
BaseDialogBuilder(context)
|
||||
.setItems(menu.toTypedArray()) { _: DialogInterface?, which: Int ->
|
||||
if (which < 0 || which >= menuId.size()) {
|
||||
if (which < 0 || which >= menuId.size) {
|
||||
return@setItems
|
||||
}
|
||||
val id = menuId[which]
|
||||
if (id == R.id.copy) {
|
||||
requireActivity().addTextToClipboard(comment.comment, false)
|
||||
requireActivity().addTextToClipboard(text, false)
|
||||
} else if (id == R.id.block_commenter) {
|
||||
showFilterCommenterDialog(comment.user, position)
|
||||
} else if (id == R.id.vote_up) {
|
||||
@@ -372,7 +465,7 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
} else if (id == R.id.check_vote_status) {
|
||||
showVoteStatusDialog(context, comment.voteState)
|
||||
} else if (id == R.id.edit_comment) {
|
||||
prepareEditComment(comment.id)
|
||||
prepareEditComment(comment.id, text)
|
||||
if (!mInAnimation && mEditPanel != null && mEditPanel!!.visibility != View.VISIBLE) {
|
||||
showEditPanel(true)
|
||||
}
|
||||
@@ -389,7 +482,7 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
if (span is URLSpan) {
|
||||
UrlOpener.openUrl(activity, span.url, true, mGalleryDetail)
|
||||
} else {
|
||||
showCommentDialog(position)
|
||||
showCommentDialog(position, holder.sp)
|
||||
}
|
||||
} else if (holder is MoreCommentHolder && !mRefreshingComments && mAdapter != null) {
|
||||
mRefreshingComments = true
|
||||
@@ -426,8 +519,9 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareEditComment(commentId: Long) {
|
||||
private fun prepareEditComment(commentId: Long, text: CharSequence) {
|
||||
mCommentId = commentId
|
||||
mEditText?.setText(text)
|
||||
if (mSendImage != null) {
|
||||
mSendImage!!.setImageDrawable(mPencilDrawable)
|
||||
}
|
||||
@@ -571,7 +665,7 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
}
|
||||
} else if (mSendImage === v) {
|
||||
if (!mInAnimation) {
|
||||
val comment = mEditText!!.text.toString()
|
||||
val comment = mEditText!!.text.toBBCode()
|
||||
if (TextUtils.isEmpty(comment)) {
|
||||
// Comment is empty
|
||||
return
|
||||
@@ -761,13 +855,14 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
private val user: TextView = itemView.findViewById(R.id.user)
|
||||
private val time: TextView = itemView.findViewById(R.id.time)
|
||||
val comment: LinkifyTextView = itemView.findViewById(R.id.comment)
|
||||
lateinit var sp: CharSequence
|
||||
|
||||
private fun generateComment(
|
||||
context: Context,
|
||||
textView: ObservedTextView,
|
||||
comment: GalleryComment
|
||||
): CharSequence {
|
||||
val sp = Html.fromHtml(
|
||||
sp = Html.fromHtml(
|
||||
comment.comment,
|
||||
Html.FROM_HTML_MODE_LEGACY,
|
||||
URLImageGetter(textView),
|
||||
@@ -777,64 +872,42 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
if (0L != comment.id && 0 != comment.score) {
|
||||
val score = comment.score
|
||||
val scoreString = if (score > 0) "+$score" else score.toString()
|
||||
val ss = SpannableString(scoreString)
|
||||
ss.setSpan(
|
||||
ssb.append(" ").inSpans(
|
||||
RelativeSizeSpan(0.8f),
|
||||
0,
|
||||
scoreString.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
ss.setSpan(
|
||||
StyleSpan(Typeface.BOLD),
|
||||
0,
|
||||
scoreString.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
ss.setSpan(
|
||||
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
|
||||
0,
|
||||
scoreString.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
ssb.append(" ").append(ss)
|
||||
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary))
|
||||
) {
|
||||
append(scoreString)
|
||||
}
|
||||
}
|
||||
if (comment.lastEdited != 0L) {
|
||||
val str = context.getString(
|
||||
R.string.last_edited,
|
||||
ReadableTime.getTimeAgo(comment.lastEdited)
|
||||
)
|
||||
val ss = SpannableString(str)
|
||||
ss.setSpan(
|
||||
ssb.append("\n\n").inSpans(
|
||||
RelativeSizeSpan(0.8f),
|
||||
0,
|
||||
str.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
ss.setSpan(
|
||||
StyleSpan(Typeface.BOLD),
|
||||
0,
|
||||
str.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
ss.setSpan(
|
||||
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
|
||||
0, str.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
ssb.append("\n\n").append(ss)
|
||||
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary))
|
||||
) {
|
||||
append(str)
|
||||
}
|
||||
}
|
||||
return TextUrl.handleTextUrl(ssb)
|
||||
}
|
||||
|
||||
fun bind(value: GalleryComment) {
|
||||
user.text = if (value.uploader) getString(
|
||||
R.string.comment_user_uploader,
|
||||
value.user
|
||||
) else value.user
|
||||
user.text = value.user?.let {
|
||||
if (value.uploader) getString(R.string.comment_user_uploader, it) else it
|
||||
}
|
||||
user.setOnClickListener {
|
||||
val lub = ListUrlBuilder()
|
||||
lub.mode = ListUrlBuilder.MODE_UPLOADER
|
||||
lub.keyword = value.user
|
||||
navigate(R.id.galleryListScene, lub.toStartArgs(), true)
|
||||
value.user?.let {
|
||||
val lub = ListUrlBuilder().apply {
|
||||
mode = ListUrlBuilder.MODE_UPLOADER
|
||||
keyword = it
|
||||
}
|
||||
navigate(R.id.galleryListScene, lub.toStartArgs(), true)
|
||||
}
|
||||
}
|
||||
time.text = ReadableTime.getTimeAgo(value.time)
|
||||
comment.text = generateComment(comment.context, comment, value)
|
||||
|
||||
@@ -46,6 +46,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.Coil.imageLoader
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
@@ -62,7 +63,6 @@ import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.UrlOpener
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory
|
||||
import com.hippo.ehviewer.client.EhClient
|
||||
import com.hippo.ehviewer.client.EhFilter
|
||||
import com.hippo.ehviewer.client.EhRequest
|
||||
@@ -78,6 +78,7 @@ import com.hippo.ehviewer.client.data.GalleryTagGroup
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||
import com.hippo.ehviewer.client.parser.ArchiveParser
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.client.parser.ParserUtils
|
||||
import com.hippo.ehviewer.client.parser.RateGalleryParser
|
||||
import com.hippo.ehviewer.client.parser.TorrentParser
|
||||
@@ -86,9 +87,8 @@ import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.ehviewer.databinding.SceneGalleryDetailBinding
|
||||
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
|
||||
import com.hippo.ehviewer.gallery.EhPageLoader
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.spider.SpiderDen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.ui.CommonOperations
|
||||
import com.hippo.ehviewer.ui.GalleryInfoBottomSheet
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
@@ -100,6 +100,7 @@ import com.hippo.util.AppHelper
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.util.addTextToClipboard
|
||||
import com.hippo.util.getParcelableCompat
|
||||
import com.hippo.view.ViewTransition
|
||||
import com.hippo.widget.AutoWrapLayout
|
||||
import com.hippo.widget.LoadImageView
|
||||
@@ -149,7 +150,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
}
|
||||
private var mArchiveFormParamOr: String? = null
|
||||
private var mArchiveList: List<ArchiveParser.Archive>? = null
|
||||
private var mCurrentFunds: ArchiveParser.Funds? = null
|
||||
private var mCurrentFunds: HomeParser.Funds? = null
|
||||
|
||||
@State
|
||||
private var mState = STATE_INIT
|
||||
@@ -177,7 +178,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
val action = args?.getString(KEY_ACTION) ?: return
|
||||
mAction = action
|
||||
if (ACTION_GALLERY_INFO == action) {
|
||||
mGalleryInfo = args.getParcelable(KEY_GALLERY_INFO)
|
||||
mGalleryInfo = args.getParcelableCompat(KEY_GALLERY_INFO)
|
||||
// Add history
|
||||
mGalleryInfo?.let { lifecycleScope.launchIO { EhDB.putHistoryInfo(it) } }
|
||||
} else if (ACTION_GID_TOKEN == action) {
|
||||
@@ -260,15 +261,21 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
_binding ?: return
|
||||
try {
|
||||
val galleryProvider: PageLoader2 = EhPageLoader(mGalleryInfo)
|
||||
galleryProvider.start()
|
||||
val startPage = galleryProvider.startPage
|
||||
if (startPage != 0) {
|
||||
binding.content.header.read.text = getString(R.string.read_from, startPage + 1)
|
||||
mGalleryInfo?.let {
|
||||
viewLifecycleOwner.lifecycleScope.launchIO {
|
||||
runCatching {
|
||||
val startPage = SpiderQueen.getStartPage(it.gid)
|
||||
withUIContext {
|
||||
binding.content.header.read.text = if (startPage == 0) {
|
||||
getString(R.string.read)
|
||||
} else {
|
||||
getString(R.string.read_from, startPage + 1)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
galleryProvider.stop()
|
||||
} catch (ignore: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,10 +285,10 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
|
||||
private fun onRestore(savedInstanceState: Bundle) {
|
||||
mAction = savedInstanceState.getString(KEY_ACTION)
|
||||
mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO)
|
||||
mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO)
|
||||
mGid = savedInstanceState.getLong(KEY_GID)
|
||||
mToken = savedInstanceState.getString(KEY_TOKEN)
|
||||
mGalleryDetail = savedInstanceState.getParcelable(KEY_GALLERY_DETAIL)
|
||||
mGalleryDetail = savedInstanceState.getParcelableCompat(KEY_GALLERY_DETAIL)
|
||||
mRequestId = savedInstanceState.getInt(KEY_REQUEST_ID)
|
||||
}
|
||||
|
||||
@@ -507,10 +514,14 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
mGalleryInfo?.let { gi ->
|
||||
if (mAction == ACTION_GALLERY_INFO) {
|
||||
binding.content.header.run {
|
||||
(thumb as LoadImageView).load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb!!)
|
||||
(thumb as LoadImageView).load(gi.thumb!!)
|
||||
setTitle(EhUtils.getSuitableTitle(gi))
|
||||
uploader.text = gi.uploader
|
||||
uploader.alpha = if (gi.disowned) .5f else 1f
|
||||
if (gi.disowned) {
|
||||
uploader.run {
|
||||
setTextColor(ColorUtils.setAlphaComponent(currentTextColor, 128))
|
||||
}
|
||||
}
|
||||
category.text = EhUtils.getCategory(gi.category)
|
||||
category.setTextColor(EhUtils.getCategoryColor(gi.category))
|
||||
updateDownloadText()
|
||||
@@ -563,10 +574,14 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
_binding ?: return
|
||||
val resources = resources
|
||||
binding.content.header.run {
|
||||
(thumb as LoadImageView).load(EhCacheKeyFactory.getThumbKey(gd.gid), gd.thumb!!, true)
|
||||
(thumb as LoadImageView).load(gd.thumb!!, true)
|
||||
setTitle(EhUtils.getSuitableTitle(gd))
|
||||
uploader.text = gd.uploader
|
||||
uploader.alpha = if (gd.disowned) .5f else 1f
|
||||
if (gd.disowned) {
|
||||
uploader.run {
|
||||
setTextColor(ColorUtils.setAlphaComponent(currentTextColor, 128))
|
||||
}
|
||||
}
|
||||
category.text = EhUtils.getCategory(gd.category)
|
||||
category.setTextColor(EhUtils.getCategoryColor(gd.category))
|
||||
}
|
||||
@@ -620,7 +635,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
binding.content.tags.tags.addView(ll)
|
||||
var readableTagName: String? = null
|
||||
if (ehTags != null && ehTags.isInitialized()) {
|
||||
readableTagName = ehTags.getTranslation("n", tg.groupName)
|
||||
readableTagName = ehTags.getTranslation(tag = tg.groupName)
|
||||
}
|
||||
val tgName = inflater.inflate(R.layout.item_gallery_tag, ll, false) as TextView
|
||||
ll.addView(tgName)
|
||||
@@ -633,12 +648,15 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
var j = 0
|
||||
val z = tg.size()
|
||||
while (j < z) {
|
||||
tg.forEach {
|
||||
val tag = inflater.inflate(R.layout.item_gallery_tag, awl, false) as TextView
|
||||
awl.addView(tag)
|
||||
val tagStr = tg.getTagAt(j)
|
||||
val tagStr = if (it.startsWith('_')) {
|
||||
tag.alpha = 0.5f
|
||||
it.substring(1)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
var readableTag: String? = null
|
||||
if (ehTags != null && ehTags.isInitialized()) {
|
||||
readableTag = ehTags.getTranslation(prefix, tagStr)
|
||||
@@ -648,7 +666,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
tag.setTag(R.id.tag, tg.groupName + ":" + tagStr)
|
||||
tag.setOnClickListener(this)
|
||||
tag.setOnLongClickListener(this)
|
||||
j++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -768,7 +785,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
return
|
||||
}
|
||||
try {
|
||||
val path = imageLoader(context).diskCache!![EhCacheKeyFactory.getThumbKey(gid)]!!.data
|
||||
val path = imageLoader(context).diskCache!![mGalleryInfo!!.thumb!!]!!.data
|
||||
val lub = ListUrlBuilder()
|
||||
lub.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
|
||||
lub.imagePath = path.toString()
|
||||
@@ -779,7 +796,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToPreview(nextPage: Boolean = false) {
|
||||
private fun navigateToPreview(nextPage: Boolean = false) {
|
||||
mGalleryDetail?.let {
|
||||
val args = Bundle()
|
||||
args.putParcelable(GalleryPreviewsScene.KEY_GALLERY_INFO, it)
|
||||
@@ -872,7 +889,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
.show()
|
||||
}
|
||||
} else if (binding.content.header.info === v) {
|
||||
assert(mGalleryDetail != null)
|
||||
mGalleryDetail ?: return
|
||||
val galleryInfoBottomSheet = GalleryInfoBottomSheet(mGalleryDetail!!)
|
||||
galleryInfoBottomSheet.show(
|
||||
requireActivity().supportFragmentManager,
|
||||
@@ -886,7 +903,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (containLocalFavorites || mGalleryDetail!!.isFavorited) {
|
||||
mModifyingFavorites = true
|
||||
CommonOperations.removeFromFavorites(
|
||||
activity, mGalleryDetail,
|
||||
activity, mGalleryDetail!!,
|
||||
ModifyFavoritesListener(context, true)
|
||||
)
|
||||
remove = true
|
||||
@@ -895,7 +912,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (!remove) {
|
||||
mModifyingFavorites = true
|
||||
CommonOperations.addToFavorites(
|
||||
activity, mGalleryDetail,
|
||||
activity, mGalleryDetail!!,
|
||||
ModifyFavoritesListener(context, false)
|
||||
)
|
||||
}
|
||||
@@ -1061,7 +1078,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
BaseDialogBuilder(context)
|
||||
.setTitle(tag)
|
||||
.setItems(menu.toTypedArray()) { _: DialogInterface?, which: Int ->
|
||||
if (which < 0 || which >= menuId.size()) {
|
||||
if (which < 0 || which >= menuId.size) {
|
||||
return@setItems
|
||||
}
|
||||
when (menuId[which]) {
|
||||
@@ -1132,7 +1149,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (EhDB.containLocalFavorites(mGalleryDetail!!.gid) || mGalleryDetail!!.isFavorited) {
|
||||
mModifyingFavorites = true
|
||||
CommonOperations.removeFromFavorites(
|
||||
activity, mGalleryDetail,
|
||||
activity, mGalleryDetail!!,
|
||||
ModifyFavoritesListener(activity, true)
|
||||
)
|
||||
remove = true
|
||||
@@ -1141,7 +1158,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (!remove) {
|
||||
mModifyingFavorites = true
|
||||
CommonOperations.addToFavorites(
|
||||
activity, mGalleryDetail,
|
||||
activity, mGalleryDetail!!,
|
||||
ModifyFavoritesListener(activity, false), true
|
||||
)
|
||||
}
|
||||
@@ -1285,8 +1302,8 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
private class DownloadArchiveListener(
|
||||
context: Context,
|
||||
private val mGalleryInfo: GalleryInfo?
|
||||
) : EhCallback<GalleryDetailScene?, String>(context) {
|
||||
override fun onSuccess(result: String) {
|
||||
) : EhCallback<GalleryDetailScene?, String?>(context) {
|
||||
override fun onSuccess(result: String?) {
|
||||
// TODO: Don't use buggy system download service
|
||||
val r = DownloadManager.Request(Uri.parse(result))
|
||||
val name =
|
||||
@@ -1445,7 +1462,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
}
|
||||
}
|
||||
|
||||
private fun bind(data: List<ArchiveParser.Archive>?, funds: ArchiveParser.Funds?) {
|
||||
private fun bind(data: List<ArchiveParser.Archive>?, funds: HomeParser.Funds?) {
|
||||
if (null == mDialog || null == mProgressView || null == mErrorText || null == mListView) {
|
||||
return
|
||||
}
|
||||
@@ -1455,9 +1472,19 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
mListView!!.visibility = View.GONE
|
||||
mErrorText!!.setText(R.string.no_archives)
|
||||
} else {
|
||||
val nameArray = data.stream().map { archive: ArchiveParser.Archive ->
|
||||
archive.format { id: Int ->
|
||||
resources.getString(id)
|
||||
val nameArray = data.stream().map {
|
||||
it.run {
|
||||
if (isHAtH) {
|
||||
val costStr =
|
||||
if (cost == "Free") resources.getString(R.string.archive_free) else cost
|
||||
"[H@H] $name [$size] [$costStr]"
|
||||
} else {
|
||||
val nameStr =
|
||||
resources.getString(if (res == "org") R.string.archive_original else R.string.archive_resample)
|
||||
val costStr =
|
||||
if (cost == "Free!") resources.getString(R.string.archive_free) else cost
|
||||
"$nameStr [$size] [$costStr]"
|
||||
}
|
||||
}
|
||||
}.toArray()
|
||||
mProgressView!!.visibility = View.GONE
|
||||
@@ -1468,7 +1495,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (funds != null) {
|
||||
var fundsGP = funds.fundsGP.toString()
|
||||
// Ex GP numbers are rounded down to the nearest thousand
|
||||
if (Settings.getGallerySite() == EhUrl.SITE_EX) {
|
||||
if (EhUtils.isExHentai) {
|
||||
fundsGP += "+"
|
||||
}
|
||||
mDialog!!.setTitle(getString(R.string.current_funds, fundsGP, funds.fundsC))
|
||||
@@ -1487,11 +1514,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
val cost = ParserUtils.parseInt(
|
||||
mArchiveList!![position].cost.removeSuffix("GP").removeSuffix("Credits"), 0
|
||||
)
|
||||
if (cost <= mCurrentFunds!!.fundsGP) {
|
||||
mCurrentFunds!!.fundsGP -= cost
|
||||
} else if (cost <= mCurrentFunds!!.fundsC) {
|
||||
mCurrentFunds!!.fundsC -= cost
|
||||
} else {
|
||||
if (cost > maxOf(mCurrentFunds!!.fundsGP, mCurrentFunds!!.fundsC)) {
|
||||
showTip(R.string.insufficient_funds, LENGTH_SHORT)
|
||||
return
|
||||
}
|
||||
@@ -1616,7 +1639,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
FileUtils.sanitizeFilename("$name.torrent")
|
||||
)
|
||||
r.allowScanningByMediaScanner()
|
||||
r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
r.addRequestHeader("Cookie", ehCookieStore.getCookieHeader(url.toHttpUrl()))
|
||||
val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
@@ -1729,8 +1751,8 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
return null
|
||||
}
|
||||
for (tagGroup in tagGroups) {
|
||||
if ("artist" == tagGroup.groupName && tagGroup.size() > 0) {
|
||||
return tagGroup.getTagAt(0)
|
||||
if ("artist" == tagGroup.groupName && tagGroup.size > 0) {
|
||||
return tagGroup[0].removePrefix("_")
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -71,7 +71,6 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.WindowInsetsAnimationHelper
|
||||
import com.hippo.ehviewer.client.EhClient
|
||||
import com.hippo.ehviewer.client.EhRequest
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
@@ -92,6 +91,7 @@ import com.hippo.ehviewer.ui.CommonOperations
|
||||
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter
|
||||
import com.hippo.ehviewer.widget.GalleryInfoContentHelper
|
||||
import com.hippo.ehviewer.widget.SearchLayout
|
||||
import com.hippo.util.getParcelableCompat
|
||||
import com.hippo.view.BringOutTransition
|
||||
import com.hippo.view.ViewTransition
|
||||
import com.hippo.widget.FabLayout
|
||||
@@ -235,7 +235,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
|
||||
private fun onRestore(savedInstanceState: Bundle) {
|
||||
mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH)
|
||||
mUrlBuilder = savedInstanceState.getParcelable(KEY_LIST_URL_BUILDER)!!
|
||||
mUrlBuilder = savedInstanceState.getParcelableCompat(KEY_LIST_URL_BUILDER)!!
|
||||
mState = savedInstanceState.getInt(KEY_STATE)
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
}
|
||||
|
||||
private fun setSearchBarHint() {
|
||||
setEditTextHint(getString(if (EhUrl.SITE_EX == Settings.getGallerySite()) R.string.gallery_list_search_bar_hint_exhentai else R.string.gallery_list_search_bar_hint_e_hentai))
|
||||
setEditTextHint(getString(if (EhUtils.isExHentai) R.string.gallery_list_search_bar_hint_exhentai else R.string.gallery_list_search_bar_hint_e_hentai))
|
||||
}
|
||||
|
||||
private fun setSearchBarSuggestionProvider() {
|
||||
@@ -1088,8 +1088,8 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
}
|
||||
|
||||
private class AddToFavoriteListener(context: Context) :
|
||||
EhCallback<GalleryListScene, Void>(context) {
|
||||
override fun onSuccess(result: Void) {
|
||||
EhCallback<GalleryListScene, Void?>(context) {
|
||||
override fun onSuccess(result: Void?) {
|
||||
showTip(R.string.add_to_favorite_success, LENGTH_SHORT)
|
||||
}
|
||||
|
||||
@@ -1101,8 +1101,8 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
}
|
||||
|
||||
private class RemoveFromFavoriteListener(context: Context) :
|
||||
EhCallback<GalleryListScene, Void>(context) {
|
||||
override fun onSuccess(result: Void) {
|
||||
EhCallback<GalleryListScene, Void?>(context) {
|
||||
override fun onSuccess(result: Void?) {
|
||||
showTip(R.string.remove_from_favorite_success, LENGTH_SHORT)
|
||||
}
|
||||
|
||||
@@ -1148,10 +1148,13 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
return@setOnClickListener
|
||||
}
|
||||
val q = mQuickSearchList[holder.bindingAdapterPosition]
|
||||
mUrlBuilder.set(q)
|
||||
onUpdateUrlBuilder()
|
||||
val i = q.name!!.lastIndexOf("@")
|
||||
mHelper!!.goTo(if (i != -1) q.name!!.substring(i + 1) else null, true)
|
||||
val goto = if (i != -1) q.name!!.substring(i + 1) else null
|
||||
val args = ListUrlBuilder().apply {
|
||||
set(q)
|
||||
setIndex(goto)
|
||||
}.toStartArgs()
|
||||
navigate(R.id.galleryListScene, args, true)
|
||||
setState(STATE_NORMAL)
|
||||
closeDrawer(GravityCompat.END)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.hippo.ehviewer.client.data.GalleryPreview
|
||||
import com.hippo.ehviewer.client.data.PreviewSet
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.databinding.SceneGalleryPreviewsBinding
|
||||
import com.hippo.util.getParcelableCompat
|
||||
import com.hippo.widget.ContentLayout.ContentHelper
|
||||
import com.hippo.widget.LoadImageView
|
||||
import com.hippo.widget.recyclerview.AutoGridLayoutManager
|
||||
@@ -71,12 +72,12 @@ class GalleryPreviewsScene : BaseToolbarScene() {
|
||||
|
||||
private fun onInit() {
|
||||
val args = arguments ?: return
|
||||
mGalleryInfo = args.getParcelable(KEY_GALLERY_INFO)
|
||||
mGalleryInfo = args.getParcelableCompat(KEY_GALLERY_INFO)
|
||||
mNextPage = args.getBoolean(KEY_NEXT_PAGE)
|
||||
}
|
||||
|
||||
private fun onRestore(savedInstanceState: Bundle) {
|
||||
mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO)
|
||||
mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO)
|
||||
mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.hippo.ehviewer.ui.scene
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.core.view.GravityCompat
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.ui.scene.history.HistoryScreen
|
||||
|
||||
class HistoryComposeScreenFragmentBridge : BaseScene() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
Mdc3Theme {
|
||||
HistoryScreen(this@HistoryComposeScreenFragmentBridge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun toggleNavigationDrawer() {
|
||||
toggleDrawer(GravityCompat.START)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun navToDetail(gi: GalleryInfo) {
|
||||
val args = Bundle()
|
||||
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO)
|
||||
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi)
|
||||
navigate(R.id.galleryDetailScene, args)
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.hippo.app.BaseDialogBuilder
|
||||
import com.hippo.easyrecyclerview.HandlerDrawable
|
||||
import com.hippo.ehviewer.*
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
@@ -397,7 +396,7 @@ class HistoryScene : BaseToolbarScene() {
|
||||
val gi: GalleryInfo? = getItem(position)
|
||||
gi ?: return
|
||||
gi.thumb?.let {
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), EhUtils.fixThumbUrl(it))
|
||||
holder.thumb.load(EhUtils.fixThumbUrl(it))
|
||||
}
|
||||
holder.title.text = EhUtils.getSuitableTitle(gi)
|
||||
holder.uploader.text = gi.uploader
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhClient
|
||||
import com.hippo.ehviewer.client.EhRequest
|
||||
@@ -169,6 +170,7 @@ class ProgressScene : BaseScene(), View.OnClickListener {
|
||||
arg.putLong(GalleryDetailScene.KEY_GID, mGid)
|
||||
arg.putString(GalleryDetailScene.KEY_TOKEN, result)
|
||||
arg.putInt(GalleryDetailScene.KEY_PAGE, mPage)
|
||||
findNavController().popBackStack()
|
||||
navigate(R.id.galleryDetailScene, arg)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.hippo.ehviewer.ui.scene
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
@@ -170,24 +169,15 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
}
|
||||
|
||||
private fun wrapTagKeyword(keyword: String): String {
|
||||
var keyword = keyword
|
||||
keyword = keyword.trim { it <= ' ' }
|
||||
|
||||
val index1 = keyword.indexOf(':')
|
||||
if (index1 == -1 || index1 >= keyword.length - 1) {
|
||||
// Can't find :, or : is the last char
|
||||
return keyword
|
||||
}
|
||||
if (keyword[index1 + 1] == '"') {
|
||||
// The char after : is ", the word must be quoted
|
||||
return keyword
|
||||
}
|
||||
val index2 = keyword.indexOf(' ')
|
||||
return if (index2 <= index1) {
|
||||
// Can't find space, or space is before :
|
||||
return if (keyword.endsWith(':')) {
|
||||
keyword
|
||||
} else keyword.substring(0, index1 + 1) + "\"" + keyword.substring(index1 + 1) + "$\""
|
||||
|
||||
} else if (keyword.contains(' ')) {
|
||||
val tag = keyword.substringAfter(':')
|
||||
val prefix = keyword.dropLast(tag.length)
|
||||
"$prefix\"$tag$\" "
|
||||
} else {
|
||||
"$keyword$ "
|
||||
}
|
||||
}
|
||||
|
||||
interface SuggestionProvider {
|
||||
@@ -276,13 +266,11 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
override fun onClick() {
|
||||
val edittext = binding.searchview.editText
|
||||
edittext.let {
|
||||
val text = it.text.toString()
|
||||
var temp = wrapTagKeyword(mKeyword) + " "
|
||||
if (text.contains(" ")) {
|
||||
temp = text.substring(0, text.lastIndexOf(" ")) + " " + temp
|
||||
}
|
||||
it.setText(temp)
|
||||
it.setSelection(temp.length)
|
||||
val keywords = it.text.toString().substringBeforeLast(' ', "")
|
||||
val keyword = wrapTagKeyword(mKeyword)
|
||||
val newKeywords = if (keywords.isNotEmpty()) "$keywords $keyword" else keyword
|
||||
it.setText(newKeywords)
|
||||
it.setSelection(newKeywords.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,18 +328,15 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
mSuggestionProvider?.run { providerSuggestions(text)?.forEach { emit(it) } }
|
||||
mSearchDatabase.getSuggestions(text, 128).forEach { emit(KeywordSuggestion(it)) }
|
||||
EhTagDatabase.takeIf { it.isInitialized() }?.run {
|
||||
if (!TextUtils.isEmpty(text) && !text.endsWith(" ")) {
|
||||
val s = text.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (s.isNotEmpty()) {
|
||||
val keyword = s[s.size - 1]
|
||||
val translate =
|
||||
Settings.getShowTagTranslations() && isTranslatable(requireContext())
|
||||
suggestFlow(keyword, translate, true).collect {
|
||||
emit(TagSuggestion(it.first, it.second))
|
||||
}
|
||||
suggestFlow(keyword, translate).collect {
|
||||
emit(TagSuggestion(it.first, it.second))
|
||||
}
|
||||
if (text.isNotEmpty() && !text.endsWith(' ')) {
|
||||
val keyword = text.substringAfterLast(' ')
|
||||
val translate =
|
||||
Settings.getShowTagTranslations() && isTranslatable(requireContext())
|
||||
suggestFlow(keyword, translate, true).collect {
|
||||
emit(TagSuggestion(it.first, it.second))
|
||||
}
|
||||
suggestFlow(keyword, translate).collect {
|
||||
emit(TagSuggestion(it.first, it.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.hippo.ehviewer.ui.scene.history
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.AlertDialog
|
||||
// import androidx.compose.material3.DismissDirection
|
||||
// import androidx.compose.material3.DismissValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
// import androidx.compose.material3.SwipeToDismiss
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
// import androidx.compose.material3.rememberDismissState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.ehviewer.ui.scene.HistoryComposeScreenFragmentBridge
|
||||
import com.hippo.ehviewer.ui.widget.GalleryListLongClickDialog
|
||||
import com.hippo.ehviewer.ui.widget.ListInfoCard
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun HistoryScreen(hostFragment: HistoryComposeScreenFragmentBridge) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val historyData = remember {
|
||||
Pager(
|
||||
PagingConfig(20)
|
||||
) {
|
||||
EhDB.getHistoryLazyList()
|
||||
}.flow
|
||||
}.collectAsLazyPagingItems()
|
||||
|
||||
var dialogStatus by remember { mutableStateOf(HistoryScreenDialogStatus.NONE) }
|
||||
var selectedGalleryInfo by remember { mutableStateOf<GalleryInfo?>(null) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = stringResource(id = R.string.history))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { hostFragment.toggleNavigationDrawer() }) {
|
||||
Icon(imageVector = Icons.Default.Menu, contentDescription = "")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { dialogStatus = HistoryScreenDialogStatus.CLEAR_ALL }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.v_clear_all_dark_x24),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
contentPadding = paddingValues
|
||||
) {
|
||||
items(
|
||||
items = historyData,
|
||||
key = { item -> item.gid }
|
||||
) { info ->
|
||||
info?.let {
|
||||
/*
|
||||
val dismissState = rememberDismissState(
|
||||
confirmValueChange = {
|
||||
if (it == DismissValue.DismissedToStart)
|
||||
coroutineScope.launchIO {
|
||||
EhDB.deleteHistoryInfo(info)
|
||||
}
|
||||
true
|
||||
}
|
||||
)
|
||||
|
||||
SwipeToDismiss(
|
||||
state = dismissState,
|
||||
background = {
|
||||
val color by animateColorAsState(
|
||||
when (dismissState.targetValue) {
|
||||
DismissValue.Default -> Color.Transparent
|
||||
DismissValue.DismissedToEnd -> Color.Green
|
||||
DismissValue.DismissedToStart -> Color.Red
|
||||
}
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(color)
|
||||
)
|
||||
},
|
||||
dismissContent = {
|
||||
// TODO: item delete & add animation
|
||||
// Bug tracker: https://issuetracker.google.com/issues/150812265
|
||||
ListInfoCard(
|
||||
{ hostFragment.navToDetail(it) },
|
||||
{
|
||||
selectedGalleryInfo = it
|
||||
dialogStatus = HistoryScreenDialogStatus.SELECT_ITEM
|
||||
},
|
||||
info = it,
|
||||
modifier = Modifier.animateItemPlacement()
|
||||
)
|
||||
},
|
||||
directions = setOf(DismissDirection.EndToStart)
|
||||
)
|
||||
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (historyData.itemCount == 0) {
|
||||
NoHistory()
|
||||
}
|
||||
}
|
||||
|
||||
if (dialogStatus == HistoryScreenDialogStatus.CLEAR_ALL) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { dialogStatus = HistoryScreenDialogStatus.NONE },
|
||||
title = {
|
||||
Text(text = stringResource(id = R.string.clear_all))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.clear_all_history))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
coroutineScope.launchIO {
|
||||
EhDB.clearHistoryInfo()
|
||||
dialogStatus = HistoryScreenDialogStatus.NONE
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(id = R.string.clear_all))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dialogStatus = HistoryScreenDialogStatus.NONE
|
||||
}
|
||||
) {
|
||||
Text(stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (dialogStatus == HistoryScreenDialogStatus.SELECT_ITEM) {
|
||||
selectedGalleryInfo?.let { info ->
|
||||
GalleryListLongClickDialog(
|
||||
info = info,
|
||||
{ dialogStatus = HistoryScreenDialogStatus.NONE },
|
||||
{ hostFragment.showTip(it, BaseScene.LENGTH_LONG) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoHistory() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.big_history),
|
||||
contentDescription = null,
|
||||
Modifier.padding(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.no_history),
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class HistoryScreenDialogStatus {
|
||||
NONE,
|
||||
CLEAR_ALL,
|
||||
SELECT_ITEM
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.hippo.ehviewer.ui.widget
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import eu.kanade.tachiyomi.util.system.pxToDp
|
||||
import java.util.Locale
|
||||
|
||||
private val downloadManager = EhApplication.downloadManager
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ListInfoCard(
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
info: GalleryInfo,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = modifier.padding(6.dp),
|
||||
border = remember { BorderStroke(1.dp, Color.Transparent) }
|
||||
) {
|
||||
val listCardSize = remember { Settings.getListThumbSize().pxToDp }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height((listCardSize * 3).dp)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
),
|
||||
) {
|
||||
Card {
|
||||
AsyncImage(
|
||||
model = info.thumb,
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.width((listCardSize * 2).dp)
|
||||
.height((listCardSize * 3).dp)
|
||||
)
|
||||
}
|
||||
Column(Modifier.padding(8.dp, 4.dp)) {
|
||||
Text(
|
||||
text = EhUtils.getSuitableTitle(info),
|
||||
maxLines = 2,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
Column {
|
||||
Text(
|
||||
text = info.uploader ?: "(DISOWNED)",
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
SimpleRatingWidget(rating = info.rating)
|
||||
val categoryColor = remember { EhUtils.getCategoryColor(info.category) }
|
||||
Text(
|
||||
text = EhUtils.getCategory(info.category).uppercase(Locale.ROOT),
|
||||
modifier = Modifier
|
||||
.background(Color(categoryColor))
|
||||
.padding(vertical = 2.dp, horizontal = 8.dp),
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// TODO: put this calculation in coroutines
|
||||
if (downloadManager.containDownloadInfo(info.gid)) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.v_download_x16),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
if (info.favoriteSlot != -2) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.v_heart_x16),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = info.simpleLanguage.orEmpty(),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = info.posted.orEmpty(),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
package com.hippo.ehviewer.ui.widget
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
private val downloadManager = EhApplication.downloadManager
|
||||
|
||||
@Composable
|
||||
private fun DialogSelectorItem(
|
||||
onClick: () -> Unit,
|
||||
icon: Painter,
|
||||
text: String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@Composable
|
||||
fun <T : GalleryInfo> GalleryListLongClickDialog(
|
||||
info: T,
|
||||
onDismissRequest: () -> Unit,
|
||||
showTip: (String) -> Unit
|
||||
) {
|
||||
var dialogStatus by remember { mutableStateOf(DialogStatus.PRIMARY) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val removeFromFavouriteSuccess = stringResource(id = R.string.remove_from_favorite_success)
|
||||
val removeFromFavouriteFailure = stringResource(id = R.string.remove_from_favorite_failure)
|
||||
val localFavName = stringResource(id = R.string.local_favorites)
|
||||
val addToFavSuccess = stringResource(id = R.string.add_to_favorite_success)
|
||||
val addToFavFailed = stringResource(id = R.string.add_to_favorite_failure)
|
||||
|
||||
suspend fun addToFav(slot: Int) {
|
||||
val favNameList = Settings.getFavCat().toMutableList().apply { add(0, localFavName) }
|
||||
runCatching {
|
||||
if (slot == -1) {
|
||||
EhDB.putLocalFavorites(info)
|
||||
} else {
|
||||
EhEngine.addFavorites(
|
||||
info.gid,
|
||||
info.token,
|
||||
slot,
|
||||
""
|
||||
)
|
||||
info.favoriteSlot = slot
|
||||
info.favoriteName = favNameList[slot + 1]
|
||||
EhDB.putHistoryInfoNonRefresh(info)
|
||||
}
|
||||
}.onSuccess {
|
||||
showTip(addToFavSuccess)
|
||||
}.onFailure {
|
||||
showTip(addToFavFailed)
|
||||
}
|
||||
onDismissRequest()
|
||||
dialogStatus = DialogStatus.PRIMARY
|
||||
}
|
||||
|
||||
when (dialogStatus) {
|
||||
DialogStatus.PRIMARY -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dialogStatus = DialogStatus.PRIMARY
|
||||
onDismissRequest()
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = EhUtils.getSuitableTitle(info),
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
DialogSelectorItem(
|
||||
onClick = {
|
||||
val intent = Intent(context, ReaderActivity::class.java)
|
||||
intent.action = ReaderActivity.ACTION_EH
|
||||
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, info)
|
||||
startActivity(context, intent, null)
|
||||
onDismissRequest()
|
||||
dialogStatus = DialogStatus.PRIMARY
|
||||
},
|
||||
icon = painterResource(id = R.drawable.v_book_open_x24),
|
||||
text = stringResource(id = R.string.read)
|
||||
)
|
||||
if (remember { downloadManager.containDownloadInfo(info.gid) }) {
|
||||
DialogSelectorItem(
|
||||
onClick = {
|
||||
dialogStatus = DialogStatus.CONFIRM_REMOVE_DOWNLOAD
|
||||
},
|
||||
icon = painterResource(id = R.drawable.v_delete_x24),
|
||||
text = stringResource(id = R.string.delete_downloads)
|
||||
)
|
||||
} else {
|
||||
DialogSelectorItem(
|
||||
onClick = {
|
||||
dialogStatus = DialogStatus.PRIMARY
|
||||
},
|
||||
icon = painterResource(id = R.drawable.v_download_x24),
|
||||
text = stringResource(id = R.string.download)
|
||||
)
|
||||
}
|
||||
if (info.favoriteSlot == -2) {
|
||||
DialogSelectorItem(
|
||||
onClick = {
|
||||
val defaultFav = Settings.getDefaultFavSlot()
|
||||
if (defaultFav in -1..9) {
|
||||
launchIO {
|
||||
addToFav(defaultFav)
|
||||
}
|
||||
} else {
|
||||
dialogStatus = DialogStatus.SELECT_FAVOURITE
|
||||
}
|
||||
},
|
||||
icon = painterResource(id = R.drawable.v_heart_x24),
|
||||
text = stringResource(id = R.string.add_to_favourites)
|
||||
)
|
||||
} else {
|
||||
DialogSelectorItem(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
dialogStatus = DialogStatus.PRIMARY
|
||||
launchIO {
|
||||
runCatching {
|
||||
EhDB.removeLocalFavorites(info.gid)
|
||||
EhEngine.addFavorites(info.gid, info.token, -1, "")
|
||||
info.favoriteSlot = -2
|
||||
info.favoriteName = null
|
||||
EhDB.putHistoryInfoNonRefresh(info)
|
||||
showTip(removeFromFavouriteSuccess)
|
||||
}.onFailure {
|
||||
showTip(removeFromFavouriteFailure)
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = painterResource(id = R.drawable.v_heart_broken_x24),
|
||||
text = stringResource(id = R.string.remove_from_favourites)
|
||||
)
|
||||
}
|
||||
if (remember { downloadManager.containDownloadInfo(info.gid) }) {
|
||||
DialogSelectorItem(
|
||||
onClick = {
|
||||
dialogStatus = DialogStatus.PRIMARY
|
||||
},
|
||||
icon = painterResource(id = R.drawable.v_folder_move_x24),
|
||||
text = stringResource(id = R.string.download_move_dialog_title)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {}
|
||||
)
|
||||
}
|
||||
|
||||
DialogStatus.CONFIRM_REMOVE_DOWNLOAD -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
onDismissRequest()
|
||||
dialogStatus = DialogStatus.PRIMARY
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(id = R.string.download_remove_dialog_title))
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.download_remove_dialog_message,
|
||||
info.title.orEmpty()
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onDismissRequest()
|
||||
dialogStatus = DialogStatus.PRIMARY
|
||||
downloadManager.deleteDownload(info.gid)
|
||||
}) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
DialogStatus.SELECT_FAVOURITE -> {
|
||||
SelectFavouriteDialog(
|
||||
onFolderSelected = {
|
||||
launchIO {
|
||||
addToFav(it)
|
||||
}
|
||||
},
|
||||
onDismissRequest = {
|
||||
onDismissRequest()
|
||||
dialogStatus = DialogStatus.PRIMARY
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectFavouriteDialog(
|
||||
onFolderSelected: (Int) -> Unit,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
var rememberFavSlot by remember { mutableStateOf(false) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(checked = rememberFavSlot, onCheckedChange = { rememberFavSlot = it })
|
||||
Text(text = stringResource(id = R.string.remember_favorite_collection))
|
||||
}
|
||||
},
|
||||
title = { Text(text = stringResource(id = R.string.add_favorites_dialog_title)) },
|
||||
text = {
|
||||
val localFavName = stringResource(id = R.string.local_favorites)
|
||||
Column {
|
||||
remember {
|
||||
Settings.getFavCat().toMutableList().apply { add(0, localFavName) }
|
||||
}.forEachIndexed { index, s ->
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
Settings.putDefaultFavSlot(if (rememberFavSlot) index - 1 else Settings.INVALID_DEFAULT_FAV_SLOT)
|
||||
onFolderSelected(index - 1)
|
||||
},
|
||||
Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private enum class DialogStatus {
|
||||
PRIMARY,
|
||||
CONFIRM_REMOVE_DOWNLOAD,
|
||||
SELECT_FAVOURITE
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.hippo.ehviewer.ui.widget
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
@Composable
|
||||
fun SimpleRatingWidget(rating: Float) {
|
||||
val r = (rating * 2).toInt().coerceAtLeast(0).coerceAtMost(10)
|
||||
val fullStar = r.floorDiv(2)
|
||||
val halfStar = r % 2
|
||||
val outlineStar = 5 - fullStar - halfStar
|
||||
val colorYellow800 = Color(0xfff9a825)
|
||||
Row {
|
||||
repeat(fullStar) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.v_star_x16),
|
||||
contentDescription = null,
|
||||
tint = colorYellow800
|
||||
)
|
||||
Spacer(modifier = Modifier.width(dimensionResource(id = R.dimen.rating_interval)))
|
||||
}
|
||||
repeat(halfStar) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.v_star_half_x16),
|
||||
contentDescription = null,
|
||||
tint = colorYellow800
|
||||
)
|
||||
Spacer(modifier = Modifier.width(dimensionResource(id = R.dimen.rating_interval)))
|
||||
}
|
||||
repeat(outlineStar) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.v_star_outline_x16),
|
||||
contentDescription = null,
|
||||
tint = colorYellow800
|
||||
)
|
||||
Spacer(modifier = Modifier.width(dimensionResource(id = R.dimen.rating_interval)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,11 @@ package com.hippo.ehviewer.widget;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.ImageDecoder;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.AbsSavedState;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -37,9 +37,8 @@ import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder;
|
||||
import com.hippo.ehviewer.client.exception.EhException;
|
||||
import com.hippo.io.UniFileInputStreamPipe;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.util.BitmapUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
@@ -47,6 +46,7 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
@@ -121,8 +121,8 @@ public final class ImageSearchLayout extends LinearLayout implements View.OnClic
|
||||
}
|
||||
|
||||
try {
|
||||
int maxSize = context.getResources().getDimensionPixelOffset(R.dimen.image_search_max_size);
|
||||
Bitmap bitmap = BitmapUtils.decodeStream(new UniFileInputStreamPipe(file), maxSize, maxSize);
|
||||
var src = ImageDecoder.createSource(context.getContentResolver(), imageUri);
|
||||
Bitmap bitmap = ImageDecoder.decodeBitmap(src, Image.getImageSearchDecoderSampleListener());
|
||||
if (null == bitmap) {
|
||||
return;
|
||||
}
|
||||
@@ -145,8 +145,8 @@ public final class ImageSearchLayout extends LinearLayout implements View.OnClic
|
||||
} finally {
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.e(TAG, "Out of memory");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,45 +21,63 @@ import android.graphics.ColorSpace
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.ImageDecoder.ImageInfo
|
||||
import android.graphics.ImageDecoder.Source
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.AnimatedImageDrawable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.min
|
||||
|
||||
class Image private constructor(source: Source, private var src: ByteBufferSource?) {
|
||||
class Image private constructor(private val src: CloseableSource) {
|
||||
var mObtainedDrawable: Drawable? =
|
||||
ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, info: ImageInfo, _: Source ->
|
||||
ImageDecoder.decodeDrawable(src.source) { decoder: ImageDecoder, info: ImageInfo, _: Source ->
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
|
||||
// Allocating hardware bitmap may cause a crash on framework versions prior to Android Q
|
||||
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||
}
|
||||
decoder.setTargetColorSpace(colorSpace)
|
||||
decoder.setTargetSampleSize(
|
||||
min(
|
||||
info.size.width / (2 * screenWidth),
|
||||
info.size.height / (2 * screenHeight)
|
||||
).coerceAtLeast(1)
|
||||
calculateSampleSize(info, 2 * screenHeight, 2 * screenWidth)
|
||||
)
|
||||
}.also {
|
||||
(it as? BitmapDrawable)?.run {
|
||||
src?.close()
|
||||
src = null
|
||||
src.close()
|
||||
}
|
||||
}
|
||||
private set
|
||||
|
||||
val size: Int
|
||||
get() = mObtainedDrawable!!.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 }
|
||||
get() = mObtainedDrawable!!.run { intrinsicHeight * intrinsicWidth * 4 * if (this is AnimatedImageDrawable) 4 else 1 }
|
||||
|
||||
@Synchronized
|
||||
fun recycle() {
|
||||
(mObtainedDrawable as? Animatable)?.stop()
|
||||
(mObtainedDrawable as? AnimatedImageDrawable)?.stop()
|
||||
(mObtainedDrawable as? BitmapDrawable)?.bitmap?.recycle()
|
||||
mObtainedDrawable?.callback = null
|
||||
mObtainedDrawable = null
|
||||
src?.close()
|
||||
src = null
|
||||
(mObtainedDrawable as? AnimatedImageDrawable)?.let { src.close() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun calculateSampleSize(info: ImageInfo, targetHeight: Int, targetWeight: Int): Int {
|
||||
return min(
|
||||
info.size.width / targetWeight,
|
||||
info.size.height / targetHeight
|
||||
).coerceAtLeast(1)
|
||||
}
|
||||
|
||||
private val imageSearchMaxSize =
|
||||
EhApplication.application.resources.getDimensionPixelOffset(R.dimen.image_search_max_size)
|
||||
|
||||
@JvmStatic
|
||||
val imageSearchDecoderSampleListener =
|
||||
ImageDecoder.OnHeaderDecodedListener { decoder, info, _ ->
|
||||
decoder.setTargetSampleSize(
|
||||
calculateSampleSize(info, imageSearchMaxSize, imageSearchMaxSize)
|
||||
)
|
||||
}
|
||||
val screenWidth = EhApplication.application.resources.displayMetrics.widthPixels
|
||||
val screenHeight = EhApplication.application.resources.displayMetrics.heightPixels
|
||||
val isWideColorGamut =
|
||||
@@ -71,12 +89,9 @@ class Image private constructor(source: Source, private var src: ByteBufferSourc
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun decode(src: ByteBufferSource): Image? {
|
||||
val directBuffer = src.getByteBuffer()
|
||||
check(directBuffer.isDirect)
|
||||
rewriteGifSource(directBuffer)
|
||||
fun decode(src: CloseableSource): Image? {
|
||||
return runCatching {
|
||||
Image(ImageDecoder.createSource(directBuffer), src)
|
||||
Image(src)
|
||||
}.onFailure {
|
||||
src.close()
|
||||
it.printStackTrace()
|
||||
@@ -84,11 +99,10 @@ class Image private constructor(source: Source, private var src: ByteBufferSourc
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun rewriteGifSource(buffer: ByteBuffer)
|
||||
external fun rewriteGifSource(buffer: ByteBuffer)
|
||||
}
|
||||
|
||||
interface ByteBufferSource : AutoCloseable {
|
||||
// A read-write direct bytebuffer, it may in native heap or file mapping
|
||||
fun getByteBuffer(): ByteBuffer
|
||||
interface CloseableSource : AutoCloseable {
|
||||
val source: Source
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.io;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class UniFileInputStreamPipe implements InputStreamPipe {
|
||||
|
||||
private final UniFile mFile;
|
||||
private InputStream mIs;
|
||||
|
||||
public UniFileInputStreamPipe(UniFile file) {
|
||||
mFile = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// Empty
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InputStream open() throws IOException {
|
||||
if (mIs != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
mIs = mFile.openInputStream();
|
||||
return mIs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
IOUtils.closeQuietly(mIs);
|
||||
mIs = null;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.io;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class UniFileOutputStreamPipe implements OutputStreamPipe {
|
||||
|
||||
private final UniFile mUniFile;
|
||||
private OutputStream mOs;
|
||||
|
||||
public UniFileOutputStreamPipe(UniFile uniFile) {
|
||||
mUniFile = uniFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// Empty
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public OutputStream open() throws IOException {
|
||||
if (mOs != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
mOs = mUniFile.openOutputStream();
|
||||
return mOs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
IOUtils.closeQuietly(mOs);
|
||||
mOs = null;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.streampipe;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface InputStreamPipe {
|
||||
|
||||
void release();
|
||||
|
||||
@NonNull
|
||||
InputStream open() throws IOException;
|
||||
|
||||
void close();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.streampipe;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface OutputStreamPipe {
|
||||
|
||||
void release();
|
||||
|
||||
@NonNull
|
||||
OutputStream open() throws IOException;
|
||||
|
||||
void close();
|
||||
}
|
||||
@@ -16,21 +16,16 @@
|
||||
|
||||
package com.hippo.unifile;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
final class DocumentsContractApi19 {
|
||||
private static final String TAG = DocumentsContractApi19.class.getSimpleName();
|
||||
|
||||
private DocumentsContractApi19() {
|
||||
}
|
||||
@@ -118,8 +113,6 @@ final class DocumentsContractApi19 {
|
||||
return DocumentsContract.deleteDocument(context.getContentResolver(), self);
|
||||
} catch (Throwable e) {
|
||||
Utils.throwIfFatal(e);
|
||||
// Maybe user ejects tf card
|
||||
Log.e(TAG, "Failed to renameTo", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +127,6 @@ final class DocumentsContractApi19 {
|
||||
return null != c && c.getCount() > 0;
|
||||
} catch (Throwable e) {
|
||||
Utils.throwIfFatal(e);
|
||||
// Log.w(TAG, "Failed query: " + e);
|
||||
return false;
|
||||
} finally {
|
||||
Utils.closeQuietly(c);
|
||||
|
||||
@@ -16,21 +16,16 @@
|
||||
|
||||
package com.hippo.unifile;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
final class DocumentsContractApi21 {
|
||||
private static final String TAG = DocumentsContractApi21.class.getSimpleName();
|
||||
private static final String PATH_DOCUMENT = "document";
|
||||
private static final String PATH_TREE = "tree";
|
||||
|
||||
@@ -44,8 +39,6 @@ final class DocumentsContractApi21 {
|
||||
displayName);
|
||||
} catch (Throwable e) {
|
||||
Utils.throwIfFatal(e);
|
||||
// Maybe user ejects tf card
|
||||
Log.e(TAG, "Failed to createFile: " + self, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -92,12 +85,11 @@ final class DocumentsContractApi21 {
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Utils.throwIfFatal(e);
|
||||
Log.e(TAG, "Failed listFiles: " + self, e);
|
||||
} finally {
|
||||
Utils.closeQuietly(c);
|
||||
}
|
||||
|
||||
return results.toArray(new Uri[results.size()]);
|
||||
return results.toArray(new Uri[0]);
|
||||
}
|
||||
|
||||
public static Uri renameTo(Context context, Uri self, String displayName) {
|
||||
@@ -105,8 +97,6 @@ final class DocumentsContractApi21 {
|
||||
return DocumentsContract.renameDocument(context.getContentResolver(), self, displayName);
|
||||
} catch (Throwable e) {
|
||||
Utils.throwIfFatal(e);
|
||||
// Maybe user ejects tf card
|
||||
Log.e(TAG, "Failed to renameTo:" + self + ", " + displayName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.hippo.util
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spanned
|
||||
import android.text.style.*
|
||||
|
||||
object BBCode {
|
||||
fun Spanned.toBBCode(): String {
|
||||
val out = StringBuilder()
|
||||
withinParagraph(out, this, this.length)
|
||||
return out.toString()
|
||||
}
|
||||
|
||||
private fun withinParagraph(out: StringBuilder, text: Spanned, end: Int) {
|
||||
var next: Int
|
||||
var i = 0
|
||||
while (i < text.length) {
|
||||
next = text.nextSpanTransition(i, end, CharacterStyle::class.java)
|
||||
val style = text.getSpans(i, next, CharacterStyle::class.java)
|
||||
for (j in style.indices) {
|
||||
if (style[j] is StyleSpan) {
|
||||
val s = (style[j] as StyleSpan).style
|
||||
if (s and Typeface.BOLD != 0) {
|
||||
out.append("[b]")
|
||||
}
|
||||
if (s and Typeface.ITALIC != 0) {
|
||||
out.append("[i]")
|
||||
}
|
||||
}
|
||||
if (style[j] is UnderlineSpan) {
|
||||
out.append("[u]")
|
||||
}
|
||||
if (style[j] is StrikethroughSpan) {
|
||||
out.append("[s]")
|
||||
}
|
||||
if (style[j] is URLSpan) {
|
||||
out.append("[url=")
|
||||
out.append((style[j] as URLSpan).url)
|
||||
out.append("]")
|
||||
}
|
||||
if (style[j] is ImageSpan) {
|
||||
out.append("[img]")
|
||||
out.append((style[j] as ImageSpan).source)
|
||||
out.append("[/img]")
|
||||
}
|
||||
}
|
||||
out.append(text.subSequence(i, next))
|
||||
for (j in style.indices.reversed()) {
|
||||
if (style[j] is URLSpan) {
|
||||
out.append("[/url]")
|
||||
}
|
||||
if (style[j] is StrikethroughSpan) {
|
||||
out.append("[/s]")
|
||||
}
|
||||
if (style[j] is UnderlineSpan) {
|
||||
out.append("[/u]")
|
||||
}
|
||||
if (style[j] is StyleSpan) {
|
||||
val s = (style[j] as StyleSpan).style
|
||||
if (s and Typeface.BOLD != 0) {
|
||||
out.append("[/b]")
|
||||
}
|
||||
if (s and Typeface.ITALIC != 0) {
|
||||
out.append("[/i]")
|
||||
}
|
||||
}
|
||||
}
|
||||
i = next
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.util;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class BitmapUtils {
|
||||
public static ActivityManager activityManager;
|
||||
|
||||
private BitmapUtils() {
|
||||
}
|
||||
|
||||
public static void initialize(Context context) {
|
||||
activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
}
|
||||
|
||||
public static long availableMemory() {
|
||||
final Runtime runtime = Runtime.getRuntime();
|
||||
final long used = runtime.totalMemory() - runtime.freeMemory();
|
||||
|
||||
final long total = (long) activityManager.getMemoryClass() * 1024 * 1024;
|
||||
|
||||
return total - used;
|
||||
}
|
||||
|
||||
public static Bitmap decodeStream(@NonNull InputStreamPipe isp, int maxWidth, int maxHeight,
|
||||
int pixels, boolean checkMemory, boolean justCalc, int[] sampleSize) {
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(isp.open(), null, options);
|
||||
isp.close();
|
||||
|
||||
int width = options.outWidth;
|
||||
int height = options.outHeight;
|
||||
if (width <= 0 || height <= 0) {
|
||||
if (sampleSize != null && sampleSize.length >= 1) {
|
||||
sampleSize[0] = -1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int scaleW = 1;
|
||||
int scaleH = 1;
|
||||
int scaleP = 1;
|
||||
int scaleM = 1;
|
||||
if (maxWidth > 0 && width > maxWidth) {
|
||||
scaleW = MathUtils.ceilDivide(width, maxWidth);
|
||||
}
|
||||
if (maxHeight > 0 && height > maxHeight) {
|
||||
scaleH = MathUtils.ceilDivide(height, maxHeight);
|
||||
}
|
||||
if (pixels > 0 && width * height > pixels) {
|
||||
scaleP = (int) Math.ceil(Math.sqrt(width * height / (float) pixels));
|
||||
}
|
||||
if (checkMemory) {
|
||||
long m = availableMemory() - 5 * 1024 * 1024; // Leave 5m
|
||||
if (m < 0) {
|
||||
if (sampleSize != null && sampleSize.length >= 1) {
|
||||
sampleSize[0] = -1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if ((long) width * height * 3 > m) {
|
||||
scaleM = (int) Math.ceil(Math.sqrt(width * height * 3 / (float) m));
|
||||
}
|
||||
}
|
||||
options.inSampleSize = MathUtils.nextPowerOf2(MathUtils.max(scaleW, scaleH, scaleP, scaleM, 1));
|
||||
if (sampleSize != null && sampleSize.length >= 1) {
|
||||
sampleSize[0] = options.inSampleSize;
|
||||
}
|
||||
|
||||
options.inJustDecodeBounds = false;
|
||||
|
||||
if (justCalc) {
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
return BitmapFactory.decodeStream(isp.open(), null, options);
|
||||
} catch (OutOfMemoryError e) {
|
||||
if (sampleSize != null && sampleSize.length >= 1) {
|
||||
sampleSize[0] = -1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (sampleSize != null && sampleSize.length >= 1) {
|
||||
sampleSize[0] = -1;
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
isp.close();
|
||||
isp.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null or the bitmap
|
||||
*/
|
||||
public static Bitmap decodeStream(@NonNull InputStreamPipe isp, int maxWidth, int maxHeight) {
|
||||
return decodeStream(isp, maxWidth, maxHeight, -1, false, false, null);
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ infix fun Context.tellClipboardWithToast(text: String?) {
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun Context.addTextToClipboard(text: String?, isSensitive: Boolean, useToast: Boolean = false) {
|
||||
fun Context.addTextToClipboard(text: CharSequence?, isSensitive: Boolean, useToast: Boolean = false) {
|
||||
getClipboardManager().apply {
|
||||
setPrimaryClip(ClipData.newPlainText(null, text).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isSensitive)
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Hippo Seven
|
||||
*
|
||||
* 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 com.hippo.util;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Implements natural sort order.
|
||||
*/
|
||||
public class NaturalComparator implements Comparator<String> {
|
||||
|
||||
private static boolean isDigit(String str) {
|
||||
// Just check the first char
|
||||
char ch = str.charAt(0);
|
||||
return ch >= '0' && ch <= '9';
|
||||
}
|
||||
|
||||
private static String nextSlice(String str, int index) {
|
||||
int length = str.length();
|
||||
if (index == length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
char ch = str.charAt(index);
|
||||
if (ch == '.' || ch == ' ') {
|
||||
return str.substring(index, index + 1);
|
||||
} else if (ch >= '0' && ch <= '9') {
|
||||
return str.substring(index, nextNumberBound(str, index + 1));
|
||||
} else {
|
||||
return str.substring(index, nextOtherBound(str, index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private static int nextNumberBound(String str, int index) {
|
||||
for (int length = str.length(); index < length; index++) {
|
||||
char ch = str.charAt(index);
|
||||
if (ch < '0' || ch > '9') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private static int nextOtherBound(String str, int index) {
|
||||
for (int length = str.length(); index < length; index++) {
|
||||
char ch = str.charAt(index);
|
||||
if (ch == '.' || ch == ' ' || (ch >= '0' && ch <= '9')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private static String removeLeadingZero(String s) {
|
||||
if (s.length() < 1) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// At least keep the last number
|
||||
for (int i = 0, n = s.length() - 1; i < n; i++) {
|
||||
if (s.charAt(i) != '0') {
|
||||
return s.substring(i);
|
||||
}
|
||||
}
|
||||
|
||||
return s.substring(s.length() - 1);
|
||||
}
|
||||
|
||||
private static int compareNumberString(String s1, String s2) {
|
||||
String p1 = removeLeadingZero(s1);
|
||||
String p2 = removeLeadingZero(s2);
|
||||
|
||||
int l1 = p1.length();
|
||||
int l2 = p2.length();
|
||||
|
||||
if (l1 > l2) {
|
||||
return 1;
|
||||
} else if (l1 < l2) {
|
||||
return -1;
|
||||
} else {
|
||||
for (int i = 0; i < l1; i++) {
|
||||
char c1 = p1.charAt(i);
|
||||
char c2 = p2.charAt(i);
|
||||
if (c1 > c2) {
|
||||
return 1;
|
||||
} else if (c1 < c2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -Integer.compare(s1.length(), s2.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(String o1, String o2) {
|
||||
if (o1 == null && o2 == null) {
|
||||
return 0;
|
||||
}
|
||||
if (o1 == null) {
|
||||
return -1;
|
||||
}
|
||||
if (o2 == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int index1 = 0;
|
||||
int index2 = 0;
|
||||
while (true) {
|
||||
String data1 = nextSlice(o1, index1);
|
||||
String data2 = nextSlice(o2, index2);
|
||||
|
||||
if (data1 == null && data2 == null) {
|
||||
return 0;
|
||||
}
|
||||
if (data1 == null) {
|
||||
return -1;
|
||||
}
|
||||
if (data2 == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
index1 += data1.length();
|
||||
index2 += data2.length();
|
||||
|
||||
int result;
|
||||
if (isDigit(data1) && isDigit(data2)) {
|
||||
result = compareNumberString(data1, data2);
|
||||
} else {
|
||||
result = data1.compareToIgnoreCase(data2);
|
||||
}
|
||||
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.hippo.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.os.BundleCompat
|
||||
|
||||
inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String?): T? {
|
||||
return BundleCompat.getParcelable(this, key, T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String?): T? {
|
||||
return IntentCompat.getParcelableExtra(this, key, T::class.java)
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.util.getParcelableCompat
|
||||
import com.hippo.yorozuya.AnimationUtils
|
||||
import com.hippo.yorozuya.SimpleAnimatorListener
|
||||
|
||||
@@ -401,7 +402,7 @@ class FabLayout @JvmOverloads constructor(
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
if (state is Bundle) {
|
||||
super.onRestoreInstanceState(state.getParcelable(STATE_KEY_SUPER))
|
||||
super.onRestoreInstanceState(state.getParcelableCompat(STATE_KEY_SUPER))
|
||||
setAutoCancel(state.getBoolean(STATE_KEY_AUTO_CANCEL))
|
||||
setExpanded(state.getBoolean(STATE_KEY_EXPANDED), false)
|
||||
}
|
||||
|
||||
@@ -128,14 +128,14 @@ open class LoadImageView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun load(key: String, url: String, isLoaded: Boolean = false) {
|
||||
fun load(url: String, isLoaded: Boolean = false) {
|
||||
if ((mRequest?.data as? String?) == url)
|
||||
return
|
||||
mFailed = false
|
||||
clearRetry()
|
||||
task?.dispose()
|
||||
mRequest =
|
||||
ImageRequest.Builder(context).data(url).memoryCacheKey(key).diskCacheKey(key)
|
||||
ImageRequest.Builder(context).data(url).crossfade(false)
|
||||
.size(Size.ORIGINAL).target(
|
||||
{ onWait() },
|
||||
{ onFailure() },
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.hippo.yorozuya
|
||||
|
||||
fun String.unescapeXml(): String {
|
||||
return StringUtils.unescapeXml(this)
|
||||
}
|
||||
Loaded 100 of 145 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user