Compare commits
303
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63cd60b237 | ||
|
|
0df8e8522f | ||
|
|
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 | ||
|
|
d7fcda1f83 | ||
|
|
b832c24185 | ||
|
|
3b04f8c695 | ||
|
|
0ef11b58d2 | ||
|
|
bf13bbf506 | ||
|
|
92c94aa22a | ||
|
|
1c0785432b | ||
|
|
dcb9a50061 | ||
|
|
de7f4bdb6e | ||
|
|
73d636dba6 | ||
|
|
3f1c453ff6 | ||
|
|
ecb6baaebb | ||
|
|
e1cba45486 | ||
|
|
ea4181b243 | ||
|
|
6c5b9f8851 | ||
|
|
c1fccaf240 | ||
|
|
e7c223da7d | ||
|
|
6bfe1ba876 | ||
|
|
574e5dab18 | ||
|
|
5dc9b83831 | ||
|
|
57a214a9a4 | ||
|
|
cf48eb94af | ||
|
|
683f1e488a | ||
|
|
e888ae4da7 | ||
|
|
4512f2a375 | ||
|
|
68442afa0f | ||
|
|
ddf4d0cbc3 | ||
|
|
4be185e428 | ||
|
|
f75e66ac19 | ||
|
|
abda781f02 | ||
|
|
c1533ec66d | ||
|
|
c8319446b0 | ||
|
|
7ae452cf13 | ||
|
|
4c024c5faa | ||
|
|
9d16e9d98c | ||
|
|
48357d08f7 | ||
|
|
8e0bcf40db | ||
|
|
85ef5f04dd | ||
|
|
18e0748d14 | ||
|
|
cdde5b098e | ||
|
|
f83964752d | ||
|
|
0c3e34bbf9 | ||
|
|
6dea7faac8 | ||
|
|
726f482c33 | ||
|
|
cb242f664a | ||
|
|
edc0345970 | ||
|
|
a50535dd76 | ||
|
|
505942aa25 | ||
|
|
38a593a9de | ||
|
|
da87fb2551 | ||
|
|
d4dc65a4ba | ||
|
|
53463caedd | ||
|
|
0707a0fad8 | ||
|
|
dd1024af27 | ||
|
|
7edc58a6da | ||
|
|
5a5da0cace | ||
|
|
53f02ed366 | ||
|
|
27cfa885e7 | ||
|
|
beb132d604 | ||
|
|
b1b10298b4 | ||
|
|
b1993ff7d2 | ||
|
|
2afe5fe5b2 | ||
|
|
52fa8f64cc | ||
|
|
4f7978070f | ||
|
|
a909983822 | ||
|
|
5267cad896 | ||
|
|
ce274cf65b | ||
|
|
55b6b2e34d | ||
|
|
5e4e2971b5 | ||
|
|
a933bcfb1e | ||
|
|
b4b38e27f3 | ||
|
|
bc70824b57 | ||
|
|
87ecaaa6a4 | ||
|
|
c4cd9cd802 | ||
|
|
bbe0e8c5a9 | ||
|
|
3f8984d2dc |
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,7 +66,7 @@ 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
|
||||
|
||||
|
||||
@@ -1,165 +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 180030
|
||||
versionName "1.8.5.3-alpha05"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
|
||||
}
|
||||
|
||||
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/**"
|
||||
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-dev-k1.8.0-33c0ad36f83"
|
||||
}
|
||||
|
||||
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 '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 '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-alpha04"
|
||||
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
|
||||
|
||||
|
||||
+6
-11
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2016 Hippo Seven
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -14,14 +15,8 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:tint="@color/brown_500"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="@string/pd_cookie" />
|
||||
<resources>
|
||||
|
||||
</vector>
|
||||
<string name="app_name" translatable="false">EhViewer Debug</string>
|
||||
|
||||
</resources>
|
||||
@@ -34,7 +34,7 @@
|
||||
android:theme="@style/AppTheme"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name=".ui.LoginActivity"
|
||||
android:name=".ui.login.LoginActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
|
||||
@@ -45,6 +45,6 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
|
||||
add_subdirectory(libarchive)
|
||||
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifUtils.c)
|
||||
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
|
||||
@@ -1,5 +1,16 @@
|
||||
#include <jni.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define LOG_TAG "mmap_utils"
|
||||
|
||||
#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;
|
||||
@@ -13,4 +24,4 @@ Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#include <string.h>
|
||||
#include <jni.h>
|
||||
|
||||
#define GIF_HEADER_87A "GIF87a"
|
||||
#define GIF_HEADER_89A "GIF89a"
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_Native_isGif(JNIEnv *env, jobject thiz, jobject buffer) {
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#include <jni.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#define LOG_TAG "gifUtils"
|
||||
|
||||
#include "ehviewer.h"
|
||||
|
||||
#define GIF_HEADER_87A "GIF87a"
|
||||
#define GIF_HEADER_89A "GIF89a"
|
||||
|
||||
static int FRAME_DELAY_START_MARKER = 0x0021F904;
|
||||
|
||||
typedef signed char byte;
|
||||
|
||||
#define FRAME_DELAY_START_MARKER ((byte*)(&FRAME_DELAY_START_MARKER))
|
||||
#define MINIMUM_FRAME_DELAY 2
|
||||
#define DEFAULT_FRAME_DELAY 10
|
||||
|
||||
static inline bool isGif(void *addr) {
|
||||
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_image_Image_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
if (size < 7 || !isGif(addr)) return;
|
||||
for (size_t i = 0; i < size - 8; i++) {
|
||||
// TODO: Optimize this hex find with SIMD?
|
||||
if (addr[i] == FRAME_DELAY_START_MARKER[3] && addr[i + 1] == FRAME_DELAY_START_MARKER[2] &&
|
||||
addr[i + 2] == FRAME_DELAY_START_MARKER[1] &&
|
||||
addr[i + 3] == FRAME_DELAY_START_MARKER[0]) {
|
||||
byte *end = addr + i + 4;
|
||||
if (end[4] != 0) continue;
|
||||
int frameDelay = end[2] << 8 | end[1];
|
||||
if (frameDelay > MINIMUM_FRAME_DELAY)
|
||||
break; // Quit if the first block looks normal, for performance
|
||||
end[1] = DEFAULT_FRAME_DELAY;
|
||||
end[2] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -476,10 +476,8 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
|
||||
private fun setGestureDetector(context: Context) {
|
||||
detector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
|
||||
// The e1 can be null, that's weird and cause crash
|
||||
if (panEnabled && isReady && vTranslate != null && e1 != null && e2 != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
|
||||
if (panEnabled && isReady && vTranslate != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
|
||||
val vTranslateEnd = PointF(vTranslate!!.x + velocityX * 0.25F, vTranslate!!.y + velocityY * 0.25F)
|
||||
val sCenterXEnd = (width / 2 - vTranslateEnd.x) / scale
|
||||
val sCenterYEnd = (height / 2 - vTranslateEnd.y) / scale
|
||||
@@ -607,12 +605,16 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
return true
|
||||
}
|
||||
// Detect flings, taps and double taps
|
||||
if (!isQuickScaling && detector?.onTouchEvent(event) != false) {
|
||||
isZooming = false
|
||||
isPanning = false
|
||||
maxTouchCount = 0
|
||||
return true
|
||||
// May throw NPE
|
||||
runCatching {
|
||||
if (!isQuickScaling && detector?.onTouchEvent(event) != false) {
|
||||
isZooming = false
|
||||
isPanning = false
|
||||
maxTouchCount = 0
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (vTranslateStart == null) {
|
||||
vTranslateStart = PointF(0F, 0F)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package com.hippo
|
||||
|
||||
import java.io.FileDescriptor
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object Native {
|
||||
fun initialize() {
|
||||
@@ -27,6 +26,4 @@ object Native {
|
||||
|
||||
@JvmStatic
|
||||
external fun getFd(fd: FileDescriptor?): Int
|
||||
|
||||
external fun isGif(buffer: ByteBuffer): Boolean
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@
|
||||
package com.hippo
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import com.hippo.image.Image
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
@@ -28,7 +30,7 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
}
|
||||
|
||||
private external fun openArchive(fd: Int, size: Long): Int
|
||||
external fun extractToByteBuffer(index: Int): ByteBuffer?
|
||||
private external fun extractToByteBuffer(index: Int): ByteBuffer?
|
||||
external fun extractToFd(index: Int, fd: Int)
|
||||
external fun getFilename(index: Int): String
|
||||
external fun needPassword(): Boolean
|
||||
@@ -39,8 +41,24 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
pfd.close()
|
||||
}
|
||||
|
||||
fun getImageSource(index: Int): Image.CloseableSource? {
|
||||
val buffer = extractToByteBuffer(index)
|
||||
buffer ?: return null
|
||||
check(buffer.isDirect)
|
||||
Image.rewriteGifSource(buffer)
|
||||
val source = ImageDecoder.createSource(buffer)
|
||||
return object : Image.CloseableSource {
|
||||
override val source: ImageDecoder.Source
|
||||
get() = source
|
||||
|
||||
override fun close() {
|
||||
releaseByteBuffer(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
private external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,985 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* 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.beerbelly;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A cache that uses a bounded amount of space on a filesystem. Each cache
|
||||
* entry has a string key and a fixed number of values. Each key must match
|
||||
* the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
|
||||
* accessible as streams or files. Each value must be between {@code 0} and
|
||||
* {@code Integer.MAX_VALUE} bytes in length.
|
||||
*
|
||||
* <p>The cache stores its data in a directory on the filesystem. This
|
||||
* directory must be exclusive to the cache; the cache may delete or overwrite
|
||||
* files from its directory. It is an error for multiple processes to use the
|
||||
* same cache directory at the same time.
|
||||
*
|
||||
* <p>This cache limits the number of bytes that it will store on the
|
||||
* filesystem. When the number of stored bytes exceeds the limit, the cache will
|
||||
* remove entries in the background until the limit is satisfied. The limit is
|
||||
* not strict: the cache may temporarily exceed it while waiting for files to be
|
||||
* deleted. The limit does not include filesystem overhead or the cache
|
||||
* journal so space-sensitive applications should set a conservative limit.
|
||||
*
|
||||
* <p>Clients call {@link #edit} to create or update the values of an entry. An
|
||||
* entry may have only one editor at one time; if a value is not available to be
|
||||
* edited then {@link #edit} will return null.
|
||||
* <ul>
|
||||
* <li>When an entry is being <strong>created</strong> it is necessary to
|
||||
* supply a full set of values; the empty value should be used as a
|
||||
* placeholder if necessary.
|
||||
* <li>When an entry is being <strong>edited</strong>, it is not necessary
|
||||
* to supply data for every value; values default to their previous
|
||||
* value.
|
||||
* </ul>
|
||||
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
|
||||
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
|
||||
* of values as they were before or after the commit, but never a mix of values.
|
||||
*
|
||||
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
|
||||
* observe the value at the time that {@link #get} was called. Updates and
|
||||
* removals after the call do not impact ongoing reads.
|
||||
*
|
||||
* <p>This class is tolerant of some I/O errors. If files are missing from the
|
||||
* filesystem, the corresponding entries will be dropped from the cache. If
|
||||
* an error occurs while writing a cache value, the edit will fail silently.
|
||||
* Callers should handle other problems by catching {@code IOException} and
|
||||
* responding appropriately.
|
||||
*/
|
||||
public final class DiskLruCache implements Closeable {
|
||||
static final String JOURNAL_FILE = "journal";
|
||||
static final String JOURNAL_FILE_TEMP = "journal.tmp";
|
||||
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
|
||||
static final String MAGIC = "libcore.io.DiskLruCache";
|
||||
static final String VERSION_1 = "1";
|
||||
static final long ANY_SEQUENCE_NUMBER = -1;
|
||||
static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
|
||||
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
|
||||
private static final String CLEAN = "CLEAN";
|
||||
private static final String DIRTY = "DIRTY";
|
||||
private static final String REMOVE = "REMOVE";
|
||||
private static final String READ = "READ";
|
||||
|
||||
/*
|
||||
* This cache uses a journal file named "journal". A typical journal file
|
||||
* looks like this:
|
||||
* libcore.io.DiskLruCache
|
||||
* 1
|
||||
* 100
|
||||
* 2
|
||||
*
|
||||
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
|
||||
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
|
||||
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
|
||||
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
|
||||
* DIRTY 1ab96a171faeeee38496d8b330771a7a
|
||||
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
|
||||
* READ 335c4c6028171cfddfbaae1a9c313c52
|
||||
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
|
||||
*
|
||||
* The first five lines of the journal form its header. They are the
|
||||
* constant string "libcore.io.DiskLruCache", the disk cache's version,
|
||||
* the application's version, the value count, and a blank line.
|
||||
*
|
||||
* Each of the subsequent lines in the file is a record of the state of a
|
||||
* cache entry. Each line contains space-separated values: a state, a key,
|
||||
* and optional state-specific values.
|
||||
* o DIRTY lines track that an entry is actively being created or updated.
|
||||
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
|
||||
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
|
||||
* temporary files may need to be deleted.
|
||||
* o CLEAN lines track a cache entry that has been successfully published
|
||||
* and may be read. A publish line is followed by the lengths of each of
|
||||
* its values.
|
||||
* o READ lines track accesses for LRU.
|
||||
* o REMOVE lines track entries that have been deleted.
|
||||
*
|
||||
* The journal file is appended to as cache operations occur. The journal may
|
||||
* occasionally be compacted by dropping redundant lines. A temporary file named
|
||||
* "journal.tmp" will be used during compaction; that file should be deleted if
|
||||
* it exists when the cache is opened.
|
||||
*/
|
||||
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) {
|
||||
// Eat all writes silently. Nom nom.
|
||||
}
|
||||
};
|
||||
/**
|
||||
* This cache uses a single background thread to evict entries.
|
||||
*/
|
||||
final ThreadPoolExecutor executorService =
|
||||
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
|
||||
private final File directory;
|
||||
private final File journalFile;
|
||||
private final File journalFileTmp;
|
||||
private final File journalFileBackup;
|
||||
private final int appVersion;
|
||||
private final int valueCount;
|
||||
private final LinkedHashMap<String, Entry> lruEntries =
|
||||
new LinkedHashMap<>(0, 0.75f, true);
|
||||
private long maxSize;
|
||||
private long size = 0;
|
||||
private Writer journalWriter;
|
||||
private int redundantOpCount;
|
||||
/**
|
||||
* To differentiate between old and current snapshots, each entry is given
|
||||
* a sequence number each time an edit is committed. A snapshot is stale if
|
||||
* its sequence number is not equal to its entry's sequence number.
|
||||
*/
|
||||
private long nextSequenceNumber = 0;
|
||||
|
||||
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
|
||||
this.directory = directory;
|
||||
this.appVersion = appVersion;
|
||||
this.journalFile = new File(directory, JOURNAL_FILE);
|
||||
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
|
||||
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
this.valueCount = valueCount;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the cache in {@code directory}, creating a cache if none exists
|
||||
* there.
|
||||
*
|
||||
* @param directory a writable directory
|
||||
* @param valueCount the number of values per cache entry. Must be positive.
|
||||
* @param maxSize the maximum number of bytes this cache should use to store
|
||||
* @throws IOException if reading or writing the cache directory fails
|
||||
*/
|
||||
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
|
||||
throws IOException {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("maxSize <= 0");
|
||||
}
|
||||
if (valueCount <= 0) {
|
||||
throw new IllegalArgumentException("valueCount <= 0");
|
||||
}
|
||||
|
||||
// If a bkp file exists, use it instead.
|
||||
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
if (backupFile.exists()) {
|
||||
File journalFile = new File(directory, JOURNAL_FILE);
|
||||
// If journal file also exists just delete backup file.
|
||||
if (journalFile.exists()) {
|
||||
backupFile.delete();
|
||||
} else {
|
||||
renameTo(backupFile, journalFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer to pick up where we left off.
|
||||
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
if (cache.journalFile.exists()) {
|
||||
try {
|
||||
cache.readJournal();
|
||||
cache.processJournal();
|
||||
return cache;
|
||||
} catch (IOException journalIsCorrupt) {
|
||||
System.out
|
||||
.println("DiskLruCache "
|
||||
+ directory
|
||||
+ " is corrupt: "
|
||||
+ journalIsCorrupt.getMessage()
|
||||
+ ", removing");
|
||||
cache.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new empty cache.
|
||||
directory.mkdirs();
|
||||
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
cache.rebuildJournal();
|
||||
return cache;
|
||||
}
|
||||
|
||||
private static void deleteIfExists(File file) throws IOException {
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
|
||||
if (deleteDestination) {
|
||||
deleteIfExists(to);
|
||||
}
|
||||
if (!from.renameTo(to)) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static String inputStreamToString(InputStream in) throws IOException {
|
||||
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
|
||||
}
|
||||
|
||||
private void readJournal() throws IOException {
|
||||
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
|
||||
try {
|
||||
String magic = reader.readLine();
|
||||
String version = reader.readLine();
|
||||
String appVersionString = reader.readLine();
|
||||
String valueCountString = reader.readLine();
|
||||
String blank = reader.readLine();
|
||||
if (!MAGIC.equals(magic)
|
||||
|| !VERSION_1.equals(version)
|
||||
|| !Integer.toString(appVersion).equals(appVersionString)
|
||||
|| !Integer.toString(valueCount).equals(valueCountString)
|
||||
|| !"".equals(blank)) {
|
||||
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
|
||||
+ valueCountString + ", " + blank + "]");
|
||||
}
|
||||
|
||||
int lineCount = 0;
|
||||
while (true) {
|
||||
try {
|
||||
readJournalLine(reader.readLine());
|
||||
lineCount++;
|
||||
} catch (EOFException endOfJournal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
redundantOpCount = lineCount - lruEntries.size();
|
||||
|
||||
// If we ended on a truncated line, rebuild the journal before appending to it.
|
||||
if (reader.hasUnterminatedLine()) {
|
||||
rebuildJournal();
|
||||
} else {
|
||||
journalWriter = new BufferedWriter(new OutputStreamWriter(
|
||||
new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private void readJournalLine(String line) throws IOException {
|
||||
int firstSpace = line.indexOf(' ');
|
||||
if (firstSpace == -1) {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
|
||||
int keyBegin = firstSpace + 1;
|
||||
int secondSpace = line.indexOf(' ', keyBegin);
|
||||
final String key;
|
||||
if (secondSpace == -1) {
|
||||
key = line.substring(keyBegin);
|
||||
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
|
||||
lruEntries.remove(key);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
key = line.substring(keyBegin, secondSpace);
|
||||
}
|
||||
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
}
|
||||
|
||||
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
|
||||
String[] parts = line.substring(secondSpace + 1).split(" ");
|
||||
entry.readable = true;
|
||||
entry.currentEditor = null;
|
||||
entry.setLengths(parts);
|
||||
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
|
||||
entry.currentEditor = new Editor(entry);
|
||||
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
|
||||
// This work was already done by calling lruEntries.get().
|
||||
} else {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the initial size and collects garbage as a part of opening the
|
||||
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
|
||||
*/
|
||||
private void processJournal() throws IOException {
|
||||
deleteIfExists(journalFileTmp);
|
||||
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
|
||||
Entry entry = i.next();
|
||||
if (entry.currentEditor == null) {
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
size += entry.lengths[t];
|
||||
}
|
||||
} else {
|
||||
entry.currentEditor = null;
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
deleteIfExists(entry.getCleanFile(t));
|
||||
deleteIfExists(entry.getDirtyFile(t));
|
||||
}
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new journal that omits redundant information. This replaces the
|
||||
* current journal if it exists.
|
||||
*/
|
||||
private synchronized void rebuildJournal() throws IOException {
|
||||
if (journalWriter != null) {
|
||||
journalWriter.close();
|
||||
}
|
||||
|
||||
try (Writer writer = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII))) {
|
||||
writer.write(MAGIC);
|
||||
writer.write("\n");
|
||||
writer.write(VERSION_1);
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(appVersion));
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(valueCount));
|
||||
writer.write("\n");
|
||||
writer.write("\n");
|
||||
|
||||
for (Entry entry : lruEntries.values()) {
|
||||
if (entry.currentEditor != null) {
|
||||
writer.write(DIRTY + ' ' + entry.key + '\n');
|
||||
} else {
|
||||
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (journalFile.exists()) {
|
||||
renameTo(journalFile, journalFileBackup, true);
|
||||
}
|
||||
renameTo(journalFileTmp, journalFile, false);
|
||||
journalFileBackup.delete();
|
||||
|
||||
journalWriter = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
} private final Callable<Void> cleanupCallable = new Callable<>() {
|
||||
public Void call() throws Exception {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (journalWriter == null) {
|
||||
return null; // Closed.
|
||||
}
|
||||
trimToSize();
|
||||
if (journalRebuildRequired()) {
|
||||
rebuildJournal();
|
||||
redundantOpCount = 0;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return {@code true} if it contains the key
|
||||
*/
|
||||
public synchronized boolean contain(String key) {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
return entry != null && entry.readable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
|
||||
* exist is not currently readable. If a value is returned, it is moved to
|
||||
* the head of the LRU queue.
|
||||
*/
|
||||
public synchronized Snapshot get(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Open all streams eagerly to guarantee that we see a single published
|
||||
// snapshot. If we opened streams lazily then the streams could come
|
||||
// from different edits.
|
||||
InputStream[] ins = new InputStream[valueCount];
|
||||
try {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
ins[i] = new FileInputStream(entry.getCleanFile(i));
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// A file must have been deleted manually!
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (ins[i] != null) {
|
||||
Util.closeQuietly(ins[i]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(READ + ' ').append(key).append(String.valueOf('\n'));
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for the entry named {@code key}, or null if another
|
||||
* edit is in progress.
|
||||
*/
|
||||
public Editor edit(String key) throws IOException {
|
||||
return edit(key, ANY_SEQUENCE_NUMBER);
|
||||
}
|
||||
|
||||
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|
||||
|| entry.sequenceNumber != expectedSequenceNumber)) {
|
||||
return null; // Snapshot is stale.
|
||||
}
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
} else if (entry.currentEditor != null) {
|
||||
return null; // Another edit is in progress.
|
||||
}
|
||||
|
||||
Editor editor = new Editor(entry);
|
||||
entry.currentEditor = editor;
|
||||
|
||||
// Flush the journal before creating files to prevent file leaks.
|
||||
journalWriter.write(DIRTY + ' ' + key + '\n');
|
||||
journalWriter.flush();
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where this cache stores its data.
|
||||
*/
|
||||
public File getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of bytes that this cache should use to store
|
||||
* its data.
|
||||
*/
|
||||
public synchronized long getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the maximum number of bytes the cache can store and queues a job
|
||||
* to trim the existing store, if necessary.
|
||||
*/
|
||||
public synchronized void setMaxSize(long maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes currently being used to store the values in
|
||||
* this cache. This may be greater than the max size if a background
|
||||
* deletion is pending.
|
||||
*/
|
||||
public synchronized long size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
|
||||
Entry entry = editor.entry;
|
||||
if (entry.currentEditor != editor) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// If this edit is creating the entry for the first time, every index must have a value.
|
||||
if (success && !entry.readable) {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (!editor.written[i]) {
|
||||
editor.abort();
|
||||
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
|
||||
}
|
||||
if (!entry.getDirtyFile(i).exists()) {
|
||||
editor.abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File dirty = entry.getDirtyFile(i);
|
||||
if (success) {
|
||||
if (dirty.exists()) {
|
||||
File clean = entry.getCleanFile(i);
|
||||
dirty.renameTo(clean);
|
||||
long oldLength = entry.lengths[i];
|
||||
long newLength = clean.length();
|
||||
entry.lengths[i] = newLength;
|
||||
size = size - oldLength + newLength;
|
||||
}
|
||||
} else {
|
||||
deleteIfExists(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
entry.currentEditor = null;
|
||||
if (entry.readable | success) {
|
||||
entry.readable = true;
|
||||
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
if (success) {
|
||||
entry.sequenceNumber = nextSequenceNumber++;
|
||||
}
|
||||
} else {
|
||||
lruEntries.remove(entry.key);
|
||||
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
|
||||
}
|
||||
journalWriter.flush();
|
||||
|
||||
if (size > maxSize || journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We only rebuild the journal when it will halve the size of the journal
|
||||
* and eliminate at least 2000 ops.
|
||||
*/
|
||||
private boolean journalRebuildRequired() {
|
||||
final int redundantOpCompactThreshold = 2000;
|
||||
return redundantOpCount >= redundantOpCompactThreshold //
|
||||
&& redundantOpCount >= lruEntries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the entry for {@code key} if it exists and can be removed. Entries
|
||||
* actively being edited cannot be removed.
|
||||
*
|
||||
* @return true if an entry was removed.
|
||||
*/
|
||||
public synchronized boolean remove(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null || entry.currentEditor != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File file = entry.getCleanFile(i);
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException("failed to delete " + file);
|
||||
}
|
||||
size -= entry.lengths[i];
|
||||
entry.lengths[i] = 0;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(REMOVE + ' ').append(key).append(String.valueOf('\n'));
|
||||
lruEntries.remove(key);
|
||||
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this cache has been closed.
|
||||
*/
|
||||
public synchronized boolean isClosed() {
|
||||
return journalWriter == null;
|
||||
}
|
||||
|
||||
private void checkNotClosed() {
|
||||
if (journalWriter == null) {
|
||||
throw new IllegalStateException("cache is closed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force buffered operations to the filesystem.
|
||||
*/
|
||||
public synchronized void flush() throws IOException {
|
||||
checkNotClosed();
|
||||
trimToSize();
|
||||
journalWriter.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this cache. Stored values will remain on the filesystem.
|
||||
*/
|
||||
public synchronized void close() throws IOException {
|
||||
if (journalWriter == null) {
|
||||
return; // Already closed.
|
||||
}
|
||||
for (Entry entry : new ArrayList<>(lruEntries.values())) {
|
||||
if (entry.currentEditor != null) {
|
||||
entry.currentEditor.abort();
|
||||
}
|
||||
}
|
||||
trimToSize();
|
||||
journalWriter.close();
|
||||
journalWriter = null;
|
||||
}
|
||||
|
||||
private void trimToSize() throws IOException {
|
||||
while (size > maxSize) {
|
||||
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
|
||||
remove(toEvict.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cache and deletes all of its stored values. This will delete
|
||||
* all files in the cache directory including files that weren't created by
|
||||
* the cache.
|
||||
*/
|
||||
public void delete() throws IOException {
|
||||
close();
|
||||
Util.deleteContents(directory);
|
||||
}
|
||||
|
||||
private void validateKey(String key) {
|
||||
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException("keys must match regex "
|
||||
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapshot of the values for an entry.
|
||||
*/
|
||||
public final class Snapshot implements Closeable {
|
||||
private final String key;
|
||||
private final long sequenceNumber;
|
||||
private final InputStream[] ins;
|
||||
private final long[] lengths;
|
||||
|
||||
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
|
||||
this.key = key;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.ins = ins;
|
||||
this.lengths = lengths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for this snapshot's entry, or null if either the
|
||||
* entry has changed since this snapshot was created or if another edit
|
||||
* is in progress.
|
||||
*/
|
||||
public Editor edit() throws IOException {
|
||||
return DiskLruCache.this.edit(key, sequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unbuffered stream with the value for {@code index}.
|
||||
*/
|
||||
public InputStream getInputStream(int index) {
|
||||
return ins[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value for {@code index}.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
return inputStreamToString(getInputStream(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte length of the value for {@code index}.
|
||||
*/
|
||||
public long getLength(int index) {
|
||||
return lengths[index];
|
||||
}
|
||||
|
||||
public void close() {
|
||||
for (InputStream in : ins) {
|
||||
Util.closeQuietly(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the values for an entry.
|
||||
*/
|
||||
public final class Editor {
|
||||
private final Entry entry;
|
||||
private final boolean[] written;
|
||||
private boolean hasErrors;
|
||||
private boolean committed;
|
||||
|
||||
private Editor(Entry entry) {
|
||||
this.entry = entry;
|
||||
this.written = (entry.readable) ? null : new boolean[valueCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unbuffered input stream to read the last committed value,
|
||||
* or null if no value has been committed.
|
||||
*/
|
||||
public InputStream newInputStream(int index) {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new FileInputStream(entry.getCleanFile(index));
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last committed value as a string, or null if no value
|
||||
* has been committed.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
InputStream in = newInputStream(index);
|
||||
return in != null ? inputStreamToString(in) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new unbuffered output stream to write the value at
|
||||
* {@code index}. If the underlying output stream encounters errors
|
||||
* when writing to the filesystem, this edit will be aborted when
|
||||
* {@link #commit} is called. The returned output stream does not throw
|
||||
* IOExceptions.
|
||||
*/
|
||||
public OutputStream newOutputStream(int index) throws IOException {
|
||||
if (index < 0 || index >= valueCount) {
|
||||
throw new IllegalArgumentException("Expected index " + index + " to "
|
||||
+ "be greater than 0 and less than the maximum value count "
|
||||
+ "of " + valueCount);
|
||||
}
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
written[index] = true;
|
||||
}
|
||||
File dirtyFile = entry.getDirtyFile(index);
|
||||
FileOutputStream outputStream;
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Attempt to recreate the cache directory.
|
||||
directory.mkdirs();
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e2) {
|
||||
// We are unable to recover. Silently eat the writes.
|
||||
return NULL_OUTPUT_STREAM;
|
||||
}
|
||||
}
|
||||
return new FaultHidingOutputStream(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value at {@code index} to {@code value}.
|
||||
*/
|
||||
public void set(int index, String value) throws IOException {
|
||||
Writer writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
|
||||
writer.write(value);
|
||||
} finally {
|
||||
Util.closeQuietly(writer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits this edit so it is visible to readers. This releases the
|
||||
* edit lock so another edit may be started on the same key.
|
||||
*/
|
||||
public void commit() throws IOException {
|
||||
if (hasErrors) {
|
||||
completeEdit(this, false);
|
||||
remove(entry.key); // The previous entry is stale.
|
||||
} else {
|
||||
completeEdit(this, true);
|
||||
}
|
||||
committed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts this edit. This releases the edit lock so another edit may be
|
||||
* started on the same key.
|
||||
*/
|
||||
public void abort() throws IOException {
|
||||
completeEdit(this, false);
|
||||
}
|
||||
|
||||
public void abortUnlessCommitted() {
|
||||
if (!committed) {
|
||||
try {
|
||||
abort();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FaultHidingOutputStream extends FilterOutputStream {
|
||||
private FaultHidingOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int oneByte) {
|
||||
try {
|
||||
out.write(oneByte);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) {
|
||||
try {
|
||||
out.write(buffer, offset, length);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
try {
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class Entry {
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* Lengths of this entry's files.
|
||||
*/
|
||||
private final long[] lengths;
|
||||
|
||||
/**
|
||||
* True if this entry has ever been published.
|
||||
*/
|
||||
private boolean readable;
|
||||
|
||||
/**
|
||||
* The ongoing edit or null if this entry is not being edited.
|
||||
*/
|
||||
private Editor currentEditor;
|
||||
|
||||
/**
|
||||
* The sequence number of the most recently committed edit to this entry.
|
||||
*/
|
||||
private long sequenceNumber;
|
||||
|
||||
private Entry(String key) {
|
||||
this.key = key;
|
||||
this.lengths = new long[valueCount];
|
||||
}
|
||||
|
||||
public String getLengths() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (long size : lengths) {
|
||||
result.append(' ').append(size);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lengths using decimal numbers like "10123".
|
||||
*/
|
||||
private void setLengths(String[] strings) throws IOException {
|
||||
if (strings.length != valueCount) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < strings.length; i++) {
|
||||
lengths[i] = Long.parseLong(strings[i]);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException invalidLengths(String[] strings) throws IOException {
|
||||
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
|
||||
}
|
||||
|
||||
public File getCleanFile(int i) {
|
||||
return new File(directory, key + "." + i);
|
||||
}
|
||||
|
||||
public File getDirtyFile(int i) {
|
||||
return new File(directory, key + "." + i + ".tmp");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,504 +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.beerbelly;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
import com.hippo.util.HashCodeUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class SimpleDiskCache {
|
||||
|
||||
private static final String TAG = SimpleDiskCache.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Disk cache is free
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_NONE = 0;
|
||||
|
||||
/**
|
||||
* Disk cache is flushing or clearing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_IN_USE = 1;
|
||||
|
||||
/**
|
||||
* Disk cache is reading or writing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_BUSY = 2;
|
||||
|
||||
private final File mCacheDir;
|
||||
private final int mSize;
|
||||
@NonNull
|
||||
private final Map<String, CounterLock> mLockMap = new HashMap<>();
|
||||
@NonNull
|
||||
private final LockPool mLockPool = new LockPool();
|
||||
private int mDiskCacheState;
|
||||
@Nullable
|
||||
private DiskLruCache mDiskLruCache;
|
||||
|
||||
public SimpleDiskCache(File cacheDir, int size) {
|
||||
mCacheDir = cacheDir;
|
||||
mSize = size;
|
||||
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hashing method that changes a string (like a URL) into a hash suitable
|
||||
* for using as a disk filename.
|
||||
*
|
||||
* @param key The key used to store the file
|
||||
*/
|
||||
private static String hashKeyForDisk(final String key) {
|
||||
String cacheKey;
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
digest.update(key.getBytes());
|
||||
cacheKey = HashCodeUtils.bytesToHexString(digest.digest());
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
cacheKey = String.valueOf(key.hashCode());
|
||||
}
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
public synchronized boolean isValid() {
|
||||
return mDiskLruCache != null;
|
||||
}
|
||||
|
||||
private synchronized void init() throws IOException {
|
||||
if (!isValid()) {
|
||||
mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, mSize);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized long size() {
|
||||
if (null != mDiskLruCache) {
|
||||
return mDiskLruCache.size();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void flush() {
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
if (null != mDiskLruCache) {
|
||||
try {
|
||||
mDiskLruCache.flush();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache flush", e);
|
||||
}
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public synchronized boolean clear() {
|
||||
boolean result = true;
|
||||
if (null == mDiskLruCache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
try {
|
||||
mDiskLruCache.delete();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache clearCache", e);
|
||||
}
|
||||
mDiskLruCache = null;
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache init", e);
|
||||
result = false;
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
return result;
|
||||
}
|
||||
|
||||
private synchronized CounterLock obtainLock(String key) {
|
||||
// Wait for clear over
|
||||
while (mDiskCacheState == STATE_DISK_CACHE_BUSY) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_IN_USE;
|
||||
|
||||
// Get lock from key
|
||||
CounterLock lock;
|
||||
lock = mLockMap.get(key);
|
||||
if (lock == null) {
|
||||
lock = mLockPool.get();
|
||||
mLockMap.put(key, lock);
|
||||
}
|
||||
lock.obtain();
|
||||
|
||||
return lock;
|
||||
}
|
||||
|
||||
private synchronized void releaseLock(String key, CounterLock lock) {
|
||||
lock.release();
|
||||
if (lock.isFree()) {
|
||||
mLockMap.remove(key);
|
||||
mLockPool.push(lock);
|
||||
}
|
||||
|
||||
if (mLockMap.isEmpty()) {
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contain(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.readLock().lock();
|
||||
|
||||
boolean result = mDiskLruCache.contain(diskKey);
|
||||
|
||||
lock.readLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean remove(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result;
|
||||
try {
|
||||
result = mDiskLruCache.remove(diskKey);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
result = false;
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean put(@NonNull String key, @NonNull InputStream is) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result = false;
|
||||
if (isValid()) {
|
||||
result = putToDisk(diskKey, is);
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean putToDisk(String key, InputStream is) {
|
||||
DiskLruCache.Editor editor = null;
|
||||
OutputStream os = null;
|
||||
boolean completeEdit = false;
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
editor = mDiskLruCache.edit(key);
|
||||
if (editor == null) {
|
||||
// The editor is in progress
|
||||
return false;
|
||||
}
|
||||
|
||||
os = editor.newOutputStream(0);
|
||||
if (os != null) {
|
||||
Util.copy(is, os);
|
||||
completeEdit = true;
|
||||
editor.commit();
|
||||
return true;
|
||||
} else {
|
||||
// Can't get OutputStream
|
||||
completeEdit = true;
|
||||
editor.abort();
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Util.closeQuietly(os);
|
||||
try {
|
||||
if (!completeEdit && editor != null) {
|
||||
editor.abort();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key the key of the target
|
||||
* @return the InputStreamPipe, <code>null</code> for missing
|
||||
*/
|
||||
@Nullable
|
||||
public InputStreamPipe getInputStreamPipe(@NonNull String key) {
|
||||
if (contain(key)) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheInputStreamPipe(diskKey);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OutputStreamPipe getOutputStreamPipe(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheOutputStreamPipe(diskKey);
|
||||
}
|
||||
|
||||
private class CacheInputStreamPipe implements InputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Snapshot mCurrentSnapshot;
|
||||
|
||||
private CacheInputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.readLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.readLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
InputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Snapshot snapshot;
|
||||
snapshot = mDiskLruCache.get(mKey);
|
||||
if (snapshot == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
mCurrentSnapshot = snapshot;
|
||||
return snapshot.getInputStream(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Util.closeQuietly(mCurrentSnapshot);
|
||||
mCurrentSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheOutputStreamPipe implements OutputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Editor mCurrentEditor;
|
||||
|
||||
private CacheOutputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.writeLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.writeLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
OutputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Editor editor = mDiskLruCache.edit(mKey);
|
||||
if (editor == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
|
||||
OutputStream os;
|
||||
try {
|
||||
os = editor.newOutputStream(0);
|
||||
mCurrentEditor = editor;
|
||||
return os;
|
||||
} catch (IOException e) {
|
||||
// Get trouble
|
||||
try {
|
||||
editor.abort();
|
||||
} catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mCurrentEditor != null) {
|
||||
try {
|
||||
mCurrentEditor.commit();
|
||||
} catch (IOException e) {
|
||||
// Empty
|
||||
}
|
||||
mCurrentEditor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CounterLock extends ReentrantReadWriteLock {
|
||||
|
||||
private int mCount;
|
||||
|
||||
public boolean isFree() {
|
||||
return mCount == 0;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (--mCount < 0) {
|
||||
throw new IllegalStateException("Release time is more than occupy time");
|
||||
}
|
||||
}
|
||||
|
||||
public void obtain() {
|
||||
++mCount;
|
||||
}
|
||||
}
|
||||
|
||||
private class LockPool {
|
||||
|
||||
private static final int MAX_SIZE = 10;
|
||||
|
||||
private final CounterLock[] mArray = new CounterLock[MAX_SIZE];
|
||||
private final int mMaxSize = MAX_SIZE;
|
||||
private int mSize;
|
||||
|
||||
public void push(CounterLock lock) {
|
||||
if (lock != null && mSize < mMaxSize) {
|
||||
mArray[mSize++] = lock;
|
||||
}
|
||||
}
|
||||
|
||||
public CounterLock get() {
|
||||
if (mSize > 0) {
|
||||
CounterLock lock = mArray[--mSize];
|
||||
mArray[mSize] = null;
|
||||
return lock;
|
||||
} else {
|
||||
return new CounterLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* 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.beerbelly;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Buffers input from an {@link InputStream} for reading lines.
|
||||
*
|
||||
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
|
||||
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
|
||||
* line at end of input is invalid and will be ignored, the caller may use {@code
|
||||
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
|
||||
*
|
||||
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
|
||||
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
|
||||
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
|
||||
* end-of-input reporting and a more restrictive definition of a line.
|
||||
*
|
||||
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
|
||||
* and 10, respectively, and the representation of no other character contains these values.
|
||||
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
|
||||
* The default charset is US_ASCII.
|
||||
*/
|
||||
class StrictLineReader implements Closeable {
|
||||
private static final byte CR = (byte) '\r';
|
||||
private static final byte LF = (byte) '\n';
|
||||
|
||||
private final InputStream in;
|
||||
private final Charset charset;
|
||||
|
||||
/*
|
||||
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
|
||||
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
|
||||
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
|
||||
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
|
||||
*/
|
||||
private byte[] buf;
|
||||
private int pos;
|
||||
private int end;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, Charset charset) {
|
||||
this(in, 8192, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified capacity and charset.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param capacity the capacity of the buffer.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if {@code capacity} is negative or zero
|
||||
* or the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, int capacity, Charset charset) {
|
||||
if (in == null || charset == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if (capacity < 0) {
|
||||
throw new IllegalArgumentException("capacity <= 0");
|
||||
}
|
||||
if (!(charset.equals(Util.US_ASCII))) {
|
||||
throw new IllegalArgumentException("Unsupported encoding");
|
||||
}
|
||||
|
||||
this.in = in;
|
||||
this.charset = charset;
|
||||
buf = new byte[capacity];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader by closing the underlying {@code InputStream} and
|
||||
* marking this reader as closed.
|
||||
*
|
||||
* @throws IOException for errors when closing the underlying {@code InputStream}.
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf != null) {
|
||||
buf = null;
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
|
||||
* this end of line marker is not included in the result.
|
||||
*
|
||||
* @return the next line from the input.
|
||||
* @throws IOException for underlying {@code InputStream} errors.
|
||||
* @throws EOFException for the end of source stream.
|
||||
*/
|
||||
public String readLine() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf == null) {
|
||||
throw new IOException("LineReader is closed");
|
||||
}
|
||||
|
||||
// Read more data if we are at the end of the buffered data.
|
||||
// Though it's an error to read after an exception, we will let {@code fillBuf()}
|
||||
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
|
||||
if (pos >= end) {
|
||||
fillBuf();
|
||||
}
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
|
||||
String res = new String(buf, pos, lineEnd - pos, charset);
|
||||
pos = i + 1;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's anticipate up to 80 characters on top of those already read.
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
|
||||
return new String(buf, 0, length, charset);
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
out.write(buf, pos, end - pos);
|
||||
// Mark unterminated line in case fillBuf throws EOFException or IOException.
|
||||
end = -1;
|
||||
fillBuf();
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
if (i != pos) {
|
||||
out.write(buf, pos, i - pos);
|
||||
}
|
||||
pos = i + 1;
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasUnterminatedLine() {
|
||||
return end == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads new input data into the buffer. Call only with pos == end or end == -1,
|
||||
* depending on the desired outcome if the function throws.
|
||||
*/
|
||||
private void fillBuf() throws IOException {
|
||||
int result = in.read(buf, 0, buf.length);
|
||||
if (result == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
pos = 0;
|
||||
end = result;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* 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.beerbelly;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Junk drawer of utility methods.
|
||||
*/
|
||||
final class Util {
|
||||
static final Charset US_ASCII = StandardCharsets.US_ASCII;
|
||||
static final Charset UTF_8 = StandardCharsets.UTF_8;
|
||||
private static final int EOF = -1;
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
|
||||
|
||||
private Util() {
|
||||
}
|
||||
|
||||
static String readFully(Reader reader) throws IOException {
|
||||
try (reader) {
|
||||
StringWriter writer = new StringWriter();
|
||||
char[] buffer = new char[1024];
|
||||
int count;
|
||||
while ((count = reader.read(buffer)) != -1) {
|
||||
writer.write(buffer, 0, count);
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the contents of {@code dir}. Throws an IOException if any file
|
||||
* could not be deleted, or if {@code dir} is not a readable directory.
|
||||
*/
|
||||
static void deleteContents(File dir) throws IOException {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
throw new IOException("not a readable directory: " + dir);
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteContents(file);
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new IOException("failed to delete file: " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void closeQuietly(/*Auto*/Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy bytes from an <code>InputStream</code> to an
|
||||
* <code>OutputStream</code>.
|
||||
*
|
||||
* @param input the InputStream
|
||||
* @param output the OutputStream
|
||||
* @return the number of bytes copied
|
||||
*/
|
||||
public static long copy(InputStream input, OutputStream output)
|
||||
throws IOException {
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
long count = 0;
|
||||
int n;
|
||||
while (EOF != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -291,7 +291,7 @@ public class Hosts {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean contains(String host) {
|
||||
public boolean contains(String host) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[]{host})) {
|
||||
return cursor.moveToNext();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
********************/
|
||||
@@ -85,13 +101,11 @@ public class Settings {
|
||||
public static final int DEFAULT_READ_CACHE_SIZE = 320;
|
||||
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
|
||||
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;
|
||||
private static final String KEY_PREVIEW_NUM = "preview_num";
|
||||
private static final int DEFAULT_PREVIEW_NUM = 60;
|
||||
private static final String TAG = Settings.class.getSimpleName();
|
||||
private static final String KEY_DISPLAY_NAME = "display_name";
|
||||
private static final String DEFAULT_DISPLAY_NAME = null;
|
||||
@@ -111,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;
|
||||
@@ -181,6 +194,7 @@ public class Settings {
|
||||
private static final boolean DEFAULT_SAVE_CRASH_LOG = true;
|
||||
private static final boolean DEFAULT_BUILT_IN_HOSTS = false;
|
||||
private static final boolean DEFAULT_FRONTING = false;
|
||||
private static final boolean DEFAULT_BYPASS_VPN = true;
|
||||
private static final String KEY_PROXY_TYPE = "proxy_type";
|
||||
private static final int DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM;
|
||||
private static final String KEY_PROXY_IP = "proxy_ip";
|
||||
@@ -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";
|
||||
@@ -423,10 +436,6 @@ public class Settings {
|
||||
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value);
|
||||
}
|
||||
|
||||
public static int getPreviewNum() {
|
||||
return getInt(KEY_PREVIEW_NUM, DEFAULT_PREVIEW_NUM);
|
||||
}
|
||||
|
||||
public static int getDefaultCategories() {
|
||||
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
|
||||
}
|
||||
@@ -679,6 +688,10 @@ public class Settings {
|
||||
putBoolean(KEY_DOMAIN_FRONTING, value);
|
||||
}
|
||||
|
||||
public static boolean getBypassVpn() {
|
||||
return getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN);
|
||||
}
|
||||
|
||||
public static int getProxyType() {
|
||||
return getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -123,4 +123,8 @@ object EhDns : Dns {
|
||||
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.getBuiltInHosts() }
|
||||
?: Dns.SYSTEM.lookup(hostname)
|
||||
}
|
||||
}
|
||||
|
||||
fun isInHosts(hostname: String): Boolean {
|
||||
return hosts.contains(hostname) || (builtInHosts.contains(hostname) && Settings.getBuiltInHosts())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -213,7 +216,7 @@ object EhEngine {
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getGalleryList(url: String): GalleryListParser.Result {
|
||||
val referer = EhUrl.getReferer()
|
||||
val referer = EhUrl.referer
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
@@ -279,8 +282,8 @@ object EhEngine {
|
||||
}
|
||||
json.put("gidlist", ja)
|
||||
json.put("namespace", 1)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.apiUrl
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(json.toString().toRequestBody(MEDIA_TYPE_JSON))
|
||||
@@ -306,7 +309,7 @@ object EhEngine {
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getGalleryDetail(url: String?): GalleryDetail {
|
||||
val referer = EhUrl.getReferer()
|
||||
val referer = EhUrl.referer
|
||||
Log.d(TAG, url!!)
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
@@ -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)
|
||||
@@ -334,7 +337,7 @@ object EhEngine {
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getPreviewSet(url: String?): Pair<PreviewSet, Int> {
|
||||
val referer = EhUrl.getReferer()
|
||||
val referer = EhUrl.referer
|
||||
Log.d(TAG, url!!)
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
@@ -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) {
|
||||
@@ -372,9 +375,9 @@ object EhEngine {
|
||||
json.put("token", token)
|
||||
json.put("rating", ceil((rating * 2).toDouble()).toInt())
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val url = EhUrl.apiUrl
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
@@ -409,7 +412,7 @@ object EhEngine {
|
||||
builder.add("commenttext_edit", comment)
|
||||
builder.add("edit_comment", id)
|
||||
}
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url!!)
|
||||
val request = EhRequestBuilder(url, url, origin)
|
||||
.post(builder.build())
|
||||
@@ -451,9 +454,9 @@ object EhEngine {
|
||||
)
|
||||
)
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val referer = EhUrl.getReferer()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.apiUrl
|
||||
val referer = EhUrl.referer
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
@@ -480,7 +483,7 @@ object EhEngine {
|
||||
suspend fun getFavorites(
|
||||
url: String
|
||||
): FavoritesParser.Result {
|
||||
val referer = EhUrl.getReferer()
|
||||
val referer = EhUrl.referer
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
@@ -534,7 +537,7 @@ object EhEngine {
|
||||
builder.add("submit", "Apply Changes")
|
||||
builder.add("update", "1")
|
||||
val url = EhUrl.getAddFavorites(gid, token)
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, url, origin)
|
||||
.post(builder.build())
|
||||
@@ -597,7 +600,7 @@ object EhEngine {
|
||||
builder.add("modifygids[]", gid.toString())
|
||||
}
|
||||
builder.add("apply", "Apply")
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, url, origin)
|
||||
.post(builder.build())
|
||||
@@ -669,7 +672,14 @@ object EhEngine {
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
result = ArchiveParser.parse(body)
|
||||
result = ArchiveParser.parse(body!!)!!
|
||||
if (result.funds == null) {
|
||||
try {
|
||||
result.funds = getFunds()
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
@@ -703,7 +713,7 @@ object EhEngine {
|
||||
}
|
||||
val url = EhUrl.getDownloadArchive(gid, token, or)
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(builder.build())
|
||||
@@ -711,43 +721,130 @@ object EhEngine {
|
||||
val call = okHttpClient.newCall(request)
|
||||
var body: String? = null
|
||||
var headers: Headers? = null
|
||||
var result: String?
|
||||
var code = -1
|
||||
try {
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
transformException(code, headers, body, null)
|
||||
result = ArchiveParser.parseArchiveUrl(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
if (!isHAtH) {
|
||||
if (result == null) {
|
||||
// Wait for the server to prepare archives
|
||||
delay(1000)
|
||||
try {
|
||||
call.clone().executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
transformException(code, headers, body, null)
|
||||
result = ArchiveParser.parseArchiveUrl(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
if (result == null) {
|
||||
throw EhException("Archive unavailable")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getFunds(): HomeParser.Funds {
|
||||
val url = EhUrl.URL_FUNDS
|
||||
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.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()
|
||||
transformException(code, headers, body, null)
|
||||
return HomeParser.parseResetLimits(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
var result = ArchiveParser.parseArchiveUrl(body)
|
||||
if (!isHAtH) {
|
||||
if (result == null) {
|
||||
delay(1000)
|
||||
try {
|
||||
call.clone().executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
transformException(code, headers, body, null)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
result = ArchiveParser.parseArchiveUrl(body)
|
||||
if (result == null) {
|
||||
throw EhException("Archive unavailable")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
@@ -798,8 +895,7 @@ object EhEngine {
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getUConfig(): Void? {
|
||||
val url = EhUrl.getUConfigUrl()
|
||||
suspend fun getUConfig(url: String = EhUrl.uConfigUrl): Void? {
|
||||
Log.d(TAG, url)
|
||||
var request = EhRequestBuilder(url, null).build()
|
||||
var call = okHttpClient.newCall(request)
|
||||
@@ -855,9 +951,9 @@ object EhEngine {
|
||||
json.put("comment_id", commentId)
|
||||
json.put("comment_vote", commentVote)
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val referer = EhUrl.getReferer()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.apiUrl
|
||||
val referer = EhUrl.referer
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
@@ -894,9 +990,9 @@ object EhEngine {
|
||||
json.put("tags", tags)
|
||||
json.put("vote", vote)
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val referer = EhUrl.getReferer()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.apiUrl
|
||||
val referer = EhUrl.referer
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
@@ -958,9 +1054,9 @@ object EhEngine {
|
||||
Headers.headersOf("Content-Disposition", "form-data; name=\"f_sfile\""),
|
||||
"File Search".toRequestBody()
|
||||
)
|
||||
val url = EhUrl.getImageSearchUrl()
|
||||
val referer = EhUrl.getReferer()
|
||||
val origin = EhUrl.getOrigin()
|
||||
val url = EhUrl.imageSearchUrl
|
||||
val referer = EhUrl.referer
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(builder.build())
|
||||
@@ -989,7 +1085,7 @@ object EhEngine {
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Throwable::class)
|
||||
fun getGalleryPage(
|
||||
suspend fun getGalleryPage(
|
||||
url: String?,
|
||||
gid: Long,
|
||||
token: String?
|
||||
@@ -1003,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()
|
||||
@@ -1018,7 +1114,7 @@ object EhEngine {
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Throwable::class)
|
||||
fun getGalleryPageApi(
|
||||
suspend fun getGalleryPageApi(
|
||||
gid: Long,
|
||||
index: Int,
|
||||
pToken: String?,
|
||||
@@ -1032,12 +1128,12 @@ object EhEngine {
|
||||
json.put("imgkey", pToken)
|
||||
json.put("showkey", showKey)
|
||||
val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)
|
||||
val url = EhUrl.getApiUrl()
|
||||
val url = EhUrl.apiUrl
|
||||
var referer: String? = null
|
||||
if (index > 0 && previousPToken != null) {
|
||||
referer = EhUrl.getPageUrl(gid, index - 1, previousPToken)
|
||||
}
|
||||
val origin = EhUrl.getOrigin()
|
||||
val origin = EhUrl.origin
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer, origin)
|
||||
.post(requestBody)
|
||||
@@ -1048,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) }
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,8 @@ object EhSSLSocketFactory : SSLSocketFactory() {
|
||||
}
|
||||
|
||||
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
|
||||
val address = s.inetAddress.hostAddress
|
||||
val socket = sslSocketFactory.createSocket(s, address, port, autoClose) as SSLSocket
|
||||
val address = s.inetAddress.hostAddress.takeIf { EhDns.isInHosts(host) }
|
||||
val socket = sslSocketFactory.createSocket(s, address ?: host, port, autoClose) as SSLSocket
|
||||
val sslSession = socket.session
|
||||
Log.d(
|
||||
"EhSSLSocketFactory",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,196 +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 androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.network.UrlBuilder;
|
||||
|
||||
public class EhUrl {
|
||||
|
||||
public static final int SITE_E = 0;
|
||||
public static final int SITE_EX = 1;
|
||||
|
||||
public static final String DOMAIN_EX = "exhentai.org";
|
||||
public static final String DOMAIN_E = "e-hentai.org";
|
||||
public static final String DOMAIN_LOFI = "lofi.e-hentai.org";
|
||||
|
||||
public static final String HOST_EX = "https://" + DOMAIN_EX + "/";
|
||||
public static final String API_EX = HOST_EX + "api.php";
|
||||
public static final String URL_FAVORITES_EX = HOST_EX + "favorites.php";
|
||||
public static final String URL_UCONFIG_EX = HOST_EX + "uconfig.php";
|
||||
public static final String URL_MY_TAGS_EX = HOST_EX + "mytags";
|
||||
public static final String URL_WATCHED_EX = HOST_EX + "watched";
|
||||
public static final String HOST_E = "https://" + DOMAIN_E + "/";
|
||||
public static final String API_E = HOST_E + "api.php";
|
||||
public static final String URL_FAVORITES_E = HOST_E + "favorites.php";
|
||||
public static final String URL_UCONFIG_E = HOST_E + "uconfig.php";
|
||||
public static final String URL_MY_TAGS_E = HOST_E + "mytags";
|
||||
public static final String URL_WATCHED_E = HOST_E + "watched";
|
||||
public static final String API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01";
|
||||
public static final String URL_POPULAR_E = "https://e-hentai.org/popular";
|
||||
public static final String URL_POPULAR_EX = "https://exhentai.org/popular";
|
||||
public static final String URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php";
|
||||
public static final String URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php";
|
||||
public static final String URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login";
|
||||
public static final String URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00";
|
||||
public static final String URL_FORUMS = "https://forums.e-hentai.org/";
|
||||
public static final String REFERER_EX = "https://" + DOMAIN_EX;
|
||||
public static final String ORIGIN_EX = REFERER_EX;
|
||||
public static final String REFERER_E = "https://" + DOMAIN_E;
|
||||
public static final String ORIGIN_E = REFERER_E;
|
||||
|
||||
public static String getGalleryDetailUrl(long gid, String token) {
|
||||
return getGalleryDetailUrl(gid, token, 0, false);
|
||||
}
|
||||
|
||||
public static String getHost() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return HOST_E;
|
||||
case SITE_EX:
|
||||
return HOST_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFavoritesUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_FAVORITES_E;
|
||||
case SITE_EX:
|
||||
return URL_FAVORITES_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getApiUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return API_E;
|
||||
case SITE_EX:
|
||||
return API_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getReferer() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return REFERER_E;
|
||||
case SITE_EX:
|
||||
return REFERER_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getOrigin() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return ORIGIN_E;
|
||||
case SITE_EX:
|
||||
return ORIGIN_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getUConfigUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_UCONFIG_E;
|
||||
case SITE_EX:
|
||||
return URL_UCONFIG_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMyTagsUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_MY_TAGS_E;
|
||||
case SITE_EX:
|
||||
return URL_MY_TAGS_EX;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getGalleryDetailUrl(long gid, String token, int index, boolean allComment) {
|
||||
UrlBuilder builder = new UrlBuilder(getHost() + "g/" + gid + '/' + token + '/');
|
||||
if (index != 0) {
|
||||
builder.addQuery("p", index);
|
||||
}
|
||||
if (allComment) {
|
||||
builder.addQuery("hc", 1);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static String getGalleryMultiPageViewerUrl(long gid, String token) {
|
||||
UrlBuilder builder = new UrlBuilder(getHost() + "mpv/" + gid + '/' + token + '/');
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static String getPageUrl(long gid, int index, String pToken) {
|
||||
return getHost() + "s/" + pToken + '/' + gid + '-' + (index + 1);
|
||||
}
|
||||
|
||||
public static String getAddFavorites(long gid, String token) {
|
||||
return getHost() + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav";
|
||||
}
|
||||
|
||||
public static String getDownloadArchive(long gid, String token, String or) {
|
||||
return getHost() + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or;
|
||||
}
|
||||
|
||||
public static String getTagDefinitionUrl(String tag) {
|
||||
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_');
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getPopularUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_POPULAR_E;
|
||||
case SITE_EX:
|
||||
return URL_POPULAR_EX;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getImageSearchUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_IMAGE_SEARCH_E;
|
||||
case SITE_EX:
|
||||
return URL_IMAGE_SEARCH_EX;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getWatchedUrl() {
|
||||
switch (Settings.getGallerySite()) {
|
||||
default:
|
||||
case SITE_E:
|
||||
return URL_WATCHED_E;
|
||||
case SITE_EX:
|
||||
return URL_WATCHED_EX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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.ehviewer.Settings
|
||||
import com.hippo.network.UrlBuilder
|
||||
|
||||
object EhUrl {
|
||||
const val SITE_E = 0
|
||||
const val SITE_EX = 1
|
||||
const val DOMAIN_EX = "exhentai.org"
|
||||
const val DOMAIN_E = "e-hentai.org"
|
||||
const val DOMAIN_LOFI = "lofi.e-hentai.org"
|
||||
const val HOST_EX = "https://$DOMAIN_EX/"
|
||||
const val API_EX = HOST_EX + "api.php"
|
||||
const val URL_FAVORITES_EX = HOST_EX + "favorites.php"
|
||||
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
|
||||
const val URL_MY_TAGS_EX = HOST_EX + "mytags"
|
||||
const val URL_WATCHED_EX = HOST_EX + "watched"
|
||||
const val HOST_E = "https://$DOMAIN_E/"
|
||||
const val API_E = HOST_E + "api.php"
|
||||
const val URL_FAVORITES_E = HOST_E + "favorites.php"
|
||||
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
|
||||
const val URL_MY_TAGS_E = HOST_E + "mytags"
|
||||
const val URL_WATCHED_E = HOST_E + "watched"
|
||||
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
|
||||
const val URL_POPULAR_E = "https://e-hentai.org/popular"
|
||||
const val URL_POPULAR_EX = "https://exhentai.org/popular"
|
||||
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
|
||||
const val URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php"
|
||||
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 = 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"
|
||||
const val ORIGIN_E = REFERER_E
|
||||
const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
|
||||
const val URL_PREFIX_THUMB_EX = "https://exhentai.org/t/"
|
||||
|
||||
val host: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> HOST_E
|
||||
SITE_EX -> HOST_EX
|
||||
else -> HOST_E
|
||||
}
|
||||
|
||||
val favoritesUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_FAVORITES_E
|
||||
SITE_EX -> URL_FAVORITES_EX
|
||||
else -> URL_FAVORITES_E
|
||||
}
|
||||
|
||||
val apiUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> API_E
|
||||
SITE_EX -> API_EX
|
||||
else -> API_E
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val referer: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> REFERER_E
|
||||
SITE_EX -> REFERER_EX
|
||||
else -> REFERER_E
|
||||
}
|
||||
|
||||
val origin: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> ORIGIN_E
|
||||
SITE_EX -> ORIGIN_EX
|
||||
else -> ORIGIN_E
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val uConfigUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_UCONFIG_E
|
||||
SITE_EX -> URL_UCONFIG_EX
|
||||
else -> URL_UCONFIG_E
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val myTagsUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_MY_TAGS_E
|
||||
SITE_EX -> URL_MY_TAGS_EX
|
||||
else -> URL_MY_TAGS_E
|
||||
}
|
||||
|
||||
val popularUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_POPULAR_E
|
||||
SITE_EX -> URL_POPULAR_EX
|
||||
else -> URL_POPULAR_E
|
||||
}
|
||||
|
||||
val imageSearchUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_IMAGE_SEARCH_E
|
||||
SITE_EX -> URL_IMAGE_SEARCH_EX
|
||||
else -> URL_IMAGE_SEARCH_E
|
||||
}
|
||||
|
||||
val watchedUrl: String
|
||||
get() = when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_WATCHED_E
|
||||
SITE_EX -> URL_WATCHED_EX
|
||||
else -> URL_WATCHED_E
|
||||
}
|
||||
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?): String {
|
||||
return getGalleryDetailUrl(gid, token, 0, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean): String {
|
||||
val builder = UrlBuilder(host + "g/" + gid + '/' + token + '/')
|
||||
if (index != 0) {
|
||||
builder.addQuery("p", index)
|
||||
}
|
||||
if (allComment) {
|
||||
builder.addQuery("hc", 1)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String): String {
|
||||
val builder = UrlBuilder(host + "mpv/" + gid + '/' + token + '/')
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getPageUrl(gid: Long, index: Int, pToken: String): String {
|
||||
return host + "s/" + pToken + '/' + gid + '-' + (index + 1)
|
||||
}
|
||||
|
||||
fun getAddFavorites(gid: Long, token: String?): String {
|
||||
return host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
|
||||
}
|
||||
|
||||
fun getDownloadArchive(gid: Long, token: String?, or: String): String {
|
||||
return host + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or
|
||||
}
|
||||
|
||||
fun getTagDefinitionUrl(tag: String): String {
|
||||
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
|
||||
}
|
||||
|
||||
fun getThumbUrlPrefix(): String {
|
||||
return when (Settings.getGallerySite()) {
|
||||
SITE_E -> URL_PREFIX_THUMB_E
|
||||
SITE_EX -> URL_PREFIX_THUMB_EX
|
||||
else -> URL_PREFIX_THUMB_E
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,189 +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 android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EhUtils {
|
||||
|
||||
public static final int NONE = -1; // Use it for homepage
|
||||
public static final int PRIVATE = 0x400;
|
||||
public static final int UNKNOWN = 0x800;
|
||||
|
||||
public static final int BG_COLOR_DOUJINSHI = 0xfff44336;
|
||||
public static final int BG_COLOR_MANGA = 0xffff9800;
|
||||
public static final int BG_COLOR_ARTIST_CG = 0xfffbc02d;
|
||||
public static final int BG_COLOR_GAME_CG = 0xff4caf50;
|
||||
public static final int BG_COLOR_WESTERN = 0xff8bc34a;
|
||||
public static final int BG_COLOR_NON_H = 0xff2196f3;
|
||||
public static final int BG_COLOR_IMAGE_SET = 0xff3f51b5;
|
||||
public static final int BG_COLOR_COSPLAY = 0xff9c27b0;
|
||||
public static final int BG_COLOR_ASIAN_PORN = 0xff9575cd;
|
||||
public static final int BG_COLOR_MISC = 0xfff06292;
|
||||
public static final int BG_COLOR_UNKNOWN = Color.BLACK;
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||
public static final Pattern PATTERN_TITLE_PREFIX = Pattern.compile(
|
||||
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*");
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||
public static final Pattern PATTERN_TITLE_SUFFIX = Pattern.compile(
|
||||
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final int[] CATEGORY_VALUES = {
|
||||
EhConfig.MISC,
|
||||
EhConfig.DOUJINSHI,
|
||||
EhConfig.MANGA,
|
||||
EhConfig.ARTIST_CG,
|
||||
EhConfig.GAME_CG,
|
||||
EhConfig.IMAGE_SET,
|
||||
EhConfig.COSPLAY,
|
||||
EhConfig.ASIAN_PORN,
|
||||
EhConfig.NON_H,
|
||||
EhConfig.WESTERN,
|
||||
PRIVATE,
|
||||
UNKNOWN};
|
||||
|
||||
private static final String[][] CATEGORY_STRINGS = {
|
||||
new String[]{"misc"},
|
||||
new String[]{"doujinshi"},
|
||||
new String[]{"manga"},
|
||||
new String[]{"artistcg", "Artist CG Sets", "Artist CG"},
|
||||
new String[]{"gamecg", "Game CG Sets", "Game CG"},
|
||||
new String[]{"imageset", "Image Sets", "Image Set"},
|
||||
new String[]{"cosplay"},
|
||||
new String[]{"asianporn", "Asian Porn"},
|
||||
new String[]{"non-h"},
|
||||
new String[]{"western"},
|
||||
new String[]{"private"},
|
||||
new String[]{"unknown"}
|
||||
};
|
||||
|
||||
public static int getCategory(String type) {
|
||||
int i;
|
||||
for (i = 0; i < CATEGORY_STRINGS.length - 1; i++) {
|
||||
for (String str : CATEGORY_STRINGS[i])
|
||||
if (str.equalsIgnoreCase(type))
|
||||
return CATEGORY_VALUES[i];
|
||||
}
|
||||
|
||||
return CATEGORY_VALUES[i];
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getCategory(int type) {
|
||||
int i;
|
||||
for (i = 0; i < CATEGORY_VALUES.length - 1; i++) {
|
||||
if (CATEGORY_VALUES[i] == type)
|
||||
break;
|
||||
}
|
||||
return CATEGORY_STRINGS[i][0];
|
||||
}
|
||||
|
||||
public static int getCategoryColor(int category) {
|
||||
return switch (category) {
|
||||
case EhConfig.DOUJINSHI -> BG_COLOR_DOUJINSHI;
|
||||
case EhConfig.MANGA -> BG_COLOR_MANGA;
|
||||
case EhConfig.ARTIST_CG -> BG_COLOR_ARTIST_CG;
|
||||
case EhConfig.GAME_CG -> BG_COLOR_GAME_CG;
|
||||
case EhConfig.WESTERN -> BG_COLOR_WESTERN;
|
||||
case EhConfig.NON_H -> BG_COLOR_NON_H;
|
||||
case EhConfig.IMAGE_SET -> BG_COLOR_IMAGE_SET;
|
||||
case EhConfig.COSPLAY -> BG_COLOR_COSPLAY;
|
||||
case EhConfig.ASIAN_PORN -> BG_COLOR_ASIAN_PORN;
|
||||
case EhConfig.MISC -> BG_COLOR_MISC;
|
||||
default -> BG_COLOR_UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
public static void signOut() {
|
||||
EhApplication.getEhCookieStore().signOut();
|
||||
Settings.putAvatar(null);
|
||||
Settings.putDisplayName(null);
|
||||
Settings.putNeedSignIn(true);
|
||||
}
|
||||
|
||||
public static boolean needSignedIn() {
|
||||
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore().hasSignedIn();
|
||||
}
|
||||
|
||||
public static String getSuitableTitle(GalleryInfo gi) {
|
||||
if (Settings.getShowJpnTitle()) {
|
||||
return TextUtils.isEmpty(gi.getTitleJpn()) ? gi.getTitle() : gi.getTitleJpn();
|
||||
} else {
|
||||
return TextUtils.isEmpty(gi.getTitle()) ? gi.getTitleJpn() : gi.getTitle();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String extractTitle(String title) {
|
||||
if (null == title) {
|
||||
return null;
|
||||
}
|
||||
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("");
|
||||
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("");
|
||||
// Sometimes title is combined by romaji and english translation.
|
||||
// Only need romaji.
|
||||
// TODO But not sure every '|' means that
|
||||
int index = title.indexOf('|');
|
||||
if (index >= 0) {
|
||||
title = title.substring(0, index);
|
||||
}
|
||||
if (title.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
public static String handleThumbUrlResolution(String url) {
|
||||
if (null == url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String resolution;
|
||||
switch (Settings.getThumbResolution()) {
|
||||
default:
|
||||
case 0: // Auto
|
||||
return url;
|
||||
case 1: // 250
|
||||
resolution = "250";
|
||||
break;
|
||||
case 2: // 300
|
||||
resolution = "300";
|
||||
break;
|
||||
}
|
||||
|
||||
int index1 = url.lastIndexOf('_');
|
||||
int index2 = url.lastIndexOf('.');
|
||||
if (index1 >= 0 && index2 >= 0 && index1 < index2) {
|
||||
return url.substring(0, index1 + 1) + resolution + url.substring(index2);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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 android.text.TextUtils
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ehCookieStore
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object EhUtils {
|
||||
const val NONE = -1 // Use it for homepage
|
||||
const val PRIVATE = 0x400
|
||||
const val UNKNOWN = 0x800
|
||||
|
||||
// https://youtrack.jetbrains.com/issue/KT-4749
|
||||
private const val BG_COLOR_DOUJINSHI = 0xfff44336u
|
||||
private const val BG_COLOR_MANGA = 0xffff9800u
|
||||
private const val BG_COLOR_ARTIST_CG = 0xfffbc02du
|
||||
private const val BG_COLOR_GAME_CG = 0xff4caf50u
|
||||
private const val BG_COLOR_WESTERN = 0xff8bc34au
|
||||
private const val BG_COLOR_NON_H = 0xff2196f3u
|
||||
private const val BG_COLOR_IMAGE_SET = 0xff3f51b5u
|
||||
private const val BG_COLOR_COSPLAY = 0xff9c27b0u
|
||||
private const val BG_COLOR_ASIAN_PORN = 0xff9575cdu
|
||||
private const val BG_COLOR_MISC = 0xfff06292u
|
||||
private const val BG_COLOR_UNKNOWN = 0xff000000u
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||
private val PATTERN_TITLE_PREFIX = Pattern.compile(
|
||||
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*"
|
||||
)
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
|
||||
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
)
|
||||
|
||||
private val CATEGORY_VALUES = hashMapOf(
|
||||
EhConfig.MISC to arrayOf("misc"),
|
||||
EhConfig.DOUJINSHI to arrayOf("doujinshi"),
|
||||
EhConfig.MANGA to arrayOf("manga"),
|
||||
EhConfig.ARTIST_CG to arrayOf("artistcg", "Artist CG Sets", "Artist CG"),
|
||||
EhConfig.GAME_CG to arrayOf("gamecg", "Game CG Sets", "Game CG"),
|
||||
EhConfig.IMAGE_SET to arrayOf("imageset", "Image Sets", "Image Set"),
|
||||
EhConfig.COSPLAY to arrayOf("cosplay"),
|
||||
EhConfig.ASIAN_PORN to arrayOf("asianporn", "Asian Porn"),
|
||||
EhConfig.NON_H to arrayOf("non-h"),
|
||||
EhConfig.WESTERN to arrayOf("western"),
|
||||
PRIVATE to arrayOf("private"),
|
||||
UNKNOWN to arrayOf("unknown")
|
||||
)
|
||||
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) {
|
||||
for (str in entry.first)
|
||||
if (str.equals(type, ignoreCase = true))
|
||||
return entry.second
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
fun getCategory(type: Int): String {
|
||||
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
||||
}
|
||||
|
||||
fun getCategoryColor(category: Int): Int {
|
||||
return when (category) {
|
||||
EhConfig.DOUJINSHI -> BG_COLOR_DOUJINSHI
|
||||
EhConfig.MANGA -> BG_COLOR_MANGA
|
||||
EhConfig.ARTIST_CG -> BG_COLOR_ARTIST_CG
|
||||
EhConfig.GAME_CG -> BG_COLOR_GAME_CG
|
||||
EhConfig.WESTERN -> BG_COLOR_WESTERN
|
||||
EhConfig.NON_H -> BG_COLOR_NON_H
|
||||
EhConfig.IMAGE_SET -> BG_COLOR_IMAGE_SET
|
||||
EhConfig.COSPLAY -> BG_COLOR_COSPLAY
|
||||
EhConfig.ASIAN_PORN -> BG_COLOR_ASIAN_PORN
|
||||
EhConfig.MISC -> BG_COLOR_MISC
|
||||
else -> BG_COLOR_UNKNOWN
|
||||
}.toInt()
|
||||
}
|
||||
|
||||
fun signOut() {
|
||||
ehCookieStore.signOut()
|
||||
Settings.putAvatar(null)
|
||||
Settings.putDisplayName(null)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
Settings.putNeedSignIn(true)
|
||||
Settings.putSelectSite(true)
|
||||
}
|
||||
|
||||
fun needSignedIn(): Boolean {
|
||||
return Settings.getNeedSignIn() && !ehCookieStore.hasSignedIn()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
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? {
|
||||
var title: String = fullTitle ?: return null
|
||||
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("")
|
||||
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("")
|
||||
// Sometimes title is combined by romaji and english translation.
|
||||
// Only need romaji.
|
||||
// TODO But not sure every '|' means that
|
||||
val index = title.indexOf('|')
|
||||
if (index >= 0) {
|
||||
title = title.substring(0, index)
|
||||
}
|
||||
return title.ifEmpty { null }
|
||||
}
|
||||
|
||||
fun fixThumbUrl(url: String): String {
|
||||
return EhUrl.getThumbUrlPrefix() +
|
||||
url.removePrefix(EhUrl.URL_PREFIX_THUMB_E).removePrefix(EhUrl.URL_PREFIX_THUMB_EX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun handleThumbUrlResolution(url: String?): String? {
|
||||
if (null == url) {
|
||||
return null
|
||||
}
|
||||
val resolution = when (Settings.getThumbResolution()) {
|
||||
0 -> return url
|
||||
1 -> "250"
|
||||
2 -> "300"
|
||||
else -> return url
|
||||
}
|
||||
val index1 = url.lastIndexOf('_')
|
||||
val index2 = url.lastIndexOf('.')
|
||||
return if (index1 >= 0 && index2 >= 0 && index1 < index2) {
|
||||
url.substring(0, index1 + 1) + resolution + url.substring(index2)
|
||||
} else {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class FavListUrlBuilder(
|
||||
}
|
||||
|
||||
fun build(): String {
|
||||
val ub = UrlBuilder(EhUrl.getFavoritesUrl())
|
||||
val ub = UrlBuilder(EhUrl.favoritesUrl)
|
||||
if (isValidFavCat(favCat)) {
|
||||
ub.addQuery("favcat", favCat.toString())
|
||||
} else if (favCat == FAV_CAT_ALL) {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -275,21 +275,14 @@ data class ListUrlBuilder(
|
||||
return when (mode) {
|
||||
MODE_NORMAL, MODE_SUBSCRIPTION -> {
|
||||
val url: String = if (mode == MODE_NORMAL) {
|
||||
EhUrl.getHost()
|
||||
EhUrl.host
|
||||
} else {
|
||||
EhUrl.getWatchedUrl()
|
||||
EhUrl.watchedUrl
|
||||
}
|
||||
val ub = UrlBuilder(url)
|
||||
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")
|
||||
@@ -326,7 +332,7 @@ data class ListUrlBuilder(
|
||||
}
|
||||
|
||||
MODE_UPLOADER -> {
|
||||
val sb = StringBuilder(EhUrl.getHost())
|
||||
val sb = StringBuilder(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
sb.append("uploader/")
|
||||
sb.append(encodeUTF8(it))
|
||||
@@ -344,7 +350,7 @@ data class ListUrlBuilder(
|
||||
}
|
||||
|
||||
MODE_TAG -> {
|
||||
val sb = StringBuilder(EhUrl.getHost())
|
||||
val sb = StringBuilder(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
sb.append("tag/")
|
||||
sb.append(encodeUTF8(it))
|
||||
@@ -361,8 +367,8 @@ data class ListUrlBuilder(
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
MODE_WHATS_HOT -> EhUrl.getPopularUrl()
|
||||
MODE_IMAGE_SEARCH -> EhUrl.getImageSearchUrl()
|
||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
|
||||
MODE_TOPLIST -> {
|
||||
val sb = StringBuilder(EhUrl.HOST_E)
|
||||
sb.append("toplist.php?tl=")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,208 +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 com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ArchiveParser {
|
||||
|
||||
private static final Pattern PATTERN_ARCHIVE_URL = Pattern.compile("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>");
|
||||
private static final Pattern PATTERN_HATH_FORM = Pattern.compile("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">");
|
||||
private static final Pattern 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>");
|
||||
private static final Pattern PATTERN_NEED_HATH_CLIENT = Pattern.compile("You must have a H@H client assigned to your account to use this feature\\.");
|
||||
|
||||
public static Result parse(String body) {
|
||||
Matcher m = PATTERN_HATH_FORM.matcher(body);
|
||||
if (!m.find()) {
|
||||
return new Result(null, null);
|
||||
}
|
||||
String paramOr = m.group(1);
|
||||
|
||||
var archiveList = new ArrayList<Archive>();
|
||||
var d = Jsoup.parse(body);
|
||||
var es = d.select("#db>div>div");
|
||||
for (var e : es) {
|
||||
try {
|
||||
var res = e.selectFirst("form>input").attr("value");
|
||||
var name = e.selectFirst("form>div>input").attr("value");
|
||||
var size = e.selectFirst("p>strong").text();
|
||||
var cost = e.selectFirst("div>strong").text();
|
||||
var item = new Archive(res, name, size, cost, false);
|
||||
archiveList.add(item);
|
||||
} catch (NullPointerException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
m = PATTERN_HATH_ARCHIVE.matcher(body);
|
||||
while (m.find()) {
|
||||
String res = ParserUtils.trim(m.group(1));
|
||||
String name = ParserUtils.trim(m.group(2));
|
||||
String size = ParserUtils.trim(m.group(3));
|
||||
String cost = ParserUtils.trim(m.group(4));
|
||||
var item = new Archive(res, name, size, cost, true);
|
||||
archiveList.add(item);
|
||||
}
|
||||
|
||||
return new Result(paramOr, archiveList);
|
||||
}
|
||||
|
||||
public static String parseArchiveUrl(String body) throws NoHAtHClientException {
|
||||
Matcher m = PATTERN_NEED_HATH_CLIENT.matcher(body);
|
||||
if (m.find()) {
|
||||
throw new NoHAtHClientException("No H@H client");
|
||||
}
|
||||
|
||||
var d = Jsoup.parse(body);
|
||||
var a = d.selectFirst("#continue>a[href]");
|
||||
if (a != null) {
|
||||
return a.attr("href") + "?start=1";
|
||||
}
|
||||
|
||||
// TODO: Check more errors
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final class Archive {
|
||||
private final String res;
|
||||
private final String name;
|
||||
private final String size;
|
||||
private final String cost;
|
||||
private final boolean isHAtH;
|
||||
|
||||
public Archive(String res, String name, String size, String cost, boolean isHAtH) {
|
||||
this.res = res;
|
||||
this.name = name;
|
||||
this.size = size;
|
||||
this.cost = cost;
|
||||
this.isHAtH = isHAtH;
|
||||
}
|
||||
|
||||
public String format(IntFunction<String> getString) {
|
||||
if (isHAtH) {
|
||||
var costStr = cost.equals("Free") ? getString.apply(R.string.archive_free) : cost;
|
||||
return String.format("[H@H] %s [%s] [%s]", name, size, costStr);
|
||||
} else {
|
||||
var nameStr = getString.apply(res.equals("org") ? R.string.archive_original : R.string.archive_resample);
|
||||
var costStr = cost.equals("Free!") ? getString.apply(R.string.archive_free) : cost;
|
||||
return String.format("%s [%s] [%s]", nameStr, size, costStr);
|
||||
}
|
||||
}
|
||||
|
||||
public String res() {
|
||||
return res;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public String cost() {
|
||||
return cost;
|
||||
}
|
||||
|
||||
public boolean isHAtH() {
|
||||
return isHAtH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (Archive) obj;
|
||||
return Objects.equals(this.res, that.res) &&
|
||||
Objects.equals(this.name, that.name) &&
|
||||
Objects.equals(this.size, that.size) &&
|
||||
Objects.equals(this.cost, that.cost) &&
|
||||
this.isHAtH == that.isHAtH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(res, name, size, cost, isHAtH);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Archive[" +
|
||||
"res=" + res + ", " +
|
||||
"name=" + name + ", " +
|
||||
"size=" + size + ", " +
|
||||
"cost=" + cost + ", " +
|
||||
"isHAtH=" + isHAtH + ']';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final class Result {
|
||||
private final String paramOr;
|
||||
private final List<Archive> archiveList;
|
||||
|
||||
public Result(String paramOr, List<Archive> archiveList) {
|
||||
this.paramOr = paramOr;
|
||||
this.archiveList = archiveList;
|
||||
}
|
||||
|
||||
public String paramOr() {
|
||||
return paramOr;
|
||||
}
|
||||
|
||||
public List<Archive> archiveList() {
|
||||
return archiveList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (Result) obj;
|
||||
return Objects.equals(this.paramOr, that.paramOr) &&
|
||||
Objects.equals(this.archiveList, that.archiveList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(paramOr, archiveList);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Result[" +
|
||||
"paramOr=" + paramOr + ", " +
|
||||
"archiveList=" + archiveList + ']';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.client.exception.NoHAtHClientException
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object ArchiveParser {
|
||||
private val PATTERN_ARCHIVE_URL =
|
||||
Regex("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
|
||||
private val PATTERN_CURRENT_FUNDS =
|
||||
Regex("<p>([\\d,]+) GP \\[[^]]*] ([\\d,]+) Credits \\[[^]]*]</p>")
|
||||
private val PATTERN_HATH_FORM =
|
||||
Regex("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
|
||||
private val PATTERN_HATH_ARCHIVE =
|
||||
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 =
|
||||
Regex("You must have a H@H client assigned to your account to use this feature\\.")
|
||||
|
||||
fun parse(body: String): Result? {
|
||||
val m = PATTERN_HATH_FORM.find(body) ?: return null
|
||||
val paramOr = m.groupValues[1]
|
||||
val archiveList = ArrayList<Archive>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@Throws(NoHAtHClientException::class)
|
||||
fun parseArchiveUrl(body: String): String? {
|
||||
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
|
||||
}
|
||||
|
||||
class Archive(
|
||||
val res: String,
|
||||
val name: String,
|
||||
val size: String,
|
||||
val cost: String,
|
||||
val isHAtH: Boolean
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package com.hippo.ehviewer.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.ImageDecoder;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Process;
|
||||
@@ -38,7 +37,6 @@ import com.hippo.yorozuya.thread.PriorityThread;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -48,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();
|
||||
@@ -56,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, ByteBuffer> 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()),
|
||||
@@ -189,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) {
|
||||
|
||||
}
|
||||
|
||||
@@ -295,7 +294,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
|
||||
// Check index valid
|
||||
if (index < 0 || index >= size) {
|
||||
notifyPageFailed(index);
|
||||
notifyPageFailed(index, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -305,10 +304,10 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer buffer = archiveAccessor.extractToByteBuffer(index);
|
||||
Image.CloseableSource src = archiveAccessor.getImageSource(index);
|
||||
|
||||
synchronized (streams) {
|
||||
streams.put(index, buffer);
|
||||
streams.put(index, src);
|
||||
streams.notify();
|
||||
}
|
||||
}
|
||||
@@ -320,7 +319,7 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int index;
|
||||
ByteBuffer buffer;
|
||||
Image.CloseableSource src;
|
||||
synchronized (streams) {
|
||||
if (streams.isEmpty()) {
|
||||
try {
|
||||
@@ -331,26 +330,21 @@ public class ArchivePageLoader extends PageLoader2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
Iterator<Map.Entry<Integer, ByteBuffer>> iterator = streams.entrySet().iterator();
|
||||
Map.Entry<Integer, ByteBuffer> 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();
|
||||
buffer = entry.getValue();
|
||||
src = entry.getValue();
|
||||
}
|
||||
|
||||
Image image = null;
|
||||
if (buffer != null) {
|
||||
try {
|
||||
image = Image.decode(buffer.asReadOnlyBuffer());
|
||||
} catch (ImageDecoder.DecodeException e) {
|
||||
UriArchiveAccessor.releaseByteBuffer(buffer);
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (src != null) {
|
||||
image = Image.decode(src);
|
||||
}
|
||||
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`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +91,7 @@ class RestoreDownloadPreference constructor(
|
||||
runBlocking {
|
||||
fillGalleryListByApi(
|
||||
ArrayList(restoreItemList),
|
||||
EhUrl.getReferer()
|
||||
EhUrl.referer
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
|
||||
@@ -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,344 +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 android.graphics.BitmapFactory;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.beerbelly.SimpleDiskCache;
|
||||
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.io.UniFileInputStreamPipe;
|
||||
import com.hippo.io.UniFileOutputStreamPipe;
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
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.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class SpiderDen {
|
||||
|
||||
@Nullable
|
||||
private static SimpleDiskCache sCache;
|
||||
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 SimpleDiskCache(new File(context.getCacheDir(), "image"),
|
||||
MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280) * 1024 * 1024);
|
||||
}
|
||||
|
||||
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);
|
||||
return sCache.contain(key);
|
||||
}
|
||||
|
||||
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);
|
||||
InputStreamPipe pipe = sCache.getInputStreamPipe(key);
|
||||
if (pipe == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OutputStream os = null;
|
||||
try {
|
||||
// Get extension
|
||||
String extension;
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
pipe.obtain();
|
||||
BitmapFactory.decodeStream(pipe.open(), null, options);
|
||||
pipe.close();
|
||||
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(options.outMimeType);
|
||||
if (extension != null) {
|
||||
extension = '.' + extension;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// Fix extension
|
||||
extension = fixExtension(extension);
|
||||
// Copy from cache to download dir
|
||||
UniFile file = dir.createFile(generateImageFilename(index, extension));
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
os = file.openOutputStream();
|
||||
IOUtils.copy(pipe.open(), os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(os);
|
||||
pipe.close();
|
||||
pipe.release();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (sCache == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
return sCache.getOutputStreamPipe(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
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);
|
||||
return sCache.getInputStreamPipe(key);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,328 @@
|
||||
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) {
|
||||
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
|
||||
val src = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package com.hippo.ehviewer.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.navigation.ActivityNavigator
|
||||
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.ui.compose.CookieSignInScene
|
||||
import com.hippo.ehviewer.ui.compose.SelectSiteScreen
|
||||
import com.hippo.ehviewer.ui.compose.SignInScreen
|
||||
import com.hippo.ehviewer.ui.compose.WebviewSignInScreen
|
||||
|
||||
class LoginActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
Mdc3Theme {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = SIGN_IN_ROUTE_NAME) {
|
||||
composable(SIGN_IN_ROUTE_NAME) {
|
||||
SignInScreen(navController)
|
||||
}
|
||||
|
||||
composable(WEBVIEW_SIGN_IN_ROUTE_NAME) {
|
||||
WebviewSignInScreen(navController)
|
||||
}
|
||||
|
||||
composable(COOKIE_SIGN_IN_ROUTE_NAME) {
|
||||
CookieSignInScene(navController)
|
||||
}
|
||||
|
||||
composable(SELECT_SITE_ROUTE_NAME) {
|
||||
SelectSiteScreen {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SIGN_IN_ROUTE_NAME = "SignIn"
|
||||
const val WEBVIEW_SIGN_IN_ROUTE_NAME = "WebViewSignIn"
|
||||
const val COOKIE_SIGN_IN_ROUTE_NAME = "CookieSignIn"
|
||||
const val SELECT_SITE_ROUTE_NAME = "SelectSite"
|
||||
}
|
||||
}
|
||||
@@ -23,16 +23,23 @@ 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
|
||||
import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
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
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.customview.widget.Openable
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
@@ -58,16 +65,16 @@ import com.hippo.ehviewer.client.parser.GalleryListUrlParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
|
||||
import com.hippo.ehviewer.databinding.ActivityMainBinding
|
||||
import com.hippo.ehviewer.download.DownloadService
|
||||
import com.hippo.ehviewer.ui.login.LoginActivity
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
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
|
||||
@@ -82,26 +89,14 @@ import java.io.OutputStream
|
||||
class MainActivity : EhActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var navController: NavController
|
||||
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 {
|
||||
@@ -190,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
|
||||
@@ -215,15 +209,7 @@ class MainActivity : EhActivity() {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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()) {
|
||||
@@ -234,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)
|
||||
|
||||
@@ -275,6 +281,19 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun bypassVpn() {
|
||||
val network = connectivityManager.activeNetwork
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
capabilities?.let {
|
||||
if (it.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
val builder = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
connectivityManager.registerNetworkCallback(builder.build(), mNetworkCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
private fun checkAppLinkVerify() {
|
||||
@@ -333,8 +352,7 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
|
||||
private fun checkMeteredNetwork() {
|
||||
val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
if (cm.isActiveNetworkMetered) {
|
||||
if (connectivityManager.isActiveNetworkMetered) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Snackbar.make(
|
||||
binding.drawView,
|
||||
@@ -502,4 +520,25 @@ class MainActivity : EhActivity() {
|
||||
override fun onDrawerClosed(drawerView: View) {}
|
||||
override fun onDrawerStateChanged(newState: Int) {}
|
||||
}
|
||||
|
||||
private val mNetworkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
private val TAG = "mNetworkCallback"
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
Log.d(TAG, "onAvailable: $network")
|
||||
connectivityManager.bindProcessToNetwork(network)
|
||||
availableNetworks.add(network)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Log.d(TAG, "onLost: $network")
|
||||
val activeNetwork = availableNetworks.last()
|
||||
availableNetworks.remove(network)
|
||||
if (network == activeNetwork) {
|
||||
connectivityManager.bindProcessToNetwork(
|
||||
availableNetworks.takeIf { it.isNotEmpty() }?.last()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>(
|
||||
@@ -231,11 +232,12 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
addPreferencesFromResource(R.xml.advanced_settings)
|
||||
val dumpLogcat = findPreference<Preference>(KEY_DUMP_LOGCAT)
|
||||
val appLanguage = findPreference<Preference>(KEY_APP_LANGUAGE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) appLanguage!!.isVisible = false
|
||||
val importData = findPreference<Preference>(KEY_IMPORT_DATA)
|
||||
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 {
|
||||
@@ -246,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 {
|
||||
@@ -337,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()
|
||||
@@ -378,7 +385,7 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
val key = preference.key
|
||||
if (KEY_APP_LANGUAGE == key && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
if (KEY_APP_LANGUAGE == key) {
|
||||
if ("system" == newValue) {
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
|
||||
} else {
|
||||
|
||||
@@ -20,12 +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.launchNonCancellable
|
||||
|
||||
class EhFragment : BasePreferenceFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@@ -40,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
|
||||
@@ -57,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 {
|
||||
@@ -67,6 +77,13 @@ class EhFragment : BasePreferenceFragment() {
|
||||
return true
|
||||
} else if (Settings.KEY_GALLERY_SITE == key) {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
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]
|
||||
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
package com.hippo.ehviewer.ui.compose
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -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,25 +45,24 @@ 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
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
|
||||
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() }
|
||||
@@ -93,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))
|
||||
}
|
||||
|
||||
@@ -123,6 +120,9 @@ fun CookieSignInScene(navController: NavController) {
|
||||
runCatching {
|
||||
EhEngine.getProfile()
|
||||
}.onSuccess {
|
||||
withNonCancellableContext {
|
||||
postLogin()
|
||||
}
|
||||
withUIContext {
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
Mdc3Theme {
|
||||
val navController = rememberNavController()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
composable(COOKIE_SIGN_IN_ROUTE_NAME) {
|
||||
CookieSignInScene()
|
||||
}
|
||||
|
||||
composable(SELECT_SITE_ROUTE_NAME) {
|
||||
val selectSite by remember { mutableStateOf(Settings.getSelectSite()) }
|
||||
if (selectSite) {
|
||||
SelectSiteScreen()
|
||||
} else {
|
||||
SideEffect {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
const val SELECT_SITE_ROUTE_NAME = "SelectSite"
|
||||
+26
-27
@@ -1,5 +1,6 @@
|
||||
package com.hippo.ehviewer.ui.compose
|
||||
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,27 +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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-11
@@ -1,4 +1,4 @@
|
||||
package com.hippo.ehviewer.ui.compose
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -20,11 +20,10 @@ 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
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -51,25 +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.ehviewer.ui.LoginActivity.Companion.COOKIE_SIGN_IN_ROUTE_NAME
|
||||
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
|
||||
import com.hippo.ehviewer.ui.LoginActivity.Companion.WEBVIEW_SIGN_IN_ROUTE_NAME
|
||||
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) }
|
||||
@@ -114,6 +110,9 @@ fun SignInScreen(navController: NavController) {
|
||||
showErrorDialog = true
|
||||
}
|
||||
}.onSuccess {
|
||||
withNonCancellableContext {
|
||||
postLogin()
|
||||
}
|
||||
withUIContext {
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
}
|
||||
@@ -192,7 +191,7 @@ fun SignInScreen(navController: NavController) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row {
|
||||
OutlinedButton(
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
UrlOpener.openUrl(
|
||||
requireActivity(context),
|
||||
@@ -252,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 {
|
||||
+21
-10
@@ -1,4 +1,4 @@
|
||||
package com.hippo.ehviewer.ui.compose
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
@@ -7,23 +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.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
|
||||
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()
|
||||
|
||||
@@ -69,9 +73,9 @@ fun WebviewSignInScreen(navController: NavController) {
|
||||
EhCookieStore.newCookie(
|
||||
cookie,
|
||||
domain,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
forcePersistent = true,
|
||||
forceLongLive = true,
|
||||
forceNotHostOnly = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -87,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)
|
||||
@@ -97,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
|
||||
@@ -1029,8 +1021,8 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
return
|
||||
}
|
||||
val info = mList!![position]
|
||||
if (info.thumb != null) {
|
||||
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), info.thumb!!)
|
||||
info.thumb?.let {
|
||||
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) {
|
||||
@@ -941,7 +942,7 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
mModifyGiList.clear()
|
||||
val url: String = if (local) {
|
||||
// Local fav is shown now, but operation need be done for cloud fav
|
||||
EhUrl.getFavoritesUrl()
|
||||
EhUrl.favoritesUrl
|
||||
} else {
|
||||
mUrlBuilder!!.build()
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ 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
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.widget.TileThumb
|
||||
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager
|
||||
@@ -169,9 +169,12 @@ internal abstract class GalleryAdapter(
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
|
||||
val gi = getDataAt(position) ?: return
|
||||
if (gi is LocalFavoriteInfo) {
|
||||
gi.thumb = EhUtils.fixThumbUrl(gi.thumb!!)
|
||||
}
|
||||
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
|
||||
@@ -205,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,8 @@ 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
|
||||
import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||
@@ -85,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
|
||||
@@ -99,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
|
||||
@@ -148,6 +150,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
}
|
||||
private var mArchiveFormParamOr: String? = null
|
||||
private var mArchiveList: List<ArchiveParser.Archive>? = null
|
||||
private var mCurrentFunds: HomeParser.Funds? = null
|
||||
|
||||
@State
|
||||
private var mState = STATE_INIT
|
||||
@@ -175,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) {
|
||||
@@ -258,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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,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)
|
||||
}
|
||||
|
||||
@@ -393,7 +402,8 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
} else {
|
||||
binding.content.comments.comments.visibility = View.GONE
|
||||
}
|
||||
binding.content.previews.previews.setOnClickListener(this)
|
||||
binding.content.previews.previews.setOnClickListener { navigateToPreview() }
|
||||
binding.content.previews.previewText.setOnClickListener { navigateToPreview(true) }
|
||||
mViewTransition2 = ViewTransition(binding.content.belowHeader, binding.content.progress)
|
||||
if (prepareData()) {
|
||||
if (mGalleryDetail != null) {
|
||||
@@ -504,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()
|
||||
@@ -560,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))
|
||||
}
|
||||
@@ -617,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)
|
||||
@@ -630,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)
|
||||
@@ -645,7 +666,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
tag.setTag(R.id.tag, tg.groupName + ":" + tagStr)
|
||||
tag.setOnClickListener(this)
|
||||
tag.setOnLongClickListener(this)
|
||||
j++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -690,7 +710,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
private fun bindPreviews(gd: GalleryDetail) {
|
||||
val inflater = layoutInflater
|
||||
resourcesOrNull ?: return
|
||||
val previewNum = Settings.getPreviewNum()
|
||||
_binding ?: return
|
||||
binding.content.previews.run {
|
||||
gridLayout.removeAllViews()
|
||||
@@ -698,7 +717,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (gd.previewPages <= 0 || previewSet == null || previewSet.size() == 0) {
|
||||
previewText.setText(R.string.no_previews)
|
||||
return
|
||||
} else if (gd.previewPages == 1 && previewSet.size() <= previewNum) {
|
||||
} else if (gd.previewPages == 1) {
|
||||
previewText.setText(R.string.no_more_previews)
|
||||
} else {
|
||||
previewText.setText(R.string.more_previews)
|
||||
@@ -706,16 +725,19 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
val columnWidth = Settings.getThumbSize()
|
||||
gridLayout.setColumnSize(columnWidth)
|
||||
gridLayout.setStrategy(SimpleGridAutoSpanLayout.STRATEGY_SUITABLE_SIZE)
|
||||
val size = previewNum.coerceAtMost(previewSet.size())
|
||||
for (i in 0 until size) {
|
||||
val view = inflater.inflate(R.layout.item_gallery_preview, gridLayout, false)
|
||||
gridLayout.addView(view)
|
||||
val image = view.findViewById<LoadImageView>(R.id.image)
|
||||
previewSet.load(image, gd.gid, i)
|
||||
image.setTag(R.id.index, i)
|
||||
image.setOnClickListener(this@GalleryDetailScene)
|
||||
val text = view.findViewById<TextView>(R.id.text)
|
||||
text.text = (previewSet.getPosition(i) + 1).toString()
|
||||
viewLifecycleOwner.lifecycleScope.launchIO {
|
||||
for (i in 0 until previewSet.size()) {
|
||||
val view = inflater.inflate(R.layout.item_gallery_preview, gridLayout, false)
|
||||
val image = view.findViewById<LoadImageView>(R.id.image)
|
||||
previewSet.load(image, gd.gid, i)
|
||||
image.setTag(R.id.index, i)
|
||||
withUIContext {
|
||||
gridLayout.addView(view)
|
||||
image.setOnClickListener(this@GalleryDetailScene)
|
||||
val text = view.findViewById<TextView>(R.id.text)
|
||||
text.text = (previewSet.getPosition(i) + 1).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -763,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()
|
||||
@@ -774,6 +796,15 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToPreview(nextPage: Boolean = false) {
|
||||
mGalleryDetail?.let {
|
||||
val args = Bundle()
|
||||
args.putParcelable(GalleryPreviewsScene.KEY_GALLERY_INFO, it)
|
||||
args.putBoolean(GalleryPreviewsScene.KEY_NEXT_PAGE, nextPage)
|
||||
navigate(R.id.galleryPreviewsScene, args)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val context = context
|
||||
val activity = mainActivity
|
||||
@@ -858,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,
|
||||
@@ -872,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
|
||||
@@ -881,7 +912,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (!remove) {
|
||||
mModifyingFavorites = true
|
||||
CommonOperations.addToFavorites(
|
||||
activity, mGalleryDetail,
|
||||
activity, mGalleryDetail!!,
|
||||
ModifyFavoritesListener(context, false)
|
||||
)
|
||||
}
|
||||
@@ -961,12 +992,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
args.putParcelable(GalleryCommentsScene.KEY_COMMENT_LIST, mGalleryDetail!!.comments)
|
||||
args.putParcelable(GalleryCommentsScene.KEY_GALLERY_DETAIL, mGalleryDetail)
|
||||
navigate(R.id.galleryCommentsScene, args)
|
||||
} else if (binding.content.previews.previews === v) {
|
||||
if (null != mGalleryDetail) {
|
||||
val args = Bundle()
|
||||
args.putParcelable(GalleryPreviewsScene.KEY_GALLERY_INFO, mGalleryDetail)
|
||||
navigate(R.id.galleryPreviewsScene, args)
|
||||
}
|
||||
} else {
|
||||
var o = v.getTag(R.id.tag)
|
||||
if (o is String) {
|
||||
@@ -1053,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]) {
|
||||
@@ -1124,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
|
||||
@@ -1133,7 +1158,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (!remove) {
|
||||
mModifyingFavorites = true
|
||||
CommonOperations.addToFavorites(
|
||||
activity, mGalleryDetail,
|
||||
activity, mGalleryDetail!!,
|
||||
ModifyFavoritesListener(activity, false), true
|
||||
)
|
||||
}
|
||||
@@ -1277,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 =
|
||||
@@ -1432,12 +1457,12 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
.setCallback(this)
|
||||
mRequest!!.enqueue(this@GalleryDetailScene)
|
||||
} else {
|
||||
bind(mArchiveList)
|
||||
bind(mArchiveList, mCurrentFunds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bind(data: List<ArchiveParser.Archive>?) {
|
||||
private fun bind(data: List<ArchiveParser.Archive>?, funds: HomeParser.Funds?) {
|
||||
if (null == mDialog || null == mProgressView || null == mErrorText || null == mListView) {
|
||||
return
|
||||
}
|
||||
@@ -1447,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
|
||||
@@ -1457,6 +1492,14 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
mListView!!.visibility = View.VISIBLE
|
||||
mListView!!.adapter =
|
||||
ArrayAdapter(mDialog!!.context, R.layout.item_select_dialog, nameArray)
|
||||
if (funds != null) {
|
||||
var fundsGP = funds.fundsGP.toString()
|
||||
// Ex GP numbers are rounded down to the nearest thousand
|
||||
if (EhUtils.isExHentai) {
|
||||
fundsGP += "+"
|
||||
}
|
||||
mDialog!!.setTitle(getString(R.string.current_funds, fundsGP, funds.fundsC))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1464,7 +1507,19 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
val context = context
|
||||
val activity = mainActivity
|
||||
if (null != context && null != activity && null != mArchiveList && position < mArchiveList!!.size) {
|
||||
val res = mArchiveList!![position].res()
|
||||
if (mArchiveList!![position].name == "Insufficient Funds") {
|
||||
showTip(R.string.insufficient_funds, LENGTH_SHORT)
|
||||
return
|
||||
} else if (mCurrentFunds != null) {
|
||||
val cost = ParserUtils.parseInt(
|
||||
mArchiveList!![position].cost.removeSuffix("GP").removeSuffix("Credits"), 0
|
||||
)
|
||||
if (cost > maxOf(mCurrentFunds!!.fundsGP, mCurrentFunds!!.fundsC)) {
|
||||
showTip(R.string.insufficient_funds, LENGTH_SHORT)
|
||||
return
|
||||
}
|
||||
}
|
||||
val res = mArchiveList!![position].res
|
||||
val isHAtH = mArchiveList!![position].isHAtH
|
||||
val request = EhRequest()
|
||||
request.setMethod(EhClient.METHOD_DOWNLOAD_ARCHIVE)
|
||||
@@ -1498,9 +1553,10 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
override fun onSuccess(result: ArchiveParser.Result) {
|
||||
if (mRequest != null) {
|
||||
mRequest = null
|
||||
mArchiveFormParamOr = result.paramOr()
|
||||
mArchiveList = result.archiveList()
|
||||
bind(result.archiveList())
|
||||
mArchiveFormParamOr = result.paramOr
|
||||
mArchiveList = result.archiveList
|
||||
mCurrentFunds = result.funds
|
||||
bind(result.archiveList, result.funds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1583,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
|
||||
@@ -1696,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() {
|
||||
@@ -401,12 +401,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
val colorID = theme.resolveColor(com.google.android.material.R.attr.colorOnSurface)
|
||||
mActionFabDrawable = AddDeleteDrawable(context, colorID)
|
||||
binding.fabLayout.primaryFab!!.setImageDrawable(mActionFabDrawable)
|
||||
mSearchFab!!.setOnClickListener {
|
||||
if (STATE_NORMAL != mState) {
|
||||
onApplySearch()
|
||||
hideSoftInput()
|
||||
}
|
||||
}
|
||||
mSearchFab!!.setOnClickListener { onApplySearch() }
|
||||
|
||||
// Update list url builder
|
||||
onUpdateUrlBuilder()
|
||||
@@ -701,7 +696,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
|
||||
override fun onSearchViewExpanded() {
|
||||
super.onSearchViewExpanded()
|
||||
selectSearchFab(true)
|
||||
if (mState == STATE_NORMAL) selectSearchFab(true)
|
||||
}
|
||||
|
||||
override fun onSearchViewHidden() {
|
||||
@@ -1093,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)
|
||||
}
|
||||
|
||||
@@ -1106,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)
|
||||
}
|
||||
|
||||
@@ -1153,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)
|
||||
}
|
||||
@@ -1279,11 +1277,11 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
return if (null != mHelper) mHelper!!.size() else 0
|
||||
}
|
||||
|
||||
public override fun onItemClick(view: View, position: Int) {
|
||||
override fun onItemClick(view: View, position: Int) {
|
||||
this@GalleryListScene.onItemClick(position)
|
||||
}
|
||||
|
||||
public override fun onItemLongClick(view: View, position: Int): Boolean {
|
||||
override fun onItemLongClick(view: View, position: Int): Boolean {
|
||||
return this@GalleryListScene.onItemLongClick(position)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -59,6 +60,7 @@ class GalleryPreviewsScene : BaseToolbarScene() {
|
||||
private var mAdapter: GalleryPreviewAdapter? = null
|
||||
private var mHelper: GalleryPreviewHelper? = null
|
||||
private var mHasFirstRefresh = false
|
||||
private var mNextPage: Boolean = false
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (savedInstanceState == null) {
|
||||
@@ -70,11 +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)
|
||||
}
|
||||
|
||||
@@ -137,6 +140,12 @@ class GalleryPreviewsScene : BaseToolbarScene() {
|
||||
if (mGalleryInfo != null) {
|
||||
if ((mGalleryInfo as GalleryDetail).previewPages > 2) showMenu(R.menu.scene_gallery_previews)
|
||||
}
|
||||
runCatching {
|
||||
mHelper!!.mPages = (mGalleryInfo as GalleryDetail).previewPages
|
||||
if (mNextPage && mHelper!!.pages > 1) mHelper!!.goTo(1)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
@@ -344,6 +353,7 @@ class GalleryPreviewsScene : BaseToolbarScene() {
|
||||
|
||||
companion object {
|
||||
const val KEY_GALLERY_INFO = "gallery_info"
|
||||
const val KEY_NEXT_PAGE = "next_page"
|
||||
private const val KEY_HAS_FIRST_REFRESH = "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
|
||||
@@ -396,7 +395,9 @@ class HistoryScene : BaseToolbarScene() {
|
||||
override fun onBindViewHolder(holder: HistoryHolder, position: Int) {
|
||||
val gi: GalleryInfo? = getItem(position)
|
||||
gi ?: return
|
||||
gi.thumb?.let { holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), it) }
|
||||
gi.thumb?.let {
|
||||
holder.thumb.load(EhUtils.fixThumbUrl(it))
|
||||
}
|
||||
holder.title.text = EhUtils.getSuitableTitle(gi)
|
||||
holder.uploader.text = gi.uploader
|
||||
holder.rating.rating = gi.rating
|
||||
|
||||
@@ -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
|
||||
@@ -73,7 +74,7 @@ class ProgressScene : BaseScene(), View.OnClickListener {
|
||||
context
|
||||
)
|
||||
)
|
||||
request.enqueue(this)
|
||||
request.enqueue()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -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
|
||||
@@ -53,8 +52,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
updateSuggestions()
|
||||
}
|
||||
binding.searchview.editText.setOnEditorActionListener { _, _, _ ->
|
||||
binding.toolbar.text = binding.searchview.text
|
||||
binding.searchview.hide()
|
||||
onApplySearch()
|
||||
true
|
||||
}
|
||||
@@ -157,6 +154,8 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
}
|
||||
|
||||
fun onApplySearch() {
|
||||
binding.toolbar.text = binding.searchview.text
|
||||
binding.searchview.hide()
|
||||
val query = binding.toolbar.text.toString().trim()
|
||||
if (!mAllowEmptySearch && query.isEmpty()) {
|
||||
return
|
||||
@@ -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
|
||||
}
|
||||
Loaded 100 of 179 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user