Compare commits
362
Commits
1.8.5.3
...
1.8.7.0-alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42f36b8e74 | ||
|
|
9bc3c9a902 | ||
|
|
1dd04740bb | ||
|
|
35201e6615 | ||
|
|
86d416bccf | ||
|
|
979841665a | ||
|
|
c29b8c114b | ||
|
|
a03eae3c87 | ||
|
|
704c2a261e | ||
|
|
8fabdd1926 | ||
|
|
f9df373a16 | ||
|
|
f959f98012 | ||
|
|
e12a93f36b | ||
|
|
c3aca0a88d | ||
|
|
add7819945 | ||
|
|
3c6d3bbd80 | ||
|
|
66e1315b08 | ||
|
|
9bbe010fc9 | ||
|
|
4f81d164c8 | ||
|
|
c808e9135d | ||
|
|
1e7861ef15 | ||
|
|
a70b8c6dff | ||
|
|
ce86638e8e | ||
|
|
06ab978f53 | ||
|
|
9c2026982c | ||
|
|
863b910311 | ||
|
|
68da3b7580 | ||
|
|
43556f219b | ||
|
|
6022144dce | ||
|
|
18a0e3fd9e | ||
|
|
6c7c6e4b22 | ||
|
|
ffa7a74575 | ||
|
|
37f9c28133 | ||
|
|
ea7a012e0a | ||
|
|
0d594da6c9 | ||
|
|
ed588bf26c | ||
|
|
b6673fd828 | ||
|
|
060bcc76ef | ||
|
|
8fc8dd2960 | ||
|
|
1dc36275aa | ||
|
|
f305e68202 | ||
|
|
5c481df41f | ||
|
|
e86c626536 | ||
|
|
44559bab1c | ||
|
|
2ae28f1c63 | ||
|
|
b3e16d2b2a | ||
|
|
6f186080df | ||
|
|
c9d9335b71 | ||
|
|
22c8a36224 | ||
|
|
37f705d517 | ||
|
|
8b9985a7dc | ||
|
|
f51e585d8d | ||
|
|
a6c204e444 | ||
|
|
a0a215400e | ||
|
|
37e3166f98 | ||
|
|
ddccdc2ade | ||
|
|
755a15d5e5 | ||
|
|
5c6716bd98 | ||
|
|
7d87417095 | ||
|
|
77bb6e9a82 | ||
|
|
019174d7e2 | ||
|
|
cac88cbb81 | ||
|
|
25500da486 | ||
|
|
53f2c7126b | ||
|
|
eb95026a47 | ||
|
|
6b0f8bc325 | ||
|
|
a80f41a89a | ||
|
|
8690f8aa8d | ||
|
|
d9c55a284b | ||
|
|
c8a10a8b75 | ||
|
|
8e8dc76c3d | ||
|
|
725679f5d4 | ||
|
|
dfce07d7eb | ||
|
|
73c9e37eba | ||
|
|
ccd5bd8394 | ||
|
|
fd9b7db999 | ||
|
|
f3fe115733 | ||
|
|
16f3f74a90 | ||
|
|
4a6b85003a | ||
|
|
bf651c7e40 | ||
|
|
473ff4d308 | ||
|
|
5d64e8c61b | ||
|
|
b559bb3904 | ||
|
|
359f250b3b | ||
|
|
a20336a11a | ||
|
|
0313c5705f | ||
|
|
14469638b3 | ||
|
|
d15c5fc9d1 | ||
|
|
16e7d266fe | ||
|
|
b716a98548 | ||
|
|
39f40ed906 | ||
|
|
a644a1cee1 | ||
|
|
61fd8dc778 | ||
|
|
145d5e6302 | ||
|
|
ad071e48df | ||
|
|
28b5717f5b | ||
|
|
41b818caa6 | ||
|
|
cf87a654fb | ||
|
|
9ffab097bb | ||
|
|
bdc3c2b6b5 | ||
|
|
3f39662ba1 | ||
|
|
aa485547a6 | ||
|
|
747f23bfe1 | ||
|
|
d885841826 | ||
|
|
0c457ef61d | ||
|
|
9462f1455b | ||
|
|
c07325e71e | ||
|
|
5eda7b3fc3 | ||
|
|
6b0c208b75 | ||
|
|
b0c6a26bea | ||
|
|
67a0893a97 | ||
|
|
d7438a26fb | ||
|
|
ee7ab38028 | ||
|
|
8d999ea278 | ||
|
|
0851cf50a0 | ||
|
|
0b6676aaa3 | ||
|
|
c64167c76a | ||
|
|
72a6419aff | ||
|
|
64e26a6b8b | ||
|
|
23184df1b9 | ||
|
|
dd123d7b87 | ||
|
|
0e5d1e2325 | ||
|
|
4939ce37c3 | ||
|
|
6e7e668d39 | ||
|
|
2d086a5c6e | ||
|
|
f7e592eac0 | ||
|
|
c13abafb09 | ||
|
|
eeed0720f7 | ||
|
|
f099b74aa6 | ||
|
|
f7f54692a7 | ||
|
|
b97eff4284 | ||
|
|
f234dc61bc | ||
|
|
83b40d2ccb | ||
|
|
a8d3d71ea9 | ||
|
|
621772f359 | ||
|
|
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 |
No files matched your search
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
|
||||
|
||||
- name: Push Apks
|
||||
uses: cpina/github-action-push-to-another-repository@v1.5
|
||||
uses: Ehviewer-Overhauled/github-action-push-to-another-repository@main
|
||||
env:
|
||||
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||
with:
|
||||
@@ -68,5 +68,6 @@ jobs:
|
||||
user-name: 'github-actions[bot]'
|
||||
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||
target-branch: deploy
|
||||
create-target-branch: true
|
||||
commit-message: Add apks from $GITHUB_REF
|
||||
target-directory: fdroid/tmp-apks
|
||||
@@ -1,10 +1,10 @@
|
||||
<p align="right">
|
||||
<strong>English<strong>
|
||||
<strong>English</strong>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
|
||||
简体中文
|
||||
</a>
|
||||
<span> | <span>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
|
||||
正體中文
|
||||
</a>
|
||||
@@ -66,19 +66,13 @@ with Material Design 3 and Dynamic Color Support
|
||||
|
||||
# Download
|
||||
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Git it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
|
||||
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
|
||||
|
||||
# Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
# Localization
|
||||
|
||||
Thank you to each of the **Ehviewer Overhauled** translators, and if you would like to contribute, please submit a translation via [Weblate](https://hosted.weblate.org/engage/ehviewer-overhauled/).
|
||||
|
||||
[<img src="https://hosted.weblate.org/widgets/ehviewer-overhauled/-/horizontal-auto.svg" alt="" />](https://hosted.weblate.org/engage/ehviewer-overhauled/)
|
||||
|
||||
# Thanks
|
||||
|
||||
Here is the libraries
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-parcelize'
|
||||
id 'com.google.devtools.ksp'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "33.0.1"
|
||||
ndkVersion "25.1.8937393"
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable = true
|
||||
reset()
|
||||
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
|
||||
universalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
sign {
|
||||
storeFile file(signStoreFile)
|
||||
storePassword signStorePassword
|
||||
keyAlias signkeyAlias
|
||||
keyPassword signkeyPassword
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "moe.tarsin.ehviewer"
|
||||
minSdkVersion 28
|
||||
targetSdkVersion 33
|
||||
versionCode 180031
|
||||
versionName "1.8.5.3"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th', 'nb-rNO'
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path 'src/main/cpp/CMakeLists.txt'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_19
|
||||
targetCompatibility JavaVersion.VERSION_19
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '19'
|
||||
freeCompilerArgs += [
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive"
|
||||
]
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError true
|
||||
checkReleaseBuilds false
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += "/META-INF/CHANGES"
|
||||
excludes += "/META-INF/README.md"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo.includeInApk false
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.sign
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
compose true
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.0"
|
||||
}
|
||||
|
||||
namespace 'com.hippo.ehviewer'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation "androidx.activity:activity-compose:1.6.1"
|
||||
implementation "androidx.appcompat:appcompat:1.6.0"
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
implementation "androidx.browser:browser:1.4.0"
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
||||
implementation platform("androidx.compose:compose-bom:2023.01.00")
|
||||
implementation "androidx.compose.material:material-icons-extended"
|
||||
implementation "androidx.compose.material3:material3"
|
||||
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha05"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.5.1"
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||
def nav_version = "2.5.3"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-compose:$nav_version"
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.2.0-alpha03"
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/room
|
||||
def room_version = "2.5.0"
|
||||
ksp "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-paging:$room_version"
|
||||
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
implementation 'com.drakeet.drawer:drawer:1.0.3'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Dead Dependency
|
||||
implementation 'com.github.tachiyomiorg:DirectionalViewPager:1.0.0' // Dead Dependency
|
||||
// https://github.com/google/accompanist/releases
|
||||
implementation "com.google.accompanist:accompanist-themeadapter-material3:0.28.0"
|
||||
implementation 'com.google.android.material:material:1.8.0-rc01'
|
||||
|
||||
// https://square.github.io/okhttp/changelogs/changelog/
|
||||
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp-coroutines'
|
||||
|
||||
implementation 'com.squareup.okio:okio-jvm:3.3.0'
|
||||
|
||||
implementation 'dev.chrisbanes.insetter:insetter:0.6.1' // Dead Dependency
|
||||
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
|
||||
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
|
||||
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
|
||||
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
|
||||
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
|
||||
// https://coil-kt.github.io/coil/changelog/
|
||||
// implementation 'io.coil-kt:coil:2.2.2'
|
||||
implementation 'com.github.Ehviewer-Overhauled.coil:coil:d65881d'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
|
||||
exclude group: 'dev.rikka.rikkax.material', module: 'material'
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-android-extensions-runtime'
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "33.0.2"
|
||||
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
|
||||
}
|
||||
|
||||
val commitSha by lazy {
|
||||
val stdout = ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
||||
standardOutput = stdout
|
||||
}
|
||||
stdout.toString().trim()
|
||||
}
|
||||
|
||||
val buildTime by lazy {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
|
||||
formatter.format(Instant.now())
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
versionCode = 180034
|
||||
versionName = "1.8.7.0-alpha"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
"zh-rCN",
|
||||
"zh-rHK",
|
||||
"zh-rTW",
|
||||
"es",
|
||||
"ja",
|
||||
"ko",
|
||||
"fr",
|
||||
"de",
|
||||
"th",
|
||||
"tr",
|
||||
"nb-rNO"
|
||||
)
|
||||
)
|
||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
|
||||
}
|
||||
|
||||
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=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-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.3"
|
||||
}
|
||||
|
||||
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-beta01")
|
||||
|
||||
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("androidx.work:work-runtime-ktx:2.8.0")
|
||||
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-compose:2.2.2")
|
||||
|
||||
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.4")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<activity
|
||||
android:name=".ui.login.LoginActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
|
||||
@@ -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 natsort/strnatcmp.c)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
|
||||
+95
-33
@@ -17,14 +17,12 @@
|
||||
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/mman.h>
|
||||
#include <syscall.h>
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
@@ -34,6 +32,7 @@
|
||||
|
||||
#define LOG_TAG "libarchive_wrapper"
|
||||
|
||||
#include "natsort/strnatcmp.h"
|
||||
#include "ehviewer.h"
|
||||
|
||||
typedef struct {
|
||||
@@ -46,12 +45,25 @@ typedef struct {
|
||||
typedef struct {
|
||||
const char *filename;
|
||||
int index;
|
||||
size_t size;
|
||||
} entry;
|
||||
|
||||
pthread_mutex_t ctx_lock;
|
||||
static archive_ctx **ctx_pool;
|
||||
#define CTX_POOL_SIZE 20
|
||||
|
||||
static void *mempool = NULL;
|
||||
static size_t *mempoolofs = NULL;
|
||||
|
||||
#define PAGE_ALIGN(x) ((x + ~PAGE_MASK) & PAGE_MASK)
|
||||
|
||||
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
|
||||
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
|
||||
#define MEMPOOL_ENTITY_SIZE(x) PAGE_ALIGN(entries[x].size)
|
||||
|
||||
#define PROT_RW (PROT_WRITE | PROT_READ)
|
||||
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
|
||||
|
||||
static bool need_encrypt = false;
|
||||
static char *passwd = NULL;
|
||||
static void *archiveAddr = NULL;
|
||||
@@ -59,8 +71,6 @@ static size_t archiveSize = 0;
|
||||
static entry *entries = NULL;
|
||||
static size_t entryCount = 0;
|
||||
|
||||
#define POPULATE_THRESHOLD (512 * 1024 * 1024) // 512 MiB
|
||||
|
||||
const char supportExt[9][6] = {
|
||||
"jpeg",
|
||||
"jpg",
|
||||
@@ -87,9 +97,7 @@ static inline int filename_is_playable_file(const char *name) {
|
||||
static inline int compare_entries(const void *a, const void *b) {
|
||||
const char *fa = ((entry *) a)->filename;
|
||||
const char *fb = ((entry *) b)->filename;
|
||||
int a_len = (int) strlen(fa);
|
||||
int b_len = (int) strlen(fb);
|
||||
return a_len == b_len ? strcmp(fa, fb) : a_len - b_len;
|
||||
return strnatcmp(fa, fb);
|
||||
}
|
||||
|
||||
static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
@@ -99,6 +107,7 @@ static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
if (filename_is_playable_file(name)) {
|
||||
entries[count].filename = strdup(name);
|
||||
entries[count].index = count;
|
||||
entries[count].size = archive_entry_size(ctx->entry);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -108,6 +117,48 @@ static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
return count;
|
||||
}
|
||||
|
||||
static bool archive_prealloc_mempool() {
|
||||
mempoolofs = calloc(entryCount, sizeof(size_t));
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
if (!i)
|
||||
mempoolofs[i] = PAGE_ALIGN(entries[i].size);
|
||||
else
|
||||
mempoolofs[i] = PAGE_ALIGN(entries[i].size) + mempoolofs[i - 1];
|
||||
}
|
||||
mempool = mmap(0, MEMPOOL_SIZE, PROT_RW, MAP_ANON_POOL, -1, 0);
|
||||
if (mempool == MAP_FAILED) {
|
||||
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Mincore does not support anon mapping yet
|
||||
// Now that we use MADV_FREE, how can we detect whether pages is reclaimed?
|
||||
static bool is_mempool_pages_present_and_lock(int index) {
|
||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||
size_t size = MEMPOOL_ENTITY_SIZE(index);
|
||||
mlock(addr, size);
|
||||
#if 0
|
||||
unsigned char vec[size / PAGE_SIZE];
|
||||
unsigned char r = 1;
|
||||
mincore(addr, size, vec);
|
||||
for (int i = 0; i < size / PAGE_SIZE; ++i) r &= vec[i];
|
||||
return r;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#define MEMPOOL_IN_CORE_AND_LOCK(x) is_mempool_pages_present_and_lock(x)
|
||||
|
||||
static void mempool_release_pages(void *addr, size_t size) {
|
||||
size = PAGE_ALIGN(size);
|
||||
munlock(addr, size);
|
||||
int ret = madvise(addr, size, MADV_FREE);
|
||||
if (ret == -EINVAL) madvise(addr, size, MADV_DONTNEED);
|
||||
}
|
||||
|
||||
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
@@ -158,7 +209,6 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
|
||||
}
|
||||
|
||||
static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||
assert(!(ctx->next_index > index));
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
continue;
|
||||
@@ -233,8 +283,6 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
|
||||
EH_UNUSED(thiz);
|
||||
archive_ctx *ctx = NULL;
|
||||
int mmap_flags = MAP_PRIVATE;
|
||||
if (size <= POPULATE_THRESHOLD)
|
||||
mmap_flags |= MAP_POPULATE;
|
||||
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
|
||||
if (archiveAddr == MAP_FAILED) {
|
||||
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
|
||||
@@ -255,7 +303,7 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
entryCount = archive_list_all_entries(ctx);
|
||||
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
|
||||
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
|
||||
|
||||
// We must read through the file|vm then we can know whether it is encrypted
|
||||
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
||||
@@ -292,41 +340,52 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
|
||||
} else {
|
||||
entries = calloc(entryCount, sizeof(entry));
|
||||
r = archive_map_entries_index(ctx);
|
||||
if (!archive_prealloc_mempool()) {
|
||||
r = 0;
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
free((void *) entries[i].filename);
|
||||
}
|
||||
free(entries);
|
||||
}
|
||||
}
|
||||
|
||||
archive_release_ctx(ctx);
|
||||
return r;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "ConstantConditionsOC"
|
||||
#pragma ide diagnostic ignored "UnreachableCode"
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret)
|
||||
return 0;
|
||||
long long size = archive_entry_size(ctx->entry);
|
||||
void *buffer = malloc(size);
|
||||
if (!buffer) {
|
||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||
size_t size = entries[index].size;
|
||||
if (!MEMPOOL_IN_CORE_AND_LOCK(index)) {
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret) return 0;
|
||||
ret = archive_read_data(ctx->arc, addr, size);
|
||||
ctx->using = 0;
|
||||
LOGE("Allocate buffer for decompression failed:ENOMEM");
|
||||
return 0;
|
||||
if (ret == size)
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
if (ret != size)
|
||||
LOGE("%s", "No enough data read, WTF?");
|
||||
if (ret < 0)
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
mempool_release_pages(addr, size);
|
||||
} else {
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
}
|
||||
ret = archive_read_data(ctx->arc, buffer, size);
|
||||
ctx->using = 0;
|
||||
if (ret == size)
|
||||
return (*env)->NewDirectByteBuffer(env, buffer, size);
|
||||
if (ret != size)
|
||||
LOGE("%s", "No enough data read, WTF?");
|
||||
if (ret < 0)
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
free(buffer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
|
||||
EH_UNUSED(env);
|
||||
@@ -339,6 +398,8 @@ Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
|
||||
passwd = NULL;
|
||||
need_encrypt = false;
|
||||
munmap(archiveAddr, archiveSize);
|
||||
munmap(mempool, MEMPOOL_SIZE);
|
||||
free(mempoolofs);
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
free((void *) entries[i].filename);
|
||||
}
|
||||
@@ -415,5 +476,6 @@ JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
free(addr);
|
||||
}
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
mempool_release_pages(addr, size);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <jni.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sendfile.h>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <string.h>
|
||||
@@ -9,6 +11,8 @@
|
||||
|
||||
#include "ehviewer.h"
|
||||
|
||||
// TODO: Replace it with AFileDescriptor_getFd when minsdk 31
|
||||
// https://developer.android.com/ndk/reference/group/file-descriptor
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||
jint fd = -1;
|
||||
@@ -24,20 +28,9 @@ Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||
return fd;
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_Native_mapFd(JNIEnv *env, jclass clazz, jint fd, jlong capability) {
|
||||
void *addr = mmap(0, capability, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
|
||||
if (addr != MAP_FAILED) {
|
||||
return (*env)->NewDirectByteBuffer(env, addr, capability);
|
||||
} else {
|
||||
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_Native_unmapDirectByteBuffer(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
if (munmap(addr, size)) LOGE("munmap addr:%p, size%zu failed!", addr, size);
|
||||
Java_com_hippo_Native_sendfile(JNIEnv *env, jclass clazz, jint from, jint to) {
|
||||
struct stat st;
|
||||
fstat(from, &st);
|
||||
sendfile(to, from, 0, st.st_size);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/* -*- mode: c; c-file-style: "k&r" -*-
|
||||
|
||||
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
|
||||
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
|
||||
/* partial change history:
|
||||
*
|
||||
* 2004-10-10 mbp: Lift out character type dependencies into macros.
|
||||
*
|
||||
* Eric Sosman pointed out that ctype functions take a parameter whose
|
||||
* value must be that of an unsigned int, even on platforms that have
|
||||
* negative chars in their default char type.
|
||||
*/
|
||||
|
||||
#include <stddef.h> /* size_t */
|
||||
#include <ctype.h>
|
||||
|
||||
#include "strnatcmp.h"
|
||||
|
||||
|
||||
/* These are defined as macros to make it easier to adapt this code to
|
||||
* different characters types or comparison functions. */
|
||||
static inline int
|
||||
nat_isdigit(nat_char a)
|
||||
{
|
||||
return isdigit((unsigned char) a);
|
||||
}
|
||||
|
||||
|
||||
static inline int
|
||||
nat_isspace(nat_char a)
|
||||
{
|
||||
return isspace((unsigned char) a);
|
||||
}
|
||||
|
||||
|
||||
static inline nat_char
|
||||
nat_toupper(nat_char a)
|
||||
{
|
||||
return toupper((unsigned char) a);
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
compare_right(nat_char const *a, nat_char const *b)
|
||||
{
|
||||
int bias = 0;
|
||||
|
||||
/* The longest run of digits wins. That aside, the greatest
|
||||
value wins, but we can't know that it will until we've scanned
|
||||
both numbers to know that they have the same magnitude, so we
|
||||
remember it in BIAS. */
|
||||
for (;; a++, b++) {
|
||||
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||
return bias;
|
||||
if (!nat_isdigit(*a))
|
||||
return -1;
|
||||
if (!nat_isdigit(*b))
|
||||
return +1;
|
||||
if (*a < *b) {
|
||||
if (!bias)
|
||||
bias = -1;
|
||||
} else if (*a > *b) {
|
||||
if (!bias)
|
||||
bias = +1;
|
||||
} else if (!*a && !*b)
|
||||
return bias;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
compare_left(nat_char const *a, nat_char const *b)
|
||||
{
|
||||
/* Compare two left-aligned numbers: the first to have a
|
||||
different value wins. */
|
||||
for (;; a++, b++) {
|
||||
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||
return 0;
|
||||
if (!nat_isdigit(*a))
|
||||
return -1;
|
||||
if (!nat_isdigit(*b))
|
||||
return +1;
|
||||
if (*a < *b)
|
||||
return -1;
|
||||
if (*a > *b)
|
||||
return +1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
strnatcmp0(nat_char const *a, nat_char const *b, int fold_case)
|
||||
{
|
||||
int ai, bi;
|
||||
nat_char ca, cb;
|
||||
int fractional, result;
|
||||
|
||||
ai = bi = 0;
|
||||
while (1) {
|
||||
ca = a[ai]; cb = b[bi];
|
||||
|
||||
/* skip over leading spaces or zeros */
|
||||
while (nat_isspace(ca))
|
||||
ca = a[++ai];
|
||||
|
||||
while (nat_isspace(cb))
|
||||
cb = b[++bi];
|
||||
|
||||
/* process run of digits */
|
||||
if (nat_isdigit(ca) && nat_isdigit(cb)) {
|
||||
fractional = (ca == '0' || cb == '0');
|
||||
|
||||
if (fractional) {
|
||||
if ((result = compare_left(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
} else {
|
||||
if ((result = compare_right(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ca && !cb) {
|
||||
/* The strings compare the same. Perhaps the caller
|
||||
will want to call strcmp to break the tie. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fold_case) {
|
||||
ca = nat_toupper(ca);
|
||||
cb = nat_toupper(cb);
|
||||
}
|
||||
|
||||
if (ca < cb)
|
||||
return -1;
|
||||
|
||||
if (ca > cb)
|
||||
return +1;
|
||||
|
||||
++ai; ++bi;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
strnatcmp(nat_char const *a, nat_char const *b) {
|
||||
return strnatcmp0(a, b, 0);
|
||||
}
|
||||
|
||||
|
||||
/* Compare, recognizing numeric string and ignoring case. */
|
||||
int
|
||||
strnatcasecmp(nat_char const *a, nat_char const *b) {
|
||||
return strnatcmp0(a, b, 1);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* -*- mode: c; c-file-style: "k&r" -*-
|
||||
|
||||
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
|
||||
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* CUSTOMIZATION SECTION
|
||||
*
|
||||
* You can change this typedef, but must then also change the inline
|
||||
* functions in strnatcmp.c */
|
||||
typedef char nat_char;
|
||||
|
||||
int strnatcmp(nat_char const *a, nat_char const *b);
|
||||
int strnatcasecmp(nat_char const *a, nat_char const *b);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,17 +1,15 @@
|
||||
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 {
|
||||
if (navController.currentDestination?.matchDestination(item.itemId) == true) return false
|
||||
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
|
||||
val builder = NavOptions.Builder().setLaunchSingleTop(true)
|
||||
if (
|
||||
navController.currentDestination!!.parent!!.findNode(item.itemId)
|
||||
@@ -27,13 +25,6 @@ fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boo
|
||||
.setPopEnterAnim(com.hippo.ehviewer.R.anim.scene_close_enter)
|
||||
.setPopExitAnim(com.hippo.ehviewer.R.anim.scene_close_exit)
|
||||
}
|
||||
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
|
||||
builder.setPopUpTo(
|
||||
navController.graph.findStartDestination().id,
|
||||
inclusive = false,
|
||||
saveState = false
|
||||
)
|
||||
}
|
||||
val options = builder.build()
|
||||
return try {
|
||||
// TODO provide proper API instead of using Exceptions as Control-Flow.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
@file:JvmName("GifDecodeUtils")
|
||||
|
||||
package coil.decode
|
||||
|
||||
import kotlin.experimental.and
|
||||
import okio.BufferedSource
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
|
||||
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
private val GIF_HEADER_87A = "GIF87a".encodeUtf8()
|
||||
private val GIF_HEADER_89A = "GIF89a".encodeUtf8()
|
||||
|
||||
// https://developers.google.com/speed/webp/docs/riff_container
|
||||
private val WEBP_HEADER_RIFF = "RIFF".encodeUtf8()
|
||||
private val WEBP_HEADER_WEBP = "WEBP".encodeUtf8()
|
||||
private val WEBP_HEADER_VPX8 = "VP8X".encodeUtf8()
|
||||
|
||||
// https://nokiatech.github.io/heif/technical.html
|
||||
private val HEIF_HEADER_FTYP = "ftyp".encodeUtf8()
|
||||
private val HEIF_HEADER_MSF1 = "msf1".encodeUtf8()
|
||||
private val HEIF_HEADER_HEVC = "hevc".encodeUtf8()
|
||||
private val HEIF_HEADER_HEVX = "hevx".encodeUtf8()
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains a GIF image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isGif(source: BufferedSource): Boolean {
|
||||
return source.rangeEquals(0, GIF_HEADER_89A) ||
|
||||
source.rangeEquals(0, GIF_HEADER_87A)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains a WebP image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isWebP(source: BufferedSource): Boolean {
|
||||
return source.rangeEquals(0, WEBP_HEADER_RIFF) &&
|
||||
source.rangeEquals(8, WEBP_HEADER_WEBP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains an animated WebP image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isAnimatedWebP(source: BufferedSource): Boolean {
|
||||
return isWebP(source) &&
|
||||
source.rangeEquals(12, WEBP_HEADER_VPX8) &&
|
||||
source.request(17) &&
|
||||
(source.buffer[16] and 0b00000010) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains an HEIF image. The [source] is not consumed.
|
||||
*/
|
||||
fun DecodeUtils.isHeif(source: BufferedSource): Boolean {
|
||||
return source.rangeEquals(4, HEIF_HEADER_FTYP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 'true' if the [source] contains an animated HEIF image sequence. The [source] is not
|
||||
* consumed.
|
||||
*/
|
||||
fun DecodeUtils.isAnimatedHeif(source: BufferedSource): Boolean {
|
||||
return isHeif(source) &&
|
||||
(source.rangeEquals(8, HEIF_HEADER_MSF1) ||
|
||||
source.rangeEquals(8, HEIF_HEADER_HEVC) ||
|
||||
source.rangeEquals(8, HEIF_HEADER_HEVX))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@file:Suppress("SameParameterValue")
|
||||
|
||||
package coil.decode
|
||||
|
||||
import okio.Buffer
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
|
||||
/**
|
||||
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
|
||||
* below a threshold.
|
||||
*/
|
||||
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
|
||||
|
||||
// An intermediary buffer so we can read and alter the data before it's written to the destination.
|
||||
private val buffer = Buffer()
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
// Ensure our buffer has enough bytes to satisfy this read.
|
||||
request(byteCount)
|
||||
|
||||
// Short circuit if there are no bytes in the buffer.
|
||||
if (buffer.size == 0L) {
|
||||
return if (byteCount == 0L) 0L else -1L
|
||||
}
|
||||
|
||||
// Search through the buffer and rewrite any frame delays below the threshold.
|
||||
var bytesWritten = 0L
|
||||
while (true) {
|
||||
val index = indexOf(FRAME_DELAY_START_MARKER)
|
||||
if (index == -1L) break
|
||||
|
||||
// Write up until the end of the frame delay start marker.
|
||||
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
|
||||
|
||||
// Check for the end of the graphics control extension block.
|
||||
if (!request(5) || buffer[4] != 0.toByte()) continue
|
||||
|
||||
// Rewrite the frame delay if it is below the threshold.
|
||||
// The frame delay is stored as two unsigned bytes in reverse order
|
||||
// (i.e. the most significant bits are in the byte that's read second).
|
||||
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
|
||||
if (frameDelay < MINIMUM_FRAME_DELAY) {
|
||||
sink.writeByte(buffer[0].toInt())
|
||||
sink.writeByte(DEFAULT_FRAME_DELAY)
|
||||
sink.writeByte(0)
|
||||
buffer.skip(3)
|
||||
}
|
||||
}
|
||||
|
||||
// Write anything left in the source.
|
||||
if (bytesWritten < byteCount) {
|
||||
bytesWritten += write(sink, byteCount - bytesWritten)
|
||||
}
|
||||
return if (bytesWritten == 0L) -1 else bytesWritten
|
||||
}
|
||||
|
||||
private fun indexOf(bytes: ByteString): Long {
|
||||
var index = -1L
|
||||
while (true) {
|
||||
index = buffer.indexOf(bytes[0], index + 1)
|
||||
if (index == -1L) break
|
||||
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
private fun write(sink: Buffer, byteCount: Long): Long {
|
||||
return buffer.read(sink, byteCount).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun request(byteCount: Long): Boolean {
|
||||
if (buffer.size >= byteCount) return true
|
||||
val toRead = byteCount - buffer.size
|
||||
return super.read(buffer, toRead) == toRead
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||
// See: "Graphics Control Extension"
|
||||
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
|
||||
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
|
||||
private const val MINIMUM_FRAME_DELAY = 2
|
||||
private const val DEFAULT_FRAME_DELAY = 10
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,8 @@
|
||||
package com.davemorrissey.labs.subscaleview
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import com.davemorrissey.labs.subscaleview.provider.AssetInputProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.OpenStreamProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.ResourceInputProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.UriInputProvider
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Helper class used to set the source and additional attributes from a variety of sources. Supports
|
||||
@@ -83,38 +76,6 @@ class ImageSource {
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create an instance from a resource. The correct resource for the device screen resolution will be used.
|
||||
*
|
||||
* @param resId resource ID.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun resource(context: Context, resId: Int): ImageSource {
|
||||
return ImageSource(ResourceInputProvider(context, resId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an asset name.
|
||||
*
|
||||
* @param assetName asset name.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun asset(context: Context, assetName: String): ImageSource {
|
||||
return ImageSource(AssetInputProvider(context, assetName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from a URI.
|
||||
*
|
||||
* @param uri image URI.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun uri(context: Context, uri: Uri): ImageSource {
|
||||
return ImageSource(UriInputProvider(context, uri))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an input provider.
|
||||
*
|
||||
@@ -125,16 +86,6 @@ class ImageSource {
|
||||
return ImageSource(provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an input stream.
|
||||
*
|
||||
* @param stream open input stream.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun inputStream(stream: InputStream): ImageSource {
|
||||
return ImageSource(OpenStreamProvider(stream))
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a loaded bitmap for display.
|
||||
*
|
||||
|
||||
@@ -19,12 +19,9 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.IntDef
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.asset
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.resource
|
||||
import com.davemorrissey.labs.subscaleview.decoder.Decoder
|
||||
import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
import com.hippo.ehviewer.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -303,42 +300,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
setMinimumDpi(160)
|
||||
setDoubleTapZoomDpi(160)
|
||||
setGestureDetector(context)
|
||||
|
||||
// Handle XML attributes
|
||||
if (attr != null) {
|
||||
val typedAttr = getContext().obtainStyledAttributes(attr, R.styleable.SubsamplingScaleImageView)
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_assetName)) {
|
||||
val assetName = typedAttr.getString(R.styleable.SubsamplingScaleImageView_assetName)
|
||||
if (assetName != null && assetName.isNotEmpty()) {
|
||||
setImage(asset(context, assetName))
|
||||
}
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_src)) {
|
||||
val resId = typedAttr.getResourceId(R.styleable.SubsamplingScaleImageView_src, 0)
|
||||
if (resId > 0) {
|
||||
setImage(resource(context, resId))
|
||||
}
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_panEnabled)) {
|
||||
setPanEnabled(typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_panEnabled, true))
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_zoomEnabled)) {
|
||||
isZoomEnabled = typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_zoomEnabled, true)
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_quickScaleEnabled)) {
|
||||
isQuickScaleEnabled =
|
||||
typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_quickScaleEnabled, true)
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_tileBackgroundColor)) {
|
||||
setTileBackgroundColor(
|
||||
typedAttr.getColor(
|
||||
R.styleable.SubsamplingScaleImageView_tileBackgroundColor,
|
||||
Color.argb(0, 0, 0, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
typedAttr.recycle()
|
||||
}
|
||||
quickScaleThreshold =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
|
||||
}
|
||||
@@ -1953,20 +1914,20 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
val scaleHeight = scale * sHeight
|
||||
when (panLimit) {
|
||||
PAN_LIMIT_CENTER -> {
|
||||
vTarget.top = -(vTranslate!!.y - height / 2).coerceAtLeast(0F)
|
||||
vTarget.left = -(vTranslate!!.x - width / 2).coerceAtLeast(0F)
|
||||
vTarget.top = (-(vTranslate!!.y - height / 2)).coerceAtLeast(0F)
|
||||
vTarget.left = (-(vTranslate!!.x - width / 2)).coerceAtLeast(0F)
|
||||
vTarget.bottom = (vTranslate!!.y - (height / 2 - scaleHeight)).coerceAtLeast(0F)
|
||||
vTarget.right = (vTranslate!!.x - (width / 2 - scaleWidth)).coerceAtLeast(0F)
|
||||
}
|
||||
PAN_LIMIT_OUTSIDE -> {
|
||||
vTarget.top = -(vTranslate!!.y - height).coerceAtLeast(0F)
|
||||
vTarget.left = -(vTranslate!!.x - width).coerceAtLeast(0F)
|
||||
vTarget.top = (-(vTranslate!!.y - height)).coerceAtLeast(0F)
|
||||
vTarget.left = (-(vTranslate!!.x - width)).coerceAtLeast(0F)
|
||||
vTarget.bottom = (vTranslate!!.y + scaleHeight).coerceAtLeast(0F)
|
||||
vTarget.right = (vTranslate!!.x + scaleWidth).coerceAtLeast(0F)
|
||||
}
|
||||
else -> {
|
||||
vTarget.top = -vTranslate!!.y.coerceAtLeast(0F)
|
||||
vTarget.left = -vTranslate!!.x.coerceAtLeast(0F)
|
||||
vTarget.top = (-vTranslate!!.y).coerceAtLeast(0F)
|
||||
vTarget.left = (-vTranslate!!.x).coerceAtLeast(0F)
|
||||
vTarget.bottom = (scaleHeight + vTranslate!!.y - height).coerceAtLeast(0F)
|
||||
vTarget.right = (scaleWidth + vTranslate!!.x - width).coerceAtLeast(0F)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import kotlin.Throws
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import java.lang.Exception
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
|
||||
class ImageDecoder @JvmOverloads constructor(
|
||||
private val cropBorders: Boolean,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import android.content.Context
|
||||
import java.io.IOException
|
||||
|
||||
class AssetInputProvider(context: Context, val assetName: String) : InputProvider {
|
||||
private val assets = context.assets
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = assets.open(assetName)
|
||||
}
|
||||
@@ -1,10 +1,3 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.Throws
|
||||
|
||||
interface InputProvider {
|
||||
@Throws(IOException::class)
|
||||
fun openStream(): InputStream?
|
||||
}
|
||||
interface InputProvider
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.Throws
|
||||
|
||||
/**
|
||||
* Provider from an already open input stream.
|
||||
*
|
||||
*
|
||||
* Please keep in mind this provider is not reusable.
|
||||
*/
|
||||
class OpenStreamProvider(private val stream: InputStream) : InputProvider {
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = stream
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import android.content.Context
|
||||
import java.io.IOException
|
||||
|
||||
class ResourceInputProvider(context: Context, val resource: Int) : InputProvider {
|
||||
private val resources = context.resources
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = resources.openRawResource(resource)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import java.io.IOException
|
||||
import kotlin.Throws
|
||||
|
||||
class UriInputProvider(context: Context, val uri: Uri) : InputProvider {
|
||||
private val resolver = context.contentResolver
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = resolver.openInputStream(uri)
|
||||
}
|
||||
@@ -17,8 +17,8 @@
|
||||
*/
|
||||
package com.hippo
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import java.io.FileDescriptor
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object Native {
|
||||
fun initialize() {
|
||||
@@ -29,8 +29,20 @@ object Native {
|
||||
external fun getFd(fd: FileDescriptor?): Int
|
||||
|
||||
@JvmStatic
|
||||
external fun mapFd(fd: Int, capability: Long): ByteBuffer?
|
||||
external fun sendfile(from: Int, to: Int)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
external fun unmapDirectByteBuffer(buffer: ByteBuffer)
|
||||
val FileDescriptor.fd
|
||||
get() = Native.getFd(this)
|
||||
|
||||
infix fun ParcelFileDescriptor.receivefrom(fd: FileDescriptor) {
|
||||
Native.sendfile(fd.fd, getFd())
|
||||
}
|
||||
|
||||
infix fun ParcelFileDescriptor.copyTo(fd: FileDescriptor) {
|
||||
Native.sendfile(getFd(), fd.fd)
|
||||
}
|
||||
|
||||
infix fun ParcelFileDescriptor.copyTo(fd: ParcelFileDescriptor) {
|
||||
Native.sendfile(getFd(), fd.fd)
|
||||
}
|
||||
@@ -18,13 +18,16 @@
|
||||
package com.hippo
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.hippo.image.Image
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
class UriArchiveAccessor(ctx: Context, private val uri: Uri) {
|
||||
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
|
||||
fun open(): Int {
|
||||
Log.d(DEBUG_TAG, "Open archive $uri")
|
||||
return openArchive(pfd.fd, pfd.statSize)
|
||||
}
|
||||
|
||||
@@ -38,16 +41,18 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
fun close() {
|
||||
closeArchive()
|
||||
pfd.close()
|
||||
Log.d(DEBUG_TAG, "Close archive $uri successfully!")
|
||||
}
|
||||
|
||||
fun getImageSource(index: Int): Image.ByteBufferSource? {
|
||||
fun getImageSource(index: Int): Image.CloseableSource? {
|
||||
val buffer = extractToByteBuffer(index)
|
||||
buffer ?: return null
|
||||
check(buffer.isDirect)
|
||||
return object : Image.ByteBufferSource {
|
||||
override fun getByteBuffer(): ByteBuffer {
|
||||
return buffer
|
||||
}
|
||||
Image.rewriteGifSource(buffer)
|
||||
val source = ImageDecoder.createSource(buffer)
|
||||
return object : Image.CloseableSource {
|
||||
override val source: ImageDecoder.Source
|
||||
get() = source
|
||||
|
||||
override fun close() {
|
||||
releaseByteBuffer(buffer)
|
||||
@@ -59,4 +64,6 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
@JvmStatic
|
||||
private external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEBUG_TAG = "UriArchiveAccessor"
|
||||
@@ -26,9 +26,12 @@ import androidx.appcompat.app.AlertDialog
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
|
||||
class ListCheckBoxDialogBuilder constructor(
|
||||
context: Context, items: Array<CharSequence>,
|
||||
listener: OnItemClickListener?, checkText: String?, checked: Boolean
|
||||
class ListCheckBoxDialogBuilder(
|
||||
context: Context,
|
||||
items: List<CharSequence>,
|
||||
listener: (ListCheckBoxDialogBuilder?, AlertDialog?, Int) -> Unit,
|
||||
checkText: String?,
|
||||
checked: Boolean
|
||||
) : BaseDialogBuilder(
|
||||
context
|
||||
) {
|
||||
@@ -42,10 +45,6 @@ class ListCheckBoxDialogBuilder constructor(
|
||||
return mDialog as AlertDialog
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(builder: ListCheckBoxDialogBuilder?, dialog: AlertDialog?, position: Int)
|
||||
}
|
||||
|
||||
init {
|
||||
val view =
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null)
|
||||
@@ -55,7 +54,7 @@ class ListCheckBoxDialogBuilder constructor(
|
||||
listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)
|
||||
listView.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
listener?.onItemClick(this@ListCheckBoxDialogBuilder, mDialog, position)
|
||||
listener(this@ListCheckBoxDialogBuilder, mDialog, position)
|
||||
mDialog?.dismiss()
|
||||
}
|
||||
mCheckBox.text = checkText
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.hippo.compat
|
||||
|
||||
import com.hippo.ehviewer.spider.SpiderInfo
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import java.io.InputStream
|
||||
|
||||
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
||||
fun read(): String {
|
||||
return IOUtils.readAsciiLine(inputStream)
|
||||
}
|
||||
|
||||
fun readInt(): Int {
|
||||
return read().toInt()
|
||||
}
|
||||
|
||||
fun readLong(): Long {
|
||||
return read().toLong()
|
||||
}
|
||||
repeat(2) { read() } // We assert that only info v2
|
||||
val gid = readLong()
|
||||
val token = read()
|
||||
read()
|
||||
val previewPages = readInt()
|
||||
val previewPerPage = readInt()
|
||||
val pages = read().toInt()
|
||||
val pTokenMap = hashMapOf<Int, String>()
|
||||
val info = SpiderInfo(gid, pages, pTokenMap, 0, token, previewPages, previewPerPage)
|
||||
runCatching {
|
||||
val line = read()
|
||||
val pos = line.indexOf(" ")
|
||||
if (pos > 0) {
|
||||
val index = line.substring(0, pos).toInt()
|
||||
val pToken = line.substring(pos + 1)
|
||||
if (pToken.isNotEmpty()) {
|
||||
pTokenMap[index] = pToken
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.drawable;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
public class DrawableWrapper extends Drawable implements Drawable.Callback {
|
||||
private Drawable mDrawable;
|
||||
private Rect mBounds;
|
||||
|
||||
public DrawableWrapper(Drawable drawable) {
|
||||
this.setWrappedDrawable(drawable);
|
||||
}
|
||||
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
if (mDrawable instanceof BitmapDrawable bitmapDrawable) {
|
||||
canvas.drawBitmap(bitmapDrawable.getBitmap(), null, mBounds, bitmapDrawable.getPaint());
|
||||
} else {
|
||||
synchronized (mDrawable) {
|
||||
mDrawable.setBounds(mBounds);
|
||||
mDrawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
mBounds = bounds;
|
||||
}
|
||||
|
||||
public int getChangingConfigurations() {
|
||||
return this.mDrawable.getChangingConfigurations();
|
||||
}
|
||||
|
||||
public void setChangingConfigurations(int configs) {
|
||||
this.mDrawable.setChangingConfigurations(configs);
|
||||
}
|
||||
|
||||
public void setDither(boolean dither) {
|
||||
this.mDrawable.setDither(dither);
|
||||
}
|
||||
|
||||
public void setFilterBitmap(boolean filter) {
|
||||
this.mDrawable.setFilterBitmap(filter);
|
||||
}
|
||||
|
||||
public void setAlpha(int alpha) {
|
||||
this.mDrawable.setAlpha(alpha);
|
||||
}
|
||||
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
this.mDrawable.setColorFilter(cf);
|
||||
}
|
||||
|
||||
public boolean isStateful() {
|
||||
return this.mDrawable.isStateful();
|
||||
}
|
||||
|
||||
public boolean setState(@NonNull int[] stateSet) {
|
||||
return this.mDrawable.setState(stateSet);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public int[] getState() {
|
||||
return this.mDrawable.getState();
|
||||
}
|
||||
|
||||
public void jumpToCurrentState() {
|
||||
this.mDrawable.jumpToCurrentState();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Drawable getCurrent() {
|
||||
return this.mDrawable.getCurrent();
|
||||
}
|
||||
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
return super.setVisible(visible, restart) || this.mDrawable.setVisible(visible, restart);
|
||||
}
|
||||
|
||||
public int getOpacity() {
|
||||
return this.mDrawable.getOpacity();
|
||||
}
|
||||
|
||||
public Region getTransparentRegion() {
|
||||
return this.mDrawable.getTransparentRegion();
|
||||
}
|
||||
|
||||
public int getIntrinsicWidth() {
|
||||
return this.mDrawable.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
public int getIntrinsicHeight() {
|
||||
return this.mDrawable.getIntrinsicHeight();
|
||||
}
|
||||
|
||||
public int getMinimumWidth() {
|
||||
return this.mDrawable.getMinimumWidth();
|
||||
}
|
||||
|
||||
public int getMinimumHeight() {
|
||||
return this.mDrawable.getMinimumHeight();
|
||||
}
|
||||
|
||||
public boolean getPadding(@NonNull Rect padding) {
|
||||
return this.mDrawable.getPadding(padding);
|
||||
}
|
||||
|
||||
public void invalidateDrawable(@NonNull Drawable who) {
|
||||
this.invalidateSelf();
|
||||
}
|
||||
|
||||
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
|
||||
this.scheduleSelf(what, when);
|
||||
}
|
||||
|
||||
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
|
||||
this.unscheduleSelf(what);
|
||||
}
|
||||
|
||||
protected boolean onLevelChange(int level) {
|
||||
return this.mDrawable.setLevel(level);
|
||||
}
|
||||
|
||||
public boolean isAutoMirrored() {
|
||||
return DrawableCompat.isAutoMirrored(this.mDrawable);
|
||||
}
|
||||
|
||||
public void setAutoMirrored(boolean mirrored) {
|
||||
DrawableCompat.setAutoMirrored(this.mDrawable, mirrored);
|
||||
}
|
||||
|
||||
public void setTint(int tint) {
|
||||
DrawableCompat.setTint(this.mDrawable, tint);
|
||||
}
|
||||
|
||||
public void setTintList(ColorStateList tint) {
|
||||
DrawableCompat.setTintList(this.mDrawable, tint);
|
||||
}
|
||||
|
||||
public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
|
||||
DrawableCompat.setTintMode(this.mDrawable, tintMode);
|
||||
}
|
||||
|
||||
public void setHotspot(float x, float y) {
|
||||
DrawableCompat.setHotspot(this.mDrawable, x, y);
|
||||
}
|
||||
|
||||
public void setHotspotBounds(int left, int top, int right, int bottom) {
|
||||
DrawableCompat.setHotspotBounds(this.mDrawable, left, top, right, bottom);
|
||||
}
|
||||
|
||||
public Drawable getWrappedDrawable() {
|
||||
return this.mDrawable;
|
||||
}
|
||||
|
||||
public void setWrappedDrawable(Drawable drawable) {
|
||||
if (this.mDrawable != null) {
|
||||
this.mDrawable.setCallback(null);
|
||||
}
|
||||
|
||||
this.mDrawable = drawable;
|
||||
if (drawable != null) {
|
||||
drawable.setCallback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +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.drawable;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
/**
|
||||
* Show a part of the original drawable
|
||||
*/
|
||||
public class PreciselyClipDrawable extends DrawableWrapper {
|
||||
|
||||
private final boolean mClip;
|
||||
private RectF mScale;
|
||||
private Rect mTemp;
|
||||
|
||||
public PreciselyClipDrawable(Drawable drawable, int offsetX, int offsetY, int width, int height) {
|
||||
super(drawable);
|
||||
float originWidth = drawable.getIntrinsicWidth();
|
||||
float originHeight = drawable.getIntrinsicHeight();
|
||||
|
||||
if (originWidth <= 0 || originHeight <= 0) {
|
||||
// Can not clip
|
||||
mClip = false;
|
||||
} else {
|
||||
mClip = true;
|
||||
mScale = new RectF();
|
||||
mScale.set(MathUtils.clamp(offsetX / originWidth, 0.0f, 1.0f),
|
||||
MathUtils.clamp(offsetY / originHeight, 0.0f, 1.0f),
|
||||
MathUtils.clamp((offsetX + width) / originWidth, 0.0f, 1.0f),
|
||||
MathUtils.clamp((offsetY + height) / originHeight, 0.0f, 1.0f));
|
||||
mTemp = new Rect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
if (mClip) {
|
||||
if (!mScale.isEmpty()) {
|
||||
mTemp.left = (int) ((mScale.left * bounds.right - mScale.right * bounds.left) /
|
||||
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
|
||||
mTemp.right = (int) (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) /
|
||||
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
|
||||
mTemp.top = (int) ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) /
|
||||
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
|
||||
mTemp.bottom = (int) (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) /
|
||||
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
|
||||
super.onBoundsChange(mTemp);
|
||||
}
|
||||
} else {
|
||||
super.onBoundsChange(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
if (mClip) {
|
||||
return (int) (super.getIntrinsicWidth() * mScale.width());
|
||||
} else {
|
||||
return super.getIntrinsicWidth();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
if (mClip) {
|
||||
return (int) (super.getIntrinsicHeight() * mScale.height());
|
||||
} else {
|
||||
return super.getIntrinsicHeight();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (mClip) {
|
||||
if (!mScale.isEmpty()) {
|
||||
int saveCount = canvas.save();
|
||||
canvas.clipRect(getBounds());
|
||||
super.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
} else {
|
||||
super.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.drawable
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.DrawableWrapper
|
||||
import androidx.core.graphics.withClip
|
||||
|
||||
/**
|
||||
* Show a part of the original drawable
|
||||
*/
|
||||
class PreciselyClipDrawable(
|
||||
drawable: Drawable,
|
||||
offsetX: Int,
|
||||
offsetY: Int,
|
||||
width: Int,
|
||||
height: Int
|
||||
) : DrawableWrapper(drawable) {
|
||||
private val mScale: RectF
|
||||
private val mTemp = Rect()
|
||||
|
||||
init {
|
||||
val originWidth = drawable.intrinsicWidth.toFloat()
|
||||
val originHeight = drawable.intrinsicHeight.toFloat()
|
||||
mScale = RectF(
|
||||
(offsetX / originWidth).coerceIn(0.0f, 1.0f),
|
||||
(offsetY / originHeight).coerceIn(0.0f, 1.0f),
|
||||
((offsetX + width) / originWidth).coerceIn(0.0f, 1.0f),
|
||||
((offsetY + height) / originHeight).coerceIn(0.0f, 1.0f),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
mTemp.left = ((mScale.left * bounds.right - mScale.right * bounds.left) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
|
||||
mTemp.right = (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
|
||||
mTemp.top = ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
|
||||
mTemp.bottom = (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
|
||||
super.onBoundsChange(mTemp)
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int {
|
||||
return (super.getIntrinsicWidth() * mScale.width()).toInt()
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int {
|
||||
return (super.getIntrinsicHeight() * mScale.height()).toInt()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.withClip(bounds) { super.draw(canvas) }
|
||||
}
|
||||
}
|
||||
@@ -1,202 +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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Debug;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.util.PackageUtils;
|
||||
import com.hippo.util.ReadableTime;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.OSUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class Crash {
|
||||
private Crash() {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String avoidNull(String str) {
|
||||
return null != str ? str : "null";
|
||||
}
|
||||
|
||||
private static void collectInfo(Context context, FileWriter fw) throws IOException {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
|
||||
if (pi != null) {
|
||||
String versionName = pi.versionName == null ? "null" : pi.versionName;
|
||||
String versionCode = String.valueOf(pi.versionCode);
|
||||
fw.write("======== PackageInfo ========\r\n");
|
||||
fw.write("PackageName=");
|
||||
fw.write(pi.packageName);
|
||||
fw.write("\r\n");
|
||||
fw.write("VersionName=");
|
||||
fw.write(versionName);
|
||||
fw.write("\r\n");
|
||||
fw.write("VersionCode=");
|
||||
fw.write(versionCode);
|
||||
fw.write("\r\n");
|
||||
String signature = PackageUtils.getSignature(context, pi.packageName);
|
||||
fw.write("Signature=");
|
||||
fw.write(null != signature ? signature : "null");
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
fw.write("======== PackageInfo ========\r\n");
|
||||
fw.write("Can't get package information\r\n");
|
||||
fw.write("\r\n");
|
||||
}
|
||||
|
||||
// Runtime
|
||||
String topActivityClazzName = "null";
|
||||
fw.write("======== Runtime ========\r\n");
|
||||
fw.write("TopActivity=");
|
||||
fw.write(avoidNull(topActivityClazzName));
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
|
||||
// Device info
|
||||
fw.write("======== DeviceInfo ========\r\n");
|
||||
fw.write("BOARD=");
|
||||
fw.write(Build.BOARD);
|
||||
fw.write("\r\n");
|
||||
fw.write("BOOTLOADER=");
|
||||
fw.write(Build.BOOTLOADER);
|
||||
fw.write("\r\n");
|
||||
fw.write("SUPPORTED_ABIS=");
|
||||
fw.write(Arrays.toString(Build.SUPPORTED_ABIS));
|
||||
fw.write("\r\n");
|
||||
fw.write("DEVICE=");
|
||||
fw.write(Build.DEVICE);
|
||||
fw.write("\r\n");
|
||||
fw.write("DISPLAY=");
|
||||
fw.write(Build.DISPLAY);
|
||||
fw.write("\r\n");
|
||||
fw.write("FINGERPRINT=");
|
||||
fw.write(Build.FINGERPRINT);
|
||||
fw.write("\r\n");
|
||||
fw.write("HARDWARE=");
|
||||
fw.write(Build.HARDWARE);
|
||||
fw.write("\r\n");
|
||||
fw.write("HOST=");
|
||||
fw.write(Build.HOST);
|
||||
fw.write("\r\n");
|
||||
fw.write("ID=");
|
||||
fw.write(Build.ID);
|
||||
fw.write("\r\n");
|
||||
fw.write("MANUFACTURER=");
|
||||
fw.write(Build.MANUFACTURER);
|
||||
fw.write("\r\n");
|
||||
fw.write("MODEL=");
|
||||
fw.write(Build.MODEL);
|
||||
fw.write("\r\n");
|
||||
fw.write("PRODUCT=");
|
||||
fw.write(Build.PRODUCT);
|
||||
fw.write("\r\n");
|
||||
fw.write("RADIO=");
|
||||
fw.write(Build.getRadioVersion());
|
||||
fw.write("\r\n");
|
||||
fw.write("TAGS=");
|
||||
fw.write(Build.TAGS);
|
||||
fw.write("\r\n");
|
||||
fw.write("TYPE=");
|
||||
fw.write(Build.TYPE);
|
||||
fw.write("\r\n");
|
||||
fw.write("USER=");
|
||||
fw.write(Build.USER);
|
||||
fw.write("\r\n");
|
||||
fw.write("CODENAME=");
|
||||
fw.write(Build.VERSION.CODENAME);
|
||||
fw.write("\r\n");
|
||||
fw.write("INCREMENTAL=");
|
||||
fw.write(Build.VERSION.INCREMENTAL);
|
||||
fw.write("\r\n");
|
||||
fw.write("RELEASE=");
|
||||
fw.write(Build.VERSION.RELEASE);
|
||||
fw.write("\r\n");
|
||||
fw.write("SDK=");
|
||||
fw.write(Integer.toString(Build.VERSION.SDK_INT));
|
||||
fw.write("\r\n");
|
||||
fw.write("MEMORY=");
|
||||
fw.write(
|
||||
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false));
|
||||
fw.write("\r\n");
|
||||
fw.write("MEMORY_NATIVE=");
|
||||
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));
|
||||
fw.write("\r\n");
|
||||
fw.write("MEMORY_MAX=");
|
||||
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false));
|
||||
fw.write("\r\n");
|
||||
fw.write("MEMORY_TOTAL=");
|
||||
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false));
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
}
|
||||
|
||||
private static void getThrowableInfo(Throwable t, FileWriter fw) {
|
||||
PrintWriter printWriter = new PrintWriter(fw);
|
||||
t.printStackTrace(printWriter);
|
||||
Throwable cause = t.getCause();
|
||||
while (cause != null) {
|
||||
cause.printStackTrace(printWriter);
|
||||
cause = cause.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveCrashLog(Context context, Throwable t) {
|
||||
File dir = AppConfig.getExternalCrashDir();
|
||||
if (dir == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis());
|
||||
String fileName = "crash-" + nowString + ".log";
|
||||
File file = new File(dir, fileName);
|
||||
|
||||
FileWriter fw = null;
|
||||
try {
|
||||
fw = new FileWriter(file);
|
||||
fw.write("TIME=");
|
||||
fw.write(nowString);
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
collectInfo(context, fw);
|
||||
fw.write("======== CrashInfo ========\r\n");
|
||||
getThrowableInfo(t, fw);
|
||||
fw.write("\r\n");
|
||||
fw.flush();
|
||||
} catch (Exception e) {
|
||||
file.delete();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(fw);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Debug
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import com.hippo.yorozuya.OSUtils
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.io.PrintWriter
|
||||
import java.util.Arrays
|
||||
|
||||
object Crash {
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun collectInfo(fw: FileWriter) {
|
||||
fw.write("======== PackageInfo ========\r\n")
|
||||
fw.write("PackageName=")
|
||||
fw.write(BuildConfig.APPLICATION_ID)
|
||||
fw.write("\r\n")
|
||||
fw.write("VersionName=")
|
||||
fw.write(BuildConfig.VERSION_NAME)
|
||||
fw.write("\r\n")
|
||||
fw.write("VersionCode=")
|
||||
fw.write(BuildConfig.VERSION_CODE)
|
||||
fw.write("\r\n")
|
||||
fw.write("CommitSha=")
|
||||
fw.write(BuildConfig.COMMIT_SHA)
|
||||
fw.write("\r\n")
|
||||
fw.write("BuildTime=")
|
||||
fw.write(BuildConfig.BUILD_TIME)
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
|
||||
// Runtime
|
||||
val topActivityClazzName = EhApplication.application.topActivity?.javaClass?.name
|
||||
fw.write("======== Runtime ========\r\n")
|
||||
fw.write("TopActivity=")
|
||||
fw.write(topActivityClazzName ?: "null")
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
|
||||
// Device info
|
||||
fw.write("======== DeviceInfo ========\r\n")
|
||||
fw.write("BOARD=")
|
||||
fw.write(Build.BOARD)
|
||||
fw.write("\r\n")
|
||||
fw.write("BOOTLOADER=")
|
||||
fw.write(Build.BOOTLOADER)
|
||||
fw.write("\r\n")
|
||||
fw.write("SUPPORTED_ABIS=")
|
||||
fw.write(Arrays.toString(Build.SUPPORTED_ABIS))
|
||||
fw.write("\r\n")
|
||||
fw.write("DEVICE=")
|
||||
fw.write(Build.DEVICE)
|
||||
fw.write("\r\n")
|
||||
fw.write("DISPLAY=")
|
||||
fw.write(Build.DISPLAY)
|
||||
fw.write("\r\n")
|
||||
fw.write("FINGERPRINT=")
|
||||
fw.write(Build.FINGERPRINT)
|
||||
fw.write("\r\n")
|
||||
fw.write("HARDWARE=")
|
||||
fw.write(Build.HARDWARE)
|
||||
fw.write("\r\n")
|
||||
fw.write("HOST=")
|
||||
fw.write(Build.HOST)
|
||||
fw.write("\r\n")
|
||||
fw.write("ID=")
|
||||
fw.write(Build.ID)
|
||||
fw.write("\r\n")
|
||||
fw.write("MANUFACTURER=")
|
||||
fw.write(Build.MANUFACTURER)
|
||||
fw.write("\r\n")
|
||||
fw.write("MODEL=")
|
||||
fw.write(Build.MODEL)
|
||||
fw.write("\r\n")
|
||||
fw.write("PRODUCT=")
|
||||
fw.write(Build.PRODUCT)
|
||||
fw.write("\r\n")
|
||||
fw.write("RADIO=")
|
||||
fw.write(Build.getRadioVersion())
|
||||
fw.write("\r\n")
|
||||
fw.write("TAGS=")
|
||||
fw.write(Build.TAGS)
|
||||
fw.write("\r\n")
|
||||
fw.write("TYPE=")
|
||||
fw.write(Build.TYPE)
|
||||
fw.write("\r\n")
|
||||
fw.write("USER=")
|
||||
fw.write(Build.USER)
|
||||
fw.write("\r\n")
|
||||
fw.write("CODENAME=")
|
||||
fw.write(Build.VERSION.CODENAME)
|
||||
fw.write("\r\n")
|
||||
fw.write("INCREMENTAL=")
|
||||
fw.write(Build.VERSION.INCREMENTAL)
|
||||
fw.write("\r\n")
|
||||
fw.write("RELEASE=")
|
||||
fw.write(Build.VERSION.RELEASE)
|
||||
fw.write("\r\n")
|
||||
fw.write("SDK=")
|
||||
fw.write(Build.VERSION.SDK_INT.toString())
|
||||
fw.write("\r\n")
|
||||
fw.write("MEMORY=")
|
||||
fw.write(
|
||||
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false)
|
||||
)
|
||||
fw.write("\r\n")
|
||||
fw.write("MEMORY_NATIVE=")
|
||||
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false))
|
||||
fw.write("\r\n")
|
||||
fw.write("MEMORY_MAX=")
|
||||
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false))
|
||||
fw.write("\r\n")
|
||||
fw.write("MEMORY_TOTAL=")
|
||||
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false))
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
}
|
||||
|
||||
private fun getThrowableInfo(t: Throwable, fw: FileWriter) {
|
||||
val printWriter = PrintWriter(fw)
|
||||
t.printStackTrace(printWriter)
|
||||
var cause = t.cause
|
||||
while (cause != null) {
|
||||
cause.printStackTrace(printWriter)
|
||||
cause = cause.cause
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCrashLog(t: Throwable) {
|
||||
val dir = AppConfig.getExternalCrashDir() ?: return
|
||||
val nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis())
|
||||
val fileName = "crash-$nowString.log"
|
||||
val file = File(dir, fileName)
|
||||
var fw: FileWriter? = null
|
||||
try {
|
||||
fw = FileWriter(file)
|
||||
fw.write("TIME=")
|
||||
fw.write(nowString)
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
collectInfo(fw)
|
||||
fw.write("======== CrashInfo ========\r\n")
|
||||
getThrowableInfo(t, fw)
|
||||
fw.write("\r\n")
|
||||
fw.flush()
|
||||
} catch (e: Exception) {
|
||||
file.delete()
|
||||
} finally {
|
||||
IOUtils.closeQuietly(fw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,39 +18,26 @@ package com.hippo.ehviewer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.LruCache
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
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
|
||||
import com.hippo.ehviewer.client.EhDns
|
||||
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.coil.MergeInterceptor
|
||||
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
|
||||
@@ -59,15 +46,9 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
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
|
||||
|
||||
@@ -95,7 +76,7 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
Thread.setDefaultUncaughtExceptionHandler { t, e ->
|
||||
try {
|
||||
if (Settings.getSaveCrashLog()) {
|
||||
Crash.saveCrashLog(application, e)
|
||||
Crash.saveCrashLog(e)
|
||||
}
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
@@ -107,8 +88,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 {
|
||||
@@ -116,14 +95,11 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
ehDatabase
|
||||
}
|
||||
launchIO {
|
||||
downloadManager
|
||||
DownloadManager.isIdle
|
||||
}
|
||||
launchIO {
|
||||
cleanupDownload()
|
||||
}
|
||||
launchIO {
|
||||
theDawnOfNewDay()
|
||||
}
|
||||
}
|
||||
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
|
||||
}
|
||||
@@ -150,60 +126,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
}
|
||||
}
|
||||
|
||||
private fun theDawnOfNewDay() {
|
||||
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
|
||||
)
|
||||
) {
|
||||
val referer = EhUrl.REFERER_E
|
||||
val request = EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
try {
|
||||
call.execute().use { response ->
|
||||
val responseBody = response.body
|
||||
val body = responseBody.string()
|
||||
val html = EventPaneParser.parse(body)
|
||||
if (html != null) {
|
||||
showEventPane(html)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showEventPane(html: String) {
|
||||
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
|
||||
return
|
||||
}
|
||||
val activity = topActivity
|
||||
activity?.runOnUiThread {
|
||||
val dialog = BaseDialogBuilder(activity)
|
||||
.setMessage(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
dialog.setOnShowListener {
|
||||
val messageView = dialog.findViewById<View>(android.R.id.message)
|
||||
if (messageView is TextView) {
|
||||
messageView.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
try {
|
||||
dialog.show()
|
||||
} catch (t: Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearTempDir() {
|
||||
var dir = AppConfig.getTempDir()
|
||||
if (null != dir) {
|
||||
@@ -259,15 +181,11 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
locked = true
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
okHttpClient(okHttpClient)
|
||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
// Coil spawns a new thread for every image load by default
|
||||
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
|
||||
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
components { add(MergeInterceptor) }
|
||||
crossfade(300)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@@ -290,29 +208,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,15 +233,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() }
|
||||
|
||||
@JvmStatic
|
||||
val hosts by lazy { Hosts(application, "hosts.db") }
|
||||
|
||||
|
||||
@@ -1,595 +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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.paging.PagingSource;
|
||||
import androidx.room.Room;
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.dao.BasicDao;
|
||||
import com.hippo.ehviewer.dao.DownloadDirname;
|
||||
import com.hippo.ehviewer.dao.DownloadDirnameDao;
|
||||
import com.hippo.ehviewer.dao.DownloadInfo;
|
||||
import com.hippo.ehviewer.dao.DownloadLabel;
|
||||
import com.hippo.ehviewer.dao.DownloadLabelDao;
|
||||
import com.hippo.ehviewer.dao.DownloadsDao;
|
||||
import com.hippo.ehviewer.dao.EhDatabase;
|
||||
import com.hippo.ehviewer.dao.Filter;
|
||||
import com.hippo.ehviewer.dao.HistoryDao;
|
||||
import com.hippo.ehviewer.dao.HistoryInfo;
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo;
|
||||
import com.hippo.ehviewer.dao.LocalFavoritesDao;
|
||||
import com.hippo.ehviewer.dao.QuickSearch;
|
||||
import com.hippo.ehviewer.dao.QuickSearchDao;
|
||||
import com.hippo.ehviewer.download.DownloadManager;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.ObjectUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EhDB {
|
||||
private static final int CUR_DB_VER = 4;
|
||||
|
||||
private static final EhDatabase db = EhApplication.getEhDatabase();
|
||||
|
||||
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
|
||||
switch (oldVersion) {
|
||||
case 1: // 1 to 2, add FILTER
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS \"FILTER\" (" + //
|
||||
"\"_id\" INTEGER PRIMARY KEY ," + // 0: id
|
||||
"\"MODE\" INTEGER NOT NULL ," + // 1: mode
|
||||
"\"TEXT\" TEXT," + // 2: text
|
||||
"\"ENABLE\" INTEGER);"); // 3: enable
|
||||
case 2: // 2 to 3, add ENABLE column to table FILTER
|
||||
db.execSQL("CREATE TABLE " + "\"FILTER2\" (" +
|
||||
"\"_id\" INTEGER PRIMARY KEY ," +
|
||||
"\"MODE\" INTEGER NOT NULL ," +
|
||||
"\"TEXT\" TEXT," +
|
||||
"\"ENABLE\" INTEGER);");
|
||||
db.execSQL("INSERT INTO \"FILTER2\" (" +
|
||||
"_id, MODE, TEXT, ENABLE)" +
|
||||
"SELECT _id, MODE, TEXT, 1 FROM FILTER;");
|
||||
db.execSQL("DROP TABLE FILTER");
|
||||
db.execSQL("ALTER TABLE FILTER2 RENAME TO FILTER");
|
||||
case 3: // 3 to 4, add PAGE_FROM and PAGE_TO column to QUICK_SEARCH
|
||||
db.execSQL("CREATE TABLE " + "\"QUICK_SEARCH2\" (" +
|
||||
"\"_id\" INTEGER PRIMARY KEY ," +
|
||||
"\"NAME\" TEXT," +
|
||||
"\"MODE\" INTEGER NOT NULL ," +
|
||||
"\"CATEGORY\" INTEGER NOT NULL ," +
|
||||
"\"KEYWORD\" TEXT," +
|
||||
"\"ADVANCE_SEARCH\" INTEGER NOT NULL ," +
|
||||
"\"MIN_RATING\" INTEGER NOT NULL ," +
|
||||
"\"PAGE_FROM\" INTEGER NOT NULL ," +
|
||||
"\"PAGE_TO\" INTEGER NOT NULL ," +
|
||||
"\"TIME\" INTEGER NOT NULL );");
|
||||
db.execSQL("INSERT INTO \"QUICK_SEARCH2\" (" +
|
||||
"_id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, PAGE_FROM, PAGE_TO, TIME)" +
|
||||
"SELECT _id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, -1, -1, TIME FROM QUICK_SEARCH;");
|
||||
db.execSQL("DROP TABLE QUICK_SEARCH");
|
||||
db.execSQL("ALTER TABLE QUICK_SEARCH2 RENAME TO QUICK_SEARCH");
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized List<DownloadInfo> getAllDownloadInfo() {
|
||||
DownloadsDao dao = db.downloadsDao();
|
||||
List<DownloadInfo> list = dao.list();
|
||||
// Fix state
|
||||
for (DownloadInfo info : list) {
|
||||
if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {
|
||||
info.state = DownloadInfo.STATE_NONE;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Insert or update
|
||||
public static synchronized void putDownloadInfo(DownloadInfo downloadInfo) {
|
||||
DownloadsDao dao = db.downloadsDao();
|
||||
if (null != dao.load(downloadInfo.getGid())) {
|
||||
// Update
|
||||
dao.update(downloadInfo);
|
||||
} else {
|
||||
// Insert
|
||||
dao.insert(downloadInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void removeDownloadInfo(DownloadInfo downloadInfo) {
|
||||
db.downloadsDao().delete(downloadInfo);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static synchronized String getDownloadDirname(long gid) {
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
DownloadDirname raw = dao.load(gid);
|
||||
if (raw != null) {
|
||||
return raw.getDirname();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update
|
||||
*/
|
||||
public static synchronized void putDownloadDirname(long gid, String dirname) {
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
DownloadDirname raw = dao.load(gid);
|
||||
if (raw != null) { // Update
|
||||
raw.setDirname(dirname);
|
||||
dao.update(raw);
|
||||
} else { // Insert
|
||||
raw = new DownloadDirname();
|
||||
raw.setGid(gid);
|
||||
raw.setDirname(dirname);
|
||||
dao.insert(raw);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void removeDownloadDirname(long gid) {
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
dao.deleteByKey(gid);
|
||||
}
|
||||
|
||||
public static synchronized void clearDownloadDirname() {
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
dao.deleteAll();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static synchronized List<DownloadLabel> getAllDownloadLabelList() {
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
return dao.list();
|
||||
}
|
||||
|
||||
public static synchronized DownloadLabel addDownloadLabel(String label) {
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
DownloadLabel raw = new DownloadLabel();
|
||||
raw.setLabel(label);
|
||||
raw.setTime(System.currentTimeMillis());
|
||||
raw.setId(dao.insert(raw));
|
||||
return raw;
|
||||
}
|
||||
|
||||
public static synchronized DownloadLabel addDownloadLabel(DownloadLabel raw) {
|
||||
// Reset id
|
||||
raw.setId(null);
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
raw.setId(dao.insert(raw));
|
||||
return raw;
|
||||
}
|
||||
|
||||
public static synchronized void updateDownloadLabel(DownloadLabel raw) {
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
dao.update(raw);
|
||||
}
|
||||
|
||||
public static synchronized void moveDownloadLabel(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;
|
||||
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
List<DownloadLabel> 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).getTime();
|
||||
for (int i = end; reverse ? i < start : i > start; i += step) {
|
||||
list.get(i).setTime(list.get(i + step).getTime());
|
||||
}
|
||||
list.get(start).setTime(toTime);
|
||||
|
||||
dao.update(list);
|
||||
}
|
||||
|
||||
public static synchronized void removeDownloadLabel(DownloadLabel raw) {
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
dao.delete(raw);
|
||||
}
|
||||
|
||||
public static synchronized List<GalleryInfo> getAllLocalFavorites() {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
List<LocalFavoriteInfo> list = dao.list();
|
||||
return new ArrayList<>(list);
|
||||
}
|
||||
|
||||
public static synchronized List<GalleryInfo> searchLocalFavorites(String query) {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
List<LocalFavoriteInfo> list = dao.list("%" + query + "%");
|
||||
return new ArrayList<>(list);
|
||||
}
|
||||
|
||||
public static synchronized void removeLocalFavorites(long gid) {
|
||||
db.localFavoritesDao().deleteByKey(gid);
|
||||
}
|
||||
|
||||
public static synchronized void removeLocalFavorites(long[] gidArray) {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
for (long gid : gidArray) {
|
||||
dao.deleteByKey(gid);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized boolean containLocalFavorites(long gid) {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
return null != dao.load(gid);
|
||||
}
|
||||
|
||||
public static synchronized void putLocalFavorites(GalleryInfo galleryInfo) {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
if (null == dao.load(galleryInfo.getGid())) {
|
||||
LocalFavoriteInfo info;
|
||||
if (galleryInfo instanceof LocalFavoriteInfo) {
|
||||
info = (LocalFavoriteInfo) galleryInfo;
|
||||
} else {
|
||||
info = new LocalFavoriteInfo(galleryInfo);
|
||||
info.time = System.currentTimeMillis();
|
||||
}
|
||||
dao.insert(info);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void putLocalFavorites(List<GalleryInfo> galleryInfoList) {
|
||||
for (GalleryInfo gi : galleryInfoList) {
|
||||
putLocalFavorites(gi);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized List<QuickSearch> getAllQuickSearch() {
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
return dao.list();
|
||||
}
|
||||
|
||||
public static synchronized void insertQuickSearch(QuickSearch quickSearch) {
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
quickSearch.setId(null);
|
||||
quickSearch.setTime(System.currentTimeMillis());
|
||||
quickSearch.setId(dao.insert(quickSearch));
|
||||
}
|
||||
|
||||
public static synchronized void importQuickSearch(List<QuickSearch> quickSearchList) {
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
for (QuickSearch quickSearch : quickSearchList) {
|
||||
dao.insert(quickSearch);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void deleteQuickSearch(QuickSearch quickSearch) {
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
dao.delete(quickSearch);
|
||||
}
|
||||
|
||||
public static synchronized void moveQuickSearch(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;
|
||||
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
List<QuickSearch> 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).getTime();
|
||||
for (int i = end; reverse ? i < start : i > start; i += step) {
|
||||
list.get(i).setTime(list.get(i + step).getTime());
|
||||
}
|
||||
list.get(start).setTime(toTime);
|
||||
|
||||
dao.update(list);
|
||||
}
|
||||
|
||||
public static synchronized PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
|
||||
return db.historyDao().listLazy();
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
HistoryInfo info;
|
||||
if (galleryInfo instanceof HistoryInfo) {
|
||||
info = (HistoryInfo) galleryInfo;
|
||||
} else {
|
||||
info = new HistoryInfo(galleryInfo);
|
||||
}
|
||||
info.time = System.currentTimeMillis();
|
||||
if (null != dao.load(info.getGid())) {
|
||||
dao.update(info);
|
||||
} else {
|
||||
dao.insert(info);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
for (HistoryInfo info : historyInfoList) {
|
||||
if (null == dao.load(info.getGid())) {
|
||||
dao.insert(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void deleteHistoryInfo(HistoryInfo info) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
dao.delete(info);
|
||||
}
|
||||
|
||||
public static synchronized void clearHistoryInfo() {
|
||||
HistoryDao dao = db.historyDao();
|
||||
dao.deleteAll();
|
||||
}
|
||||
|
||||
public static synchronized List<Filter> getAllFilter() {
|
||||
return db.filterDao().list();
|
||||
}
|
||||
|
||||
public static synchronized boolean addFilter(Filter filter) {
|
||||
Filter existFilter;
|
||||
try {
|
||||
existFilter = db.filterDao().load(filter.text, filter.mode);
|
||||
} catch (Exception e) {
|
||||
existFilter = null;
|
||||
}
|
||||
if (existFilter == null) {
|
||||
filter.setId(null);
|
||||
filter.setId(db.filterDao().insert(filter));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void deleteFilter(Filter filter) {
|
||||
db.filterDao().delete(filter);
|
||||
}
|
||||
|
||||
public static synchronized void triggerFilter(Filter filter) {
|
||||
filter.enable = !filter.enable;
|
||||
db.filterDao().update(filter);
|
||||
}
|
||||
|
||||
private static <T> void copyDao(BasicDao<T> from, BasicDao<T> to) {
|
||||
List<T> list = from.list();
|
||||
for (T item : list)
|
||||
to.insert(item);
|
||||
}
|
||||
|
||||
public static synchronized boolean exportDB(Context context, Uri uri) {
|
||||
final String ehExportName = "eh.export.db";
|
||||
|
||||
// Delete old export db
|
||||
context.deleteDatabase(ehExportName);
|
||||
EhDatabase newDb = Room.databaseBuilder(context, EhDatabase.class, ehExportName).allowMainThreadQueries().build();
|
||||
|
||||
try {
|
||||
// Copy data to a export db
|
||||
copyDao(db.downloadsDao(), newDb.downloadsDao());
|
||||
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao());
|
||||
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao());
|
||||
copyDao(db.historyDao(), newDb.historyDao());
|
||||
copyDao(db.quickSearchDao(), newDb.quickSearchDao());
|
||||
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao());
|
||||
copyDao(db.filterDao(), newDb.filterDao());
|
||||
|
||||
// Close export db so we can copy it
|
||||
if (newDb.isOpen()) {
|
||||
newDb.close();
|
||||
}
|
||||
|
||||
// Copy export db to data dir
|
||||
File dbFile = context.getDatabasePath(ehExportName);
|
||||
if (dbFile == null || !dbFile.isFile()) {
|
||||
return false;
|
||||
}
|
||||
InputStream is = null;
|
||||
OutputStream os = null;
|
||||
try {
|
||||
is = new FileInputStream(dbFile);
|
||||
os = context.getContentResolver().openOutputStream(uri);
|
||||
IOUtils.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
|
||||
// Delete failed file
|
||||
return false;
|
||||
} finally {
|
||||
context.deleteDatabase(ehExportName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return error string, null for no error
|
||||
*/
|
||||
public static synchronized String importDB(Context context, Uri uri) {
|
||||
try {
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||
|
||||
// Copy file to cache dir
|
||||
File file = File.createTempFile("importDatabase", "");
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
byte[] buff = new byte[1024];
|
||||
int read;
|
||||
if (inputStream != null) {
|
||||
while ((read = inputStream.read(buff, 0, buff.length)) > 0) {
|
||||
outputStream.write(buff, 0, read);
|
||||
}
|
||||
} else {
|
||||
return context.getString(R.string.cant_read_the_file);
|
||||
}
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
|
||||
// Check database version
|
||||
SQLiteDatabase oldDB = SQLiteDatabase.openDatabase(
|
||||
file.getPath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
|
||||
int newVersion = CUR_DB_VER;
|
||||
int oldVersion = oldDB.getVersion();
|
||||
if (oldVersion < newVersion) {
|
||||
upgradeDB(oldDB, oldVersion);
|
||||
oldDB.setVersion(newVersion);
|
||||
} else if (oldVersion > newVersion) {
|
||||
return context.getString(R.string.db_version_too_high);
|
||||
}
|
||||
|
||||
// Crete temp room database from cache file
|
||||
String tmpDBName = "tmp.db";
|
||||
context.deleteDatabase(tmpDBName);
|
||||
EhDatabase oldRoomDatabase = Room.databaseBuilder(context, EhDatabase.class, tmpDBName)
|
||||
.createFromFile(file).allowMainThreadQueries().build();
|
||||
|
||||
// Download label
|
||||
DownloadManager manager = EhApplication.getDownloadManager();
|
||||
try {
|
||||
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
|
||||
manager.addDownloadLabel(downloadLabelList);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Downloads
|
||||
try {
|
||||
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
|
||||
manager.addDownload(downloadInfoList, false);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Download dirname
|
||||
try {
|
||||
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
|
||||
for (DownloadDirname dirname : downloadDirnameList) {
|
||||
putDownloadDirname(dirname.getGid(), dirname.getDirname());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// History
|
||||
try {
|
||||
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
|
||||
putHistoryInfo(historyInfoList);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// QuickSearch
|
||||
try {
|
||||
List<QuickSearch> quickSearchList = oldRoomDatabase.quickSearchDao().list();
|
||||
List<QuickSearch> currentQuickSearchList = db.quickSearchDao().list();
|
||||
List<QuickSearch> importList = new ArrayList<>();
|
||||
for (QuickSearch quickSearch : quickSearchList) {
|
||||
String name = quickSearch.getName();
|
||||
for (QuickSearch q : currentQuickSearchList) {
|
||||
if (ObjectUtils.equal(q.getName(), name)) {
|
||||
// The same name
|
||||
name = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (null == name) {
|
||||
continue;
|
||||
}
|
||||
importList.add(quickSearch);
|
||||
}
|
||||
importQuickSearch(importList);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// LocalFavorites
|
||||
try {
|
||||
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
|
||||
for (LocalFavoriteInfo info : localFavoriteInfoList) {
|
||||
putLocalFavorites(info);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Filter
|
||||
try {
|
||||
List<Filter> filterList = oldRoomDatabase.filterDao().list();
|
||||
List<Filter> currentFilterList = db.filterDao().list();
|
||||
for (Filter filter : filterList) {
|
||||
if (!currentFilterList.contains(filter)) {
|
||||
addFilter(filter);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Delete temp database
|
||||
if (oldRoomDatabase.isOpen()) {
|
||||
oldRoomDatabase.close();
|
||||
}
|
||||
context.deleteDatabase(tmpDBName);
|
||||
|
||||
return null;
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
// Ignore
|
||||
return context.getString(R.string.cant_read_the_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Room.databaseBuilder
|
||||
import com.hippo.Native
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ehDatabase
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.dao.BasicDao
|
||||
import com.hippo.ehviewer.dao.DownloadDirname
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.dao.DownloadLabel
|
||||
import com.hippo.ehviewer.dao.EhDatabase
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.ehviewer.dao.HistoryInfo
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||
import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
|
||||
object EhDB {
|
||||
private const val CUR_DB_VER = 4
|
||||
private val db = ehDatabase
|
||||
|
||||
// Fix state
|
||||
@get:Synchronized
|
||||
val allDownloadInfo: List<DownloadInfo>
|
||||
get() = db.downloadsDao().list().onEach {
|
||||
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
|
||||
it.state = DownloadInfo.STATE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateDownloadInfo(downloadInfos: List<DownloadInfo>) {
|
||||
val dao = db.downloadsDao()
|
||||
dao.update(downloadInfos)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putDownloadInfo(downloadInfo: DownloadInfo) {
|
||||
db.downloadsDao().run {
|
||||
if (load(downloadInfo.gid) != null) {
|
||||
update(downloadInfo)
|
||||
} else {
|
||||
insert(downloadInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeDownloadInfo(downloadInfo: DownloadInfo) {
|
||||
db.downloadsDao().delete(downloadInfo)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getDownloadDirname(gid: Long): String? {
|
||||
val dao = db.downloadDirnameDao()
|
||||
val raw = dao.load(gid)
|
||||
return raw?.dirname
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putDownloadDirname(gid: Long, dirname: String?) {
|
||||
val dao = db.downloadDirnameDao()
|
||||
var raw = dao.load(gid)
|
||||
if (raw != null) {
|
||||
raw.dirname = dirname
|
||||
dao.update(raw)
|
||||
} else {
|
||||
raw = DownloadDirname(gid, dirname)
|
||||
dao.insert(raw)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeDownloadDirname(gid: Long) {
|
||||
val dao = db.downloadDirnameDao()
|
||||
dao.deleteByKey(gid)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clearDownloadDirname() {
|
||||
val dao = db.downloadDirnameDao()
|
||||
dao.deleteAll()
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val allDownloadLabelList: List<DownloadLabel>
|
||||
get() = db.downloadLabelDao().list()
|
||||
|
||||
@Synchronized
|
||||
fun addDownloadLabel(label: String): DownloadLabel {
|
||||
val dao = db.downloadLabelDao()
|
||||
val raw = DownloadLabel()
|
||||
raw.label = label
|
||||
raw.time = System.currentTimeMillis()
|
||||
raw.id = dao.insert(raw)
|
||||
return raw
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
|
||||
// Reset id
|
||||
raw.id = null
|
||||
val dao = db.downloadLabelDao()
|
||||
raw.id = dao.insert(raw)
|
||||
return raw
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateDownloadLabel(raw: DownloadLabel?) {
|
||||
val dao = db.downloadLabelDao()
|
||||
dao.update(raw!!)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun moveDownloadLabel(fromPosition: Int, toPosition: Int) {
|
||||
if (fromPosition == toPosition) {
|
||||
return
|
||||
}
|
||||
val reverse = fromPosition > toPosition
|
||||
val offset = if (reverse) toPosition else fromPosition
|
||||
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
|
||||
val dao = db.downloadLabelDao()
|
||||
val list = dao.list(offset, limit)
|
||||
val step = if (reverse) 1 else -1
|
||||
val start = if (reverse) limit - 1 else 0
|
||||
val end = if (reverse) 0 else limit - 1
|
||||
val toTime = list[end].time
|
||||
var i = end
|
||||
while (if (reverse) i < start else i > start) {
|
||||
list[i].time = list[i + step].time
|
||||
i += step
|
||||
}
|
||||
list[start].time = toTime
|
||||
dao.update(list)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeDownloadLabel(raw: DownloadLabel?) {
|
||||
val dao = db.downloadLabelDao()
|
||||
dao.delete(raw!!)
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val allLocalFavorites: List<GalleryInfo>
|
||||
get() {
|
||||
val dao = db.localFavoritesDao()
|
||||
val list = dao.list()
|
||||
return ArrayList<GalleryInfo>(list)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun searchLocalFavorites(query: String): List<GalleryInfo> {
|
||||
val dao = db.localFavoritesDao()
|
||||
val list = dao.list("%$query%")
|
||||
return ArrayList<GalleryInfo>(list)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeLocalFavorites(gid: Long) {
|
||||
db.localFavoritesDao().deleteByKey(gid)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeLocalFavorites(gidArray: LongArray) {
|
||||
val dao = db.localFavoritesDao()
|
||||
for (gid in gidArray) {
|
||||
dao.deleteByKey(gid)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun containLocalFavorites(gid: Long): Boolean {
|
||||
val dao = db.localFavoritesDao()
|
||||
return null != dao.load(gid)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putLocalFavorites(galleryInfo: GalleryInfo) {
|
||||
val dao = db.localFavoritesDao()
|
||||
if (null == dao.load(galleryInfo.gid)) {
|
||||
val info: LocalFavoriteInfo
|
||||
if (galleryInfo is LocalFavoriteInfo) {
|
||||
info = galleryInfo
|
||||
} else {
|
||||
info = LocalFavoriteInfo(galleryInfo)
|
||||
info.time = System.currentTimeMillis()
|
||||
}
|
||||
dao.insert(info)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putLocalFavorites(galleryInfoList: List<GalleryInfo>) {
|
||||
for (gi in galleryInfoList) {
|
||||
putLocalFavorites(gi)
|
||||
}
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val allQuickSearch: List<QuickSearch>
|
||||
get() {
|
||||
val dao = db.quickSearchDao()
|
||||
return dao.list()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun insertQuickSearch(quickSearch: QuickSearch) {
|
||||
val dao = db.quickSearchDao()
|
||||
quickSearch.id = null
|
||||
quickSearch.time = System.currentTimeMillis()
|
||||
quickSearch.id = dao.insert(quickSearch)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun importQuickSearch(quickSearchList: List<QuickSearch?>) {
|
||||
val dao = db.quickSearchDao()
|
||||
for (quickSearch in quickSearchList) {
|
||||
dao.insert(quickSearch!!)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteQuickSearch(quickSearch: QuickSearch?) {
|
||||
val dao = db.quickSearchDao()
|
||||
dao.delete(quickSearch)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun moveQuickSearch(fromPosition: Int, toPosition: Int) {
|
||||
if (fromPosition == toPosition) {
|
||||
return
|
||||
}
|
||||
val reverse = fromPosition > toPosition
|
||||
val offset = if (reverse) toPosition else fromPosition
|
||||
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
|
||||
val dao = db.quickSearchDao()
|
||||
val list = dao.list(offset, limit)
|
||||
val step = if (reverse) 1 else -1
|
||||
val start = if (reverse) limit - 1 else 0
|
||||
val end = if (reverse) 0 else limit - 1
|
||||
val toTime = list[end].time
|
||||
var i = end
|
||||
while (if (reverse) i < start else i > start) {
|
||||
list[i].time = list[i + step].time
|
||||
i += step
|
||||
}
|
||||
list[start].time = toTime
|
||||
dao.update(list)
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val historyLazyList: PagingSource<Int, HistoryInfo>
|
||||
get() = db.historyDao().listLazy()
|
||||
|
||||
@Synchronized
|
||||
fun putHistoryInfo(galleryInfo: GalleryInfo?) {
|
||||
val dao = db.historyDao()
|
||||
val info: HistoryInfo = if (galleryInfo is HistoryInfo) {
|
||||
galleryInfo
|
||||
} else {
|
||||
HistoryInfo(galleryInfo!!)
|
||||
}
|
||||
info.time = System.currentTimeMillis()
|
||||
if (null != dao.load(info.gid)) {
|
||||
dao.update(info)
|
||||
} else {
|
||||
dao.insert(info)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putHistoryInfoNonRefresh(info: GalleryInfo) {
|
||||
val dao = db.historyDao()
|
||||
val i = dao.load(info.gid)
|
||||
if (null != i) {
|
||||
val historyInfo: HistoryInfo
|
||||
if (info is HistoryInfo) {
|
||||
historyInfo = info
|
||||
} else {
|
||||
historyInfo = HistoryInfo(info)
|
||||
historyInfo.time = i.time
|
||||
}
|
||||
dao.update(historyInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putHistoryInfo(historyInfoList: List<HistoryInfo>) {
|
||||
val dao = db.historyDao()
|
||||
for (info in historyInfoList) {
|
||||
if (null == dao.load(info.gid)) {
|
||||
dao.insert(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteHistoryInfo(info: HistoryInfo?) {
|
||||
val dao = db.historyDao()
|
||||
dao.delete(info!!)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clearHistoryInfo() {
|
||||
val dao = db.historyDao()
|
||||
dao.deleteAll()
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val allFilter: List<Filter>
|
||||
get() = db.filterDao().list()
|
||||
|
||||
@Synchronized
|
||||
fun addFilter(filter: Filter): Boolean {
|
||||
val existFilter: Filter? = try {
|
||||
db.filterDao().load(filter.text!!, filter.mode)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
return if (existFilter == null) {
|
||||
filter.id = null
|
||||
filter.id = db.filterDao().insert(filter)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteFilter(filter: Filter) {
|
||||
db.filterDao().delete(filter)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun triggerFilter(filter: Filter) {
|
||||
filter.enable = filter.enable?.not() ?: false
|
||||
db.filterDao().update(filter)
|
||||
}
|
||||
|
||||
private fun <T> copyDao(from: BasicDao<T>, to: BasicDao<T>) {
|
||||
val list = from.list()
|
||||
for (item in list) to.insert(item)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun exportDB(context: Context, uri: Uri): Boolean {
|
||||
val ehExportName = "eh.export.db"
|
||||
runCatching {
|
||||
context.deleteDatabase(ehExportName)
|
||||
val newDb = databaseBuilder(context, EhDatabase::class.java, ehExportName).build()
|
||||
copyDao(db.downloadsDao(), newDb.downloadsDao())
|
||||
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao())
|
||||
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao())
|
||||
copyDao(db.historyDao(), newDb.historyDao())
|
||||
copyDao(db.quickSearchDao(), newDb.quickSearchDao())
|
||||
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao())
|
||||
copyDao(db.filterDao(), newDb.filterDao())
|
||||
val dbFile = context.getDatabasePath(ehExportName)
|
||||
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
|
||||
ParcelFileDescriptor.open(dbFile, MODE_READ_ONLY).use { fromFd ->
|
||||
Native.sendfile(fromFd.fd, toFd.fd)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return error string, null for no error
|
||||
*/
|
||||
@Synchronized
|
||||
fun importDB(context: Context, uri: Uri): String? {
|
||||
runCatching {
|
||||
val oldDB = databaseBuilder(context, EhDatabase::class.java, "tmp.db")
|
||||
.createFromInputStream { context.contentResolver.openInputStream(uri) }.build()
|
||||
|
||||
// Download label
|
||||
val manager = DownloadManager
|
||||
runCatching {
|
||||
val downloadLabelList = oldDB.downloadLabelDao().list()
|
||||
manager.addDownloadLabel(downloadLabelList)
|
||||
}
|
||||
|
||||
// Downloads
|
||||
runCatching {
|
||||
val downloadInfoList = oldDB.downloadsDao().list()
|
||||
manager.addDownload(downloadInfoList, false)
|
||||
}
|
||||
|
||||
// Download dirname
|
||||
runCatching {
|
||||
oldDB.downloadDirnameDao().list().forEach {
|
||||
putDownloadDirname(it.gid, it.dirname)
|
||||
}
|
||||
}
|
||||
|
||||
// History
|
||||
runCatching {
|
||||
val historyInfoList = oldDB.historyDao().list()
|
||||
putHistoryInfo(historyInfoList)
|
||||
}
|
||||
|
||||
// QuickSearch
|
||||
runCatching {
|
||||
val quickSearchList = oldDB.quickSearchDao().list()
|
||||
val currentQuickSearchList = db.quickSearchDao().list()
|
||||
val importList = quickSearchList.mapNotNull { newQS ->
|
||||
newQS.takeIf { currentQuickSearchList.find { it.name == newQS.name } == null }
|
||||
}
|
||||
importQuickSearch(importList)
|
||||
}
|
||||
|
||||
// LocalFavorites
|
||||
runCatching {
|
||||
oldDB.localFavoritesDao().list().forEach {
|
||||
putLocalFavorites(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter
|
||||
runCatching {
|
||||
val filterList = oldDB.filterDao().list()
|
||||
val currentFilterList = db.filterDao().list()
|
||||
filterList.forEach {
|
||||
if (it !in currentFilterList) addFilter(it)
|
||||
}
|
||||
}
|
||||
oldDB.close()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
return context.getString(R.string.cant_read_the_file)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder;
|
||||
import com.hippo.ehviewer.ui.CommonOperations;
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.AssertUtils;
|
||||
import com.hippo.yorozuya.AssertUtilsKt;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -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,28 @@ 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_REQUEST_NEWS_TIMER = "request_news_timer";
|
||||
public static final String KEY_REQUEST_NEWS_TIMER_HOUR = "request_news_timer_hour";
|
||||
public static final String KEY_REQUEST_NEWS_TIMER_MINUTE = "request_news_timer_minute";
|
||||
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_REQUEST_NEWS_TIMER,
|
||||
KEY_HIDE_HV_EVENTS
|
||||
};
|
||||
/********************
|
||||
****** Privacy and Security
|
||||
********************/
|
||||
@@ -87,8 +107,7 @@ public class Settings {
|
||||
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
|
||||
public static final String KEY_BYPASS_VPN = "bypass_vpn";
|
||||
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
|
||||
private static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
|
||||
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
|
||||
private static final boolean DEFAULT_HIDE_HV_EVENTS = false;
|
||||
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
|
||||
private static final boolean DEFAULT_SHOW_COMMENTS = true;
|
||||
private static final String TAG = Settings.class.getSimpleName();
|
||||
@@ -110,7 +129,6 @@ public class Settings {
|
||||
private static final int DEFAULT_DETAIL_SIZE = 0;
|
||||
private static final int DEFAULT_THUMB_SIZE = 120;
|
||||
private static final int DEFAULT_THUMB_RESOLUTION = 0;
|
||||
private static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
|
||||
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
|
||||
private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages";
|
||||
private static final boolean DEFAULT_SHOW_GALLERY_PAGES = false;
|
||||
@@ -191,8 +209,7 @@ 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 boolean DEFAULT_REQUEST_NEWS = false;
|
||||
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
|
||||
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
|
||||
private static final boolean DEFAULT_QS_SAVE_PROGRESS = true;
|
||||
@@ -549,7 +566,7 @@ public class Settings {
|
||||
}
|
||||
|
||||
public static void putFavCat(String[] value) {
|
||||
AssertUtils.assertEquals(10, value.length);
|
||||
AssertUtilsKt.assertEquals(10, value.length);
|
||||
sSettingsPre.edit()
|
||||
.putString(KEY_FAV_CAT_0, value[0])
|
||||
.putString(KEY_FAV_CAT_1, value[1])
|
||||
@@ -580,7 +597,7 @@ public class Settings {
|
||||
}
|
||||
|
||||
public static void putFavCount(int[] count) {
|
||||
AssertUtils.assertEquals(10, count.length);
|
||||
AssertUtilsKt.assertEquals(10, count.length);
|
||||
sSettingsPre.edit()
|
||||
.putInt(KEY_FAV_COUNT_0, count[0])
|
||||
.putInt(KEY_FAV_COUNT_1, count[1])
|
||||
@@ -675,7 +692,7 @@ public class Settings {
|
||||
putBoolean(KEY_DOMAIN_FRONTING, value);
|
||||
}
|
||||
|
||||
public static boolean getBypassVPN() {
|
||||
public static boolean getBypassVpn() {
|
||||
return getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
public class EhCacheKeyFactory {
|
||||
|
||||
public static String getThumbKey(long gid) {
|
||||
return "preview:large:" + gid + ":" + 0; // "thumb:" + gid;
|
||||
}
|
||||
|
||||
public static String getNormalPreviewKey(long gid, int index) {
|
||||
return "preview:normal:" + gid + ":" + index;
|
||||
}
|
||||
|
||||
public static String getLargePreviewKey(long gid, int index) {
|
||||
return "preview:large:" + gid + ":" + index;
|
||||
}
|
||||
|
||||
public static String getLargePreviewSetKey(long gid, int index) {
|
||||
return "large_preview_set:" + gid + ":" + index;
|
||||
}
|
||||
|
||||
public static String getImageKey(long gid, int index) {
|
||||
return "image:" + gid + ":" + index;
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -13,6 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client.exception
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
class Image509Exception : EhException("509")
|
||||
object EhCacheKeyFactory {
|
||||
fun getImageKey(gid: Long, index: Int): String {
|
||||
return "image:$gid:$index"
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,12 @@ object EhCookieStore : CookieJar {
|
||||
|
||||
fun hasSignedIn(): Boolean {
|
||||
val url = EhUrl.HOST_E.toHttpUrl()
|
||||
return contains(url, KEY_IPD_MEMBER_ID) &&
|
||||
contains(url, KEY_IPD_PASS_HASH)
|
||||
return contains(url, KEY_IPB_MEMBER_ID) &&
|
||||
contains(url, KEY_IPB_PASS_HASH)
|
||||
}
|
||||
|
||||
const val KEY_IPD_MEMBER_ID = "ipb_member_id"
|
||||
const val KEY_IPD_PASS_HASH = "ipb_pass_hash"
|
||||
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
||||
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
||||
const val KEY_IGNEOUS = "igneous"
|
||||
private val sTipsCookie: Cookie = Cookie.Builder()
|
||||
.name(EhConfig.KEY_CONTENT_WARNING)
|
||||
@@ -183,7 +183,10 @@ object EhCookieStore : CookieJar {
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
for (cookie in cookies) {
|
||||
addCookie(cookie)
|
||||
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
||||
if (cookie.name != "__utmp") {
|
||||
addCookie(cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import android.util.Log
|
||||
import android.util.Pair
|
||||
import com.hippo.ehviewer.AppConfig
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhApplication.Companion.application
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -41,6 +40,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
|
||||
@@ -49,7 +49,8 @@ import com.hippo.ehviewer.client.parser.VoteCommentParser
|
||||
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 +86,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 +100,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 +115,6 @@ object EhEngine {
|
||||
throw EhException(GetText.getString(R.string.error_parse_error))
|
||||
}
|
||||
}
|
||||
if (code >= 400) {
|
||||
throw StatusCodeException(code)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
@@ -319,11 +320,7 @@ object EhEngine {
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
val html = EventPaneParser.parse(body)
|
||||
if (html != null) {
|
||||
application.showEventPane(html)
|
||||
}
|
||||
return GalleryDetailParser.parse(body)
|
||||
return GalleryDetailParser.parse(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
@@ -348,8 +345,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) {
|
||||
@@ -563,7 +560,7 @@ object EhEngine {
|
||||
gidArray: LongArray,
|
||||
tokenArray: Array<String?>, dstCat: Int
|
||||
): Void? {
|
||||
AssertUtils.assertEquals(gidArray.size, tokenArray.size)
|
||||
check(gidArray.size == tokenArray.size)
|
||||
var i = 0
|
||||
val n = gidArray.size
|
||||
while (i < n) {
|
||||
@@ -760,7 +757,7 @@ object EhEngine {
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getFunds(): ArchiveParser.Funds? {
|
||||
suspend fun getFunds(): HomeParser.Funds {
|
||||
val url = EhUrl.URL_FUNDS
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, null).build()
|
||||
@@ -773,7 +770,93 @@ object EhEngine {
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
return ArchiveParser.parseFunds(body!!)
|
||||
return HomeParser.parseFunds(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
private suspend fun getImageLimitsInternal(): HomeParser.Limits {
|
||||
val url = EhUrl.URL_HOME
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, null).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
var body: String? = null
|
||||
var headers: Headers? = null
|
||||
var code = -1
|
||||
try {
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
return HomeParser.parse(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getImageLimits(): HomeParser.Result = coroutineScope {
|
||||
val limitsDeferred = async {
|
||||
getImageLimitsInternal()
|
||||
}
|
||||
val fundsDeferred = async {
|
||||
getFunds()
|
||||
}
|
||||
HomeParser.Result(limitsDeferred.await(), fundsDeferred.await())
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun resetImageLimits(): HomeParser.Limits? {
|
||||
val builder = FormBody.Builder()
|
||||
.add("act", "limits")
|
||||
.add("reset", "Reset Limit")
|
||||
val url = EhUrl.URL_HOME
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, null)
|
||||
.post(builder.build())
|
||||
.build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
var body: String? = null
|
||||
var headers: Headers? = null
|
||||
var code = -1
|
||||
try {
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
return HomeParser.parseResetLimits(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getNews(parse: Boolean): String? {
|
||||
val url = EhUrl.URL_NEWS
|
||||
val referer = EhUrl.REFERER_E
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer).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 if (parse) EventPaneParser.parse(body!!) else null
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
@@ -830,8 +913,7 @@ object EhEngine {
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getUConfig(): Void? {
|
||||
val url = EhUrl.uConfigUrl
|
||||
suspend fun getUConfig(url: String = EhUrl.uConfigUrl): Void? {
|
||||
Log.d(TAG, url)
|
||||
var request = EhRequestBuilder(url, null).build()
|
||||
var call = okHttpClient.newCall(request)
|
||||
@@ -1021,7 +1103,7 @@ object EhEngine {
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Throwable::class)
|
||||
fun getGalleryPage(
|
||||
suspend fun getGalleryPage(
|
||||
url: String?,
|
||||
gid: Long,
|
||||
token: String?
|
||||
@@ -1035,7 +1117,7 @@ object EhEngine {
|
||||
var headers: Headers? = null
|
||||
var code = -1
|
||||
try {
|
||||
call.execute().use { response ->
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
@@ -1050,7 +1132,7 @@ object EhEngine {
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Throwable::class)
|
||||
fun getGalleryPageApi(
|
||||
suspend fun getGalleryPageApi(
|
||||
gid: Long,
|
||||
index: Int,
|
||||
pToken: String?,
|
||||
@@ -1080,7 +1162,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()
|
||||
|
||||
@@ -39,7 +39,7 @@ object EhFilter {
|
||||
private val TAG = EhFilter::class.java.simpleName
|
||||
|
||||
init {
|
||||
val list = EhDB.getAllFilter()
|
||||
val list = EhDB.allFilter
|
||||
var i = 0
|
||||
val n = list.size
|
||||
while (i < n) {
|
||||
@@ -112,7 +112,7 @@ object EhFilter {
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun triggerFilter(filter: Filter?) {
|
||||
fun triggerFilter(filter: Filter) {
|
||||
EhDB.triggerFilter(filter)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,9 @@ import com.hippo.ehviewer.AppConfig
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.HashCodeUtils
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import com.hippo.yorozuya.copyToFile
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -37,41 +36,67 @@ import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
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 +105,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"
|
||||
@@ -170,22 +195,21 @@ object EhTagDatabase {
|
||||
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
|
||||
val request: Request = Request.Builder().url(url).build()
|
||||
val call = client.newCall(request)
|
||||
try {
|
||||
runCatching {
|
||||
call.execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return false
|
||||
}
|
||||
val body = response.body
|
||||
body.byteStream()
|
||||
.use { ins -> FileOutputStream(file).use { os -> IOUtils.copy(ins, os) } }
|
||||
response.body.use {
|
||||
it.copyToFile(file)
|
||||
}
|
||||
return true
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
FileUtils.delete(file)
|
||||
t.printStackTrace()
|
||||
ExceptionUtils.throwIfFatal(t)
|
||||
return false
|
||||
}.onFailure {
|
||||
file.delete()
|
||||
it.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
|
||||
@@ -44,7 +44,9 @@ object EhUrl {
|
||||
const val URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login"
|
||||
const val URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00"
|
||||
const val URL_FORUMS = "https://forums.e-hentai.org/"
|
||||
const val URL_FUNDS = "https://e-hentai.org/exchange.php?t=gp"
|
||||
const val URL_FUNDS = HOST_E + "exchange.php?t=gp"
|
||||
const val URL_HOME = HOST_E + "home.php"
|
||||
const val URL_NEWS = HOST_E + "news.php"
|
||||
const val REFERER_EX = "https://$DOMAIN_EX"
|
||||
const val ORIGIN_EX = REFERER_EX
|
||||
const val REFERER_E = "https://$DOMAIN_E"
|
||||
|
||||
@@ -66,6 +66,9 @@ object EhUtils {
|
||||
)
|
||||
private val CATEGORY_STRINGS = CATEGORY_VALUES.entries.map { (k, v) -> v to k }
|
||||
|
||||
val isExHentai: Boolean
|
||||
get() = Settings.getGallerySite() == EhUrl.SITE_EX
|
||||
|
||||
@JvmStatic
|
||||
fun getCategory(type: String?): Int {
|
||||
for (entry in CATEGORY_STRINGS) {
|
||||
@@ -102,6 +105,7 @@ object EhUtils {
|
||||
Settings.putDisplayName(null)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
Settings.putNeedSignIn(true)
|
||||
Settings.putSelectSite(true)
|
||||
}
|
||||
|
||||
fun needSignedIn(): Boolean {
|
||||
@@ -109,12 +113,12 @@ object EhUtils {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getSuitableTitle(gi: GalleryInfo): String? {
|
||||
fun getSuitableTitle(gi: GalleryInfo): String {
|
||||
return if (Settings.getShowJpnTitle()) {
|
||||
if (TextUtils.isEmpty(gi.titleJpn)) gi.title else gi.titleJpn
|
||||
} else {
|
||||
if (TextUtils.isEmpty(gi.title)) gi.titleJpn else gi.title
|
||||
}
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
fun extractTitle(fullTitle: String?): String? {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANGS
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -70,6 +72,33 @@ class GalleryDetail(
|
||||
@JvmField
|
||||
var previewPages: Int = 0,
|
||||
|
||||
@IgnoredOnParcel
|
||||
@JvmField
|
||||
var previewSet: PreviewSet? = null,
|
||||
) : AbstractGalleryInfo by galleryInfo, GalleryInfo
|
||||
) : AbstractGalleryInfo by galleryInfo, GalleryInfo {
|
||||
override fun generateSLang() {
|
||||
val index = LANGUAGES.indexOf(language)
|
||||
if (index != -1) {
|
||||
simpleLanguage = S_LANGS[index]
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LANGUAGES = arrayOf(
|
||||
"English",
|
||||
"Chinese",
|
||||
"Spanish",
|
||||
"Korean",
|
||||
"Russian",
|
||||
"French",
|
||||
"Portuguese",
|
||||
"Thai",
|
||||
"German",
|
||||
"Italian",
|
||||
"Vietnamese",
|
||||
"Polish",
|
||||
"Hungarian",
|
||||
"Dutch"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,6 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class GalleryPreview(
|
||||
@JvmField
|
||||
var imageKey: String? = null,
|
||||
@JvmField
|
||||
var imageUrl: String? = null,
|
||||
@JvmField
|
||||
@@ -40,6 +38,6 @@ class GalleryPreview(
|
||||
) : Parcelable {
|
||||
fun load(view: LoadImageView) {
|
||||
view.setClip(offsetX, offsetY, clipWidth, clipHeight)
|
||||
view.load(imageKey!!, imageUrl!!)
|
||||
view.load(imageUrl!!)
|
||||
}
|
||||
}
|
||||
@@ -23,16 +23,4 @@ class GalleryTagGroup(
|
||||
private val mTagList: ArrayList<String> = arrayListOf(),
|
||||
@JvmField
|
||||
var groupName: String? = null
|
||||
) : Parcelable {
|
||||
fun addTag(tag: String) {
|
||||
mTagList.add(tag)
|
||||
}
|
||||
|
||||
fun size(): Int {
|
||||
return mTagList.size
|
||||
}
|
||||
|
||||
fun getTagAt(position: Int): String {
|
||||
return mTagList[position]
|
||||
}
|
||||
}
|
||||
) : Parcelable, MutableList<String> by mTagList
|
||||
@@ -15,12 +15,9 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory
|
||||
import com.hippo.widget.LoadImageView
|
||||
import com.hippo.yorozuya.collect.IntList
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class LargePreviewSet(
|
||||
private val mPositionList: IntList = IntList(),
|
||||
private val mImageUrlList: ArrayList<String> = ArrayList(),
|
||||
@@ -48,7 +45,6 @@ class LargePreviewSet(
|
||||
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
|
||||
val galleryPreview = GalleryPreview()
|
||||
galleryPreview.position = mPositionList[index]
|
||||
galleryPreview.imageKey = EhCacheKeyFactory.getLargePreviewKey(gid, galleryPreview.position)
|
||||
galleryPreview.imageUrl = mImageUrlList[index]
|
||||
galleryPreview.pageUrl = mPageUrlList[index]
|
||||
return galleryPreview
|
||||
@@ -57,7 +53,6 @@ class LargePreviewSet(
|
||||
override fun load(view: LoadImageView, gid: Long, index: Int) {
|
||||
view.resetClip()
|
||||
view.load(
|
||||
EhCacheKeyFactory.getLargePreviewKey(gid, mPositionList[index]),
|
||||
mImageUrlList[index]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -283,13 +283,6 @@ data class ListUrlBuilder(
|
||||
if (this.category != EhUtils.NONE) {
|
||||
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
|
||||
}
|
||||
// Search key
|
||||
mKeyword?.run {
|
||||
val keyword = trim { it <= ' ' }
|
||||
if (keyword.isNotEmpty()) {
|
||||
ub.addQuery("f_search", encodeUTF8(this))
|
||||
}
|
||||
}
|
||||
mSHash?.let {
|
||||
ub.addQuery("f_shash", it)
|
||||
}
|
||||
@@ -302,6 +295,19 @@ data class ListUrlBuilder(
|
||||
mNext?.let {
|
||||
ub.addQuery("next", it)
|
||||
}
|
||||
// Search key
|
||||
// the settings of ub:UrlBuilder may be overwritten by following Advance search
|
||||
StringUtils.split(mKeyword, '|')?.forEachIndexed { idx, keyword ->
|
||||
val keyword = keyword.trim { it <= ' ' }
|
||||
when (idx) {
|
||||
0 -> keyword.takeIf { it.isNotEmpty() }?.let { ub.addQuery("f_search", encodeUTF8(it)) }
|
||||
else -> keyword.indexOf(':').takeIf { it >= 0 }?.run {
|
||||
val key = keyword.substring(0, this).trim { it <= ' ' }
|
||||
val value = keyword.substring(this + 1).trim { it <= ' ' }
|
||||
ub.addQuery(key, encodeUTF8(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Advance search
|
||||
if (advanceSearch != -1) {
|
||||
ub.addQuery("advsearch", "1")
|
||||
|
||||
@@ -17,12 +17,9 @@ package com.hippo.ehviewer.client.data
|
||||
|
||||
import com.hippo.widget.LoadImageView
|
||||
import com.hippo.yorozuya.collect.IntList
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class NormalPreviewSet(
|
||||
private var mPositionList: IntList = IntList(),
|
||||
private var mImageKeyList: ArrayList<String> = ArrayList(),
|
||||
private var mImageUrlList: ArrayList<String> = ArrayList(),
|
||||
private var mOffsetXList: IntList = IntList(),
|
||||
private var mOffsetYList: IntList = IntList(),
|
||||
@@ -31,21 +28,11 @@ class NormalPreviewSet(
|
||||
private var mPageUrlList: ArrayList<String> = ArrayList()
|
||||
) : PreviewSet() {
|
||||
|
||||
private fun getImageKey(imageUrl: String): String {
|
||||
val index = imageUrl.indexOf('/')
|
||||
return if (index >= 0) {
|
||||
imageUrl.substring(index + 1)
|
||||
} else {
|
||||
imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
fun addItem(
|
||||
position: Int, imageUrl: String, xOffset: Int, yOffset: Int, width: Int,
|
||||
height: Int, pageUrl: String
|
||||
) {
|
||||
mPositionList.add(position)
|
||||
mImageKeyList.add(getImageKey(imageUrl))
|
||||
mImageUrlList.add(imageUrl)
|
||||
mOffsetXList.add(xOffset)
|
||||
mOffsetYList.add(yOffset)
|
||||
@@ -55,7 +42,7 @@ class NormalPreviewSet(
|
||||
}
|
||||
|
||||
override fun size(): Int {
|
||||
return mPositionList.size()
|
||||
return mPositionList.size
|
||||
}
|
||||
|
||||
override fun getPosition(index: Int): Int {
|
||||
@@ -69,7 +56,6 @@ class NormalPreviewSet(
|
||||
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
|
||||
val galleryPreview = GalleryPreview()
|
||||
galleryPreview.position = mPositionList[index]
|
||||
galleryPreview.imageKey = mImageKeyList[index]
|
||||
galleryPreview.imageUrl = mImageUrlList[index]
|
||||
galleryPreview.pageUrl = mPageUrlList[index]
|
||||
galleryPreview.offsetX = mOffsetXList[index]
|
||||
@@ -84,6 +70,6 @@ class NormalPreviewSet(
|
||||
mOffsetXList[index], mOffsetYList[index],
|
||||
mClipWidthList[index], mClipHeightList[index]
|
||||
)
|
||||
view.load(mImageKeyList[index], mImageUrlList[index])
|
||||
view.load(mImageUrlList[index])
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,9 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.hippo.widget.LoadImageView
|
||||
|
||||
abstract class PreviewSet : Parcelable {
|
||||
abstract class PreviewSet {
|
||||
abstract fun size(): Int
|
||||
abstract fun getPosition(index: Int): Int
|
||||
abstract fun getPageUrlAt(index: Int): String
|
||||
|
||||
@@ -15,113 +15,71 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||
import org.jsoup.Jsoup
|
||||
import java.util.function.IntFunction
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object ArchiveParser {
|
||||
private val PATTERN_ARCHIVE_URL =
|
||||
Pattern.compile("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
|
||||
Regex("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
|
||||
private val PATTERN_CURRENT_FUNDS =
|
||||
Pattern.compile("<p>([\\d,]+) GP \\[[^]]*] ([\\d,]+) Credits \\[[^]]*]</p>")
|
||||
private val PATTERN_FUNDS =
|
||||
Pattern.compile("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", Pattern.DOTALL)
|
||||
Regex("<p>([\\d,]+) GP \\[[^]]*] ([\\d,]+) Credits \\[[^]]*]</p>")
|
||||
private val PATTERN_HATH_FORM =
|
||||
Pattern.compile("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
|
||||
Regex("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
|
||||
private val PATTERN_HATH_ARCHIVE =
|
||||
Pattern.compile("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
|
||||
Regex("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
|
||||
private val PATTERN_NEED_HATH_CLIENT =
|
||||
Pattern.compile("You must have a H@H client assigned to your account to use this feature\\.")
|
||||
Regex("You must have a H@H client assigned to your account to use this feature\\.")
|
||||
|
||||
fun parse(body: String): Result? {
|
||||
var m = PATTERN_HATH_FORM.matcher(body)
|
||||
if (!m.find()) {
|
||||
return null
|
||||
}
|
||||
val paramOr = m.group(1)
|
||||
val m = PATTERN_HATH_FORM.find(body) ?: return null
|
||||
val paramOr = m.groupValues[1]
|
||||
val archiveList = ArrayList<Archive>()
|
||||
val d = Jsoup.parse(body)
|
||||
val es = d.select("#db>div>div")
|
||||
for (e in es) {
|
||||
if (e.childrenSize() > 0 && !e.attr("style").contains("color:#CCCCCC")) {
|
||||
try {
|
||||
val res = e.selectFirst("form>input")!!.attr("value")
|
||||
val name = e.selectFirst("form>div>input")!!.attr("value")
|
||||
val size = e.selectFirst("p>strong")!!.text()
|
||||
val cost = e.selectFirst("div>strong")!!.text().replace(",", "")
|
||||
val item = Archive(res, name, size, cost, false)
|
||||
archiveList.add(item)
|
||||
} catch (ex: NullPointerException) {
|
||||
ex.printStackTrace()
|
||||
Jsoup.parse(body).select("#db>div>div").forEach { element ->
|
||||
if (element.childrenSize() > 0 && !element.attr("style").contains("color:#CCCCCC")) {
|
||||
runCatching {
|
||||
val res = element.selectFirst("form>input")!!.attr("value")
|
||||
val name = element.selectFirst("form>div>input")!!.attr("value")
|
||||
val size = element.selectFirst("p>strong")!!.text()
|
||||
val cost = element.selectFirst("div>strong")!!.text().replace(",", "")
|
||||
Archive(res, name, size, cost, false)
|
||||
}.onSuccess {
|
||||
archiveList.add(it)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
m = PATTERN_HATH_ARCHIVE.matcher(body)
|
||||
while (m.find()) {
|
||||
val res = ParserUtils.trim(m.group(1))
|
||||
val name = ParserUtils.trim(m.group(2))
|
||||
val size = ParserUtils.trim(m.group(3))
|
||||
val cost = ParserUtils.trim(m.group(4))
|
||||
PATTERN_HATH_ARCHIVE.findAll(body).forEach { matchResult ->
|
||||
val (res, name, size, cost) = matchResult.groupValues.slice(1..4)
|
||||
.map { ParserUtils.trim(it) }
|
||||
val item = Archive(res, name, size, cost, true)
|
||||
archiveList.add(item)
|
||||
}
|
||||
var funds: Funds? = null
|
||||
m = PATTERN_CURRENT_FUNDS.matcher(body)
|
||||
if (m.find()) {
|
||||
val fundsGP = ParserUtils.parseInt(m.group(1), 0)
|
||||
val fundsC = ParserUtils.parseInt(m.group(2), 0)
|
||||
funds = Funds(fundsGP, fundsC)
|
||||
val result = Result(paramOr, archiveList, null)
|
||||
PATTERN_CURRENT_FUNDS.find(body)?.groupValues?.run {
|
||||
val fundsGP = ParserUtils.parseInt(get(1), 0)
|
||||
val fundsC = ParserUtils.parseInt(get(2), 0)
|
||||
val funds = HomeParser.Funds(fundsGP, fundsC)
|
||||
result.funds = funds
|
||||
}
|
||||
return Result(paramOr, archiveList, funds)
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(NoHAtHClientException::class)
|
||||
fun parseArchiveUrl(body: String): String? {
|
||||
val m = PATTERN_NEED_HATH_CLIENT.matcher(body)
|
||||
if (m.find()) {
|
||||
throw NoHAtHClientException("No H@H client")
|
||||
}
|
||||
val d = Jsoup.parse(body)
|
||||
val a = d.selectFirst("#continue>a[href]")
|
||||
return if (a != null) {
|
||||
a.attr("href") + "?start=1"
|
||||
} else null
|
||||
|
||||
if (PATTERN_NEED_HATH_CLIENT.containsMatchIn(body)) throw NoHAtHClientException("No H@H client")
|
||||
return Jsoup.parse(body).selectFirst("#continue>a[href]")
|
||||
?.let { it.attr("href") + "?start=1" }
|
||||
// TODO: Check more errors
|
||||
}
|
||||
|
||||
fun parseFunds(body: String): Funds? {
|
||||
val m = PATTERN_FUNDS.matcher(body)
|
||||
return if (m.find()) {
|
||||
val fundsC = ParserUtils.parseInt(m.group(1), 0)
|
||||
val fundsGP = ParserUtils.parseInt(m.group(2), 0) * 1000
|
||||
return Funds(fundsGP, fundsC)
|
||||
} else null
|
||||
}
|
||||
|
||||
class Archive(
|
||||
val res: String,
|
||||
val name: String,
|
||||
val size: String,
|
||||
val cost: String,
|
||||
val isHAtH: Boolean
|
||||
) {
|
||||
fun format(getString: IntFunction<String?>): String {
|
||||
return if (isHAtH) {
|
||||
val costStr = if (cost == "Free") getString.apply(R.string.archive_free) else cost
|
||||
"[H@H] $name [$size] [$costStr]"
|
||||
} else {
|
||||
val nameStr =
|
||||
getString.apply(if (res == "org") R.string.archive_original else R.string.archive_resample)
|
||||
val costStr = if (cost == "Free!") getString.apply(R.string.archive_free) else cost
|
||||
"$nameStr [$size] [$costStr]"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
class Funds(var fundsGP: Int, var fundsC: Int)
|
||||
|
||||
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: Funds?)
|
||||
}
|
||||
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: HomeParser.Funds?)
|
||||
}
|
||||
@@ -1,45 +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.parser;
|
||||
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
|
||||
public class EventPaneParser {
|
||||
|
||||
public static String parse(String body) {
|
||||
String event = null;
|
||||
try {
|
||||
Document d = Jsoup.parse(body);
|
||||
Element eventpane = d.getElementById("eventpane");
|
||||
if (eventpane != null) {
|
||||
event = eventpane.html();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+5
-16
@@ -15,23 +15,12 @@
|
||||
* 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
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
@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)
|
||||
object EventPaneParser {
|
||||
fun parse(body: String): String? {
|
||||
return Jsoup.parse(body).getElementById("eventpane")?.html()
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import com.hippo.ehviewer.client.exception.EhException;
|
||||
import com.hippo.ehviewer.client.exception.ParseException;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.util.JsoupUtils;
|
||||
import com.hippo.yorozuya.AssertUtils;
|
||||
import com.hippo.yorozuya.AssertUtilsKt;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
@@ -46,7 +46,7 @@ public class FavoritesParser {
|
||||
//noinspection ConstantConditions
|
||||
Elements fps = ido.getElementsByClass("fp");
|
||||
// Last one is "fp fps"
|
||||
AssertUtils.assertEquals(11, fps.size());
|
||||
AssertUtilsKt.assertEquals(11, fps.size());
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Element fp = fps.get(i);
|
||||
|
||||
@@ -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,572 @@
|
||||
/*
|
||||
* 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.trimAnd
|
||||
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()
|
||||
// Temporary workaround, see https://github.com/jhy/jsoup/issues/1850
|
||||
val document = Jsoup.parse(body.replace("del>", "s>"))
|
||||
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 = name trimAnd { 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")
|
||||
val temp = c3!!.ownText()
|
||||
val time = if (temp.endsWith(':')) {
|
||||
// user
|
||||
comment.user = c3.child(0).text()
|
||||
temp.substring("Posted on ".length, temp.length - " by:".length)
|
||||
} else {
|
||||
temp.substring("Posted on ".length)
|
||||
}
|
||||
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).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(e.text())).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)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil.disk.DiskCache
|
||||
|
||||
inline fun DiskCache.edit(key: String, block: DiskCache.Editor.() -> Unit): Boolean {
|
||||
val editor = edit(key) ?: return false
|
||||
editor.runCatching {
|
||||
block(this)
|
||||
}.onFailure {
|
||||
editor.abort()
|
||||
throw it
|
||||
}.onSuccess {
|
||||
editor.commit()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
inline fun DiskCache.read(key: String, block: DiskCache.Snapshot.() -> Unit): Boolean {
|
||||
(get(key) ?: return false).use { block(it) }
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil.intercept.Interceptor
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.ImageResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
object MergeInterceptor : Interceptor {
|
||||
private val pendingContinuationMap: HashMap<String, MutableList<Continuation<Unit>>> = hashMapOf()
|
||||
private val pendingContinuationMapLock = Mutex()
|
||||
private val notifyScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val originRequest = chain.request
|
||||
val originListener = originRequest.listener
|
||||
val key = originRequest.data as String
|
||||
val newRequest = ImageRequest.Builder(originRequest).listener(
|
||||
{ originListener?.onStart(it) },
|
||||
{
|
||||
originListener?.onCancel(it)
|
||||
notifyScope.launch {
|
||||
pendingContinuationMapLock.withLock {
|
||||
// Wake up a pending continuation to continue executing task
|
||||
val successor = pendingContinuationMap[key]?.removeFirstOrNull()?.apply { resume(Unit) }
|
||||
// If no successor, delete this entry from hashmap
|
||||
successor ?: pendingContinuationMap.remove(key)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ request, result ->
|
||||
originListener?.onError(request, result)
|
||||
// Wake all pending continuations since this request is to be failed
|
||||
notifyScope.launch {
|
||||
pendingContinuationMapLock.withLock {
|
||||
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ request, result ->
|
||||
originListener?.onSuccess(request, result)
|
||||
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
|
||||
notifyScope.launch {
|
||||
pendingContinuationMapLock.withLock {
|
||||
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
|
||||
}
|
||||
}
|
||||
}
|
||||
).build()
|
||||
|
||||
pendingContinuationMapLock.lock()
|
||||
val existPendingContinuations = pendingContinuationMap[key]
|
||||
if (existPendingContinuations == null) {
|
||||
pendingContinuationMap[key] = mutableListOf()
|
||||
pendingContinuationMapLock.unlock()
|
||||
} else {
|
||||
existPendingContinuations.apply {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
add(continuation)
|
||||
pendingContinuationMapLock.unlock()
|
||||
continuation.invokeOnCancellation { remove(continuation) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return withContext(originRequest.interceptorDispatcher) { chain.proceed(newRequest) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.hippo.ehviewer.dailycheck
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.Html
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
|
||||
private val signedIn
|
||||
get() = EhApplication.ehCookieStore.hasSignedIn()
|
||||
|
||||
private const val CHANNEL_ID = "DailyCheckNotification"
|
||||
|
||||
class DailyCheckWork(val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
override suspend fun doWork(): Result = withIOContext {
|
||||
runCatching {
|
||||
if (signedIn) {
|
||||
EhEngine.getNews(true)?.let { showEventPane(it) }
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
return@withIOContext Result.retry()
|
||||
}
|
||||
Result.success()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun showEventPane(html: String) {
|
||||
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
|
||||
return
|
||||
}
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
val chan = NotificationChannelCompat
|
||||
.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(CHANNEL_ID)
|
||||
.build()
|
||||
notificationManager.createNotificationChannel(chan)
|
||||
val msg = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_monochrome)
|
||||
.setContentText(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.build()
|
||||
runCatching {
|
||||
notificationManager.notify(1, msg)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val schedHour
|
||||
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_HOUR, -1).takeUnless { it == -1 }
|
||||
|
||||
val schedMinute
|
||||
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_MINUTE, -1).takeUnless { it == -1 }
|
||||
|
||||
private val whenToWork
|
||||
get() = LocalDateTime.now()
|
||||
.run { withHour(schedHour ?: hour) }
|
||||
.run { withMinute(schedMinute ?: minute) }
|
||||
|
||||
private val initialDelay
|
||||
get() = Duration.between(LocalDateTime.now(), whenToWork)
|
||||
.run { if (isNegative) plusDays(1) else this }
|
||||
|
||||
private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
return PeriodicWorkRequestBuilder<DailyCheckWork>(Duration.ofDays(1))
|
||||
.setConstraints(constraints)
|
||||
.setInitialDelay(initialDelay)
|
||||
.build()
|
||||
}
|
||||
|
||||
private const val workName = "DailyCheckWork"
|
||||
|
||||
fun updateDailyCheckWork(context: Context) {
|
||||
if (Settings.getRequestNews()) {
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
workName,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
getDailyCheckWorkRequest()
|
||||
)
|
||||
} else {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(workName)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -25,11 +25,11 @@ 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
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
@@ -37,11 +37,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
|
||||
@@ -66,7 +65,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
|
||||
.setName(getString(R.string.download_service))
|
||||
.build()
|
||||
)
|
||||
mDownloadManager = EhApplication.downloadManager
|
||||
mDownloadManager = DownloadManager
|
||||
mDownloadManager!!.setDownloadListener(this)
|
||||
ensureDownloadingBuilder()
|
||||
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
|
||||
@@ -99,13 +98,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 +122,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 +136,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 +532,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
|
||||
|
||||
@@ -1,351 +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.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Process;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.Native;
|
||||
import com.hippo.UriArchiveAccessor;
|
||||
import com.hippo.ehviewer.GetText;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.thread.PVLock;
|
||||
import com.hippo.yorozuya.thread.PriorityThread;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
|
||||
public class ArchivePageLoader extends PageLoader2 {
|
||||
private static final AtomicInteger sIdGenerator = new AtomicInteger();
|
||||
public static Handler showPasswd;
|
||||
public static String passwd;
|
||||
public static PVLock pv = new PVLock(0);
|
||||
private final UriArchiveAccessor archiveAccessor;
|
||||
private final Stack<Integer> requests = new Stack<>();
|
||||
private final LinkedHashMap<Integer, Image.ByteBufferSource> streams = new LinkedHashMap<>();
|
||||
private final Thread[] decodeThread = new Thread[]{
|
||||
new Thread(new DecodeTask()),
|
||||
new Thread(new DecodeTask()),
|
||||
new Thread(new DecodeTask()),
|
||||
new Thread(new DecodeTask())
|
||||
};
|
||||
private Thread archiveThread;
|
||||
private volatile int size = PageLoader.STATE_WAIT;
|
||||
private String error;
|
||||
|
||||
public ArchivePageLoader(Context context, Uri uri) {
|
||||
UriArchiveAccessor archiveAccessor1;
|
||||
try {
|
||||
archiveAccessor1 = new UriArchiveAccessor(context, uri);
|
||||
} catch (Exception e) {
|
||||
archiveAccessor1 = null;
|
||||
e.printStackTrace();
|
||||
}
|
||||
archiveAccessor = archiveAccessor1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
int id = sIdGenerator.incrementAndGet();
|
||||
|
||||
archiveThread = new PriorityThread(
|
||||
new ArchiveHostTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
|
||||
archiveThread.start();
|
||||
for (Thread i : decodeThread) {
|
||||
i.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
if (archiveThread != null) {
|
||||
archiveThread.interrupt();
|
||||
archiveThread = null;
|
||||
}
|
||||
for (Thread i : decodeThread) {
|
||||
i.interrupt();
|
||||
}
|
||||
try {
|
||||
archiveAccessor.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRequest(int index) {
|
||||
boolean inDecodeTask;
|
||||
synchronized (streams) {
|
||||
inDecodeTask = streams.containsKey(index);
|
||||
}
|
||||
|
||||
synchronized (requests) {
|
||||
boolean inArchiveTask = requests.contains(index);
|
||||
if (!inArchiveTask && !inDecodeTask) {
|
||||
requests.add(index);
|
||||
requests.notify();
|
||||
}
|
||||
}
|
||||
notifyPageWait(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onForceRequest(int index) {
|
||||
onRequest(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelRequest(int index) {
|
||||
synchronized (requests) {
|
||||
requests.remove(Integer.valueOf(index));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilename(int index) {
|
||||
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilenameWithExtension(int index) {
|
||||
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean save(int index, @NonNull UniFile file) {
|
||||
int fd;
|
||||
FileOutputStream stream;
|
||||
try {
|
||||
stream = (FileOutputStream) file.openOutputStream();
|
||||
fd = Native.getFd(stream.getFD());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
archiveAccessor.extractToFd(index, fd);
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
|
||||
String extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index));
|
||||
UniFile dst = dir.subFile(null != extension ? filename + "." + extension : filename);
|
||||
save(index, dst);
|
||||
return dst;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preloadPages(@NonNull List<Integer> pages) {
|
||||
|
||||
}
|
||||
|
||||
private class ArchiveHostTask implements Runnable {
|
||||
public final Thread[] archiveThreads = new Thread[]{
|
||||
new Thread(new ArchiveTask()),
|
||||
new Thread(new ArchiveTask()),
|
||||
new Thread(new ArchiveTask()),
|
||||
new Thread(new ArchiveTask())
|
||||
};
|
||||
|
||||
private void waitPasswd() throws InterruptedException {
|
||||
showPasswd.sendEmptyMessage(0);
|
||||
pv.p();
|
||||
}
|
||||
|
||||
private void notifyError() {
|
||||
showPasswd.sendEmptyMessage(1);
|
||||
}
|
||||
|
||||
private void notifyDismiss() {
|
||||
showPasswd.sendEmptyMessage(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
size = archiveAccessor.open();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
size = 0;
|
||||
}
|
||||
if (size <= 0) {
|
||||
size = PageLoader.STATE_ERROR;
|
||||
error = GetText.getString(R.string.error_reading_failed);
|
||||
notifyDataChanged();
|
||||
return;
|
||||
}
|
||||
// Update size and notify changed
|
||||
notifyDataChanged();
|
||||
|
||||
if (archiveAccessor.needPassword()) {
|
||||
boolean need_request = true;
|
||||
Set<String> set = Settings.getArchivePasswds();
|
||||
if (set != null) {
|
||||
for (String passwd : set) {
|
||||
if (archiveAccessor.providePassword(passwd)) {
|
||||
need_request = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!Thread.currentThread().isInterrupted() && need_request) {
|
||||
try {
|
||||
waitPasswd();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
if (!archiveAccessor.providePassword(passwd))
|
||||
notifyError();
|
||||
else {
|
||||
Settings.putPasswdToArchivePasswds(passwd);
|
||||
notifyDismiss();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Thread i : archiveThreads) {
|
||||
i.start();
|
||||
}
|
||||
|
||||
Object o = new Object();
|
||||
synchronized (o) {
|
||||
try {
|
||||
o.wait();
|
||||
} catch (InterruptedException e) {
|
||||
for (Thread i : archiveThreads) {
|
||||
i.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ArchiveTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int index;
|
||||
synchronized (requests) {
|
||||
if (requests.isEmpty()) {
|
||||
try {
|
||||
requests.wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Interrupted
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
index = requests.pop();
|
||||
}
|
||||
|
||||
// Check index valid
|
||||
if (index < 0 || index >= size) {
|
||||
notifyPageFailed(index);
|
||||
continue;
|
||||
}
|
||||
|
||||
synchronized (streams) {
|
||||
if (streams.get(index) != null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Image.ByteBufferSource src = archiveAccessor.getImageSource(index);
|
||||
|
||||
synchronized (streams) {
|
||||
streams.put(index, src);
|
||||
streams.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DecodeTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int index;
|
||||
Image.ByteBufferSource src;
|
||||
synchronized (streams) {
|
||||
if (streams.isEmpty()) {
|
||||
try {
|
||||
streams.wait();
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Iterator<Map.Entry<Integer, Image.ByteBufferSource>> iterator = streams.entrySet().iterator();
|
||||
Map.Entry<Integer, Image.ByteBufferSource> entry = iterator.next();
|
||||
iterator.remove();
|
||||
index = entry.getKey();
|
||||
src = entry.getValue();
|
||||
}
|
||||
|
||||
Image image = null;
|
||||
if (src != null) {
|
||||
image = Image.decode(src);
|
||||
}
|
||||
if (image != null) {
|
||||
notifyPageSucceed(index, image);
|
||||
} else {
|
||||
notifyPageFailed(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2023 Tarsin Norbin
|
||||
*
|
||||
* 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.gallery
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.hippo.Native.getFd
|
||||
import com.hippo.UriArchiveAccessor
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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 java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
class ArchivePageLoader(context: Context, uri: Uri, passwdFlow: Flow<String>) : PageLoader2(),
|
||||
CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
private val archiveAccessor by lazy { UriArchiveAccessor(context, uri) }
|
||||
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
||||
size = archiveAccessor.open()
|
||||
if (size == 0) {
|
||||
return@launch
|
||||
}
|
||||
archiveAccessor.run {
|
||||
if (needPassword()) {
|
||||
Settings.getArchivePasswds()?.forEach {
|
||||
if (providePassword(it)) return@launch
|
||||
}
|
||||
passwdFlow.collect {
|
||||
if (providePassword(it)) {
|
||||
Settings.putPasswdToArchivePasswds(it)
|
||||
currentCoroutineContext().cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var size = 0
|
||||
private set
|
||||
|
||||
override fun start() {
|
||||
hostJob.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
cancel()
|
||||
archiveAccessor.close()
|
||||
super.stop()
|
||||
}
|
||||
|
||||
private val mJobMap = hashMapOf<Int, Job>()
|
||||
private val mWorkerMutex by lazy { (0 until size).map { Mutex() } }
|
||||
private val mSemaphore = Semaphore(4)
|
||||
|
||||
override fun onRequest(index: Int) {
|
||||
synchronized(mJobMap) {
|
||||
val current = mJobMap[index]
|
||||
if (current?.isActive != true) {
|
||||
mJobMap[index] = launch {
|
||||
mWorkerMutex[index].withLock {
|
||||
mSemaphore.withPermit {
|
||||
doRealWork(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doRealWork(index: Int) {
|
||||
val src = archiveAccessor.getImageSource(index) ?: return
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
src.close()
|
||||
throw it
|
||||
}
|
||||
val image = Image.decode(src) ?: return notifyPageFailed(index, null)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image.recycle()
|
||||
throw it
|
||||
}
|
||||
notifyPageSucceed(index, image)
|
||||
}
|
||||
|
||||
override fun onForceRequest(index: Int) {
|
||||
onRequest(index)
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
hostJob.join()
|
||||
return size != 0
|
||||
}
|
||||
|
||||
override val isReady: Boolean
|
||||
get() = size != 0
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
mJobMap[index]?.cancel()
|
||||
}
|
||||
|
||||
override fun getImageFilename(index: Int): String {
|
||||
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index))
|
||||
}
|
||||
|
||||
override fun getImageFilenameWithExtension(index: Int): String {
|
||||
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index))
|
||||
}
|
||||
|
||||
override fun save(index: Int, file: UniFile): Boolean {
|
||||
val fd: Int
|
||||
val stream: FileOutputStream
|
||||
try {
|
||||
stream = file.openOutputStream() as FileOutputStream
|
||||
fd = getFd(stream.fd)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
archiveAccessor.extractToFd(index, fd)
|
||||
try {
|
||||
stream.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun save(index: Int, dir: UniFile, filename: String): UniFile {
|
||||
val extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index))
|
||||
val dst = dir.subFile(if (null != extension) "$filename.$extension" else filename)
|
||||
save(index, dst!!)
|
||||
return dst
|
||||
}
|
||||
|
||||
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
|
||||
}
|
||||
@@ -1,234 +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.gallery;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.spider.SpiderQueen;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.SimpleHandler;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
|
||||
public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderListener {
|
||||
|
||||
private final GalleryInfo mGalleryInfo;
|
||||
@Nullable
|
||||
private SpiderQueen mSpiderQueen;
|
||||
|
||||
public EhPageLoader(GalleryInfo galleryInfo) {
|
||||
mGalleryInfo = galleryInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mSpiderQueen = SpiderQueen.obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ);
|
||||
mSpiderQueen.addOnSpiderListener(this);
|
||||
if (mSpiderQueen.size() > 0)
|
||||
notifyDataChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.removeOnSpiderListener(this);
|
||||
// Activity recreate may called, so wait 3000s
|
||||
SimpleHandler.getInstance().postDelayed(new ReleaseTask(mSpiderQueen), 3000);
|
||||
mSpiderQueen = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStartPage() {
|
||||
if (mSpiderQueen != null) {
|
||||
return mSpiderQueen.getStartPage();
|
||||
} else {
|
||||
return super.getStartPage();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilename(int index) {
|
||||
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilenameWithExtension(int index) {
|
||||
if (null != mSpiderQueen) {
|
||||
String extension = mSpiderQueen.getExtension(index);
|
||||
if (extension != null) {
|
||||
return String.format(Locale.US, "%d-%s-%08d.%s", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1, extension);
|
||||
}
|
||||
}
|
||||
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean save(int index, @NonNull UniFile file) {
|
||||
if (null != mSpiderQueen) {
|
||||
return mSpiderQueen.save(index, file);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
|
||||
if (null != mSpiderQueen) {
|
||||
return mSpiderQueen.save(index, dir, filename);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putStartPage(int page) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.putStartPage(page);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
if (mSpiderQueen != null) {
|
||||
return mSpiderQueen.size();
|
||||
} else {
|
||||
return PageLoader.STATE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelRequest(int index) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.cancelRequest(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getError() {
|
||||
if (mSpiderQueen != null) {
|
||||
return mSpiderQueen.getError();
|
||||
} else {
|
||||
return "Error"; // TODO
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetPages(int pages) {
|
||||
notifyDataChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGet509(int index) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageDownload(int index, long contentLength, long receivedSize, int bytesRead) {
|
||||
if (contentLength > 0) {
|
||||
notifyPagePercent(index, (float) receivedSize / contentLength);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSuccess(int index, int finished, int downloaded, int total) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.request(index, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFailure(int index, String error, int finished, int downloaded, int total) {
|
||||
notifyPageFailed(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish(int finished, int downloaded, int total) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetImageSuccess(int index, Image image) {
|
||||
notifyPageSucceed(index, image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetImageFailure(int index, String error) {
|
||||
notifyPageFailed(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preloadPages(@NonNull List<Integer> pages) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.preloadPages(pages);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReleaseTask implements Runnable {
|
||||
|
||||
private SpiderQueen mSpiderQueen;
|
||||
|
||||
public ReleaseTask(SpiderQueen spiderQueen) {
|
||||
mSpiderQueen = spiderQueen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (null != mSpiderQueen) {
|
||||
SpiderQueen.releaseSpiderQueen(mSpiderQueen, SpiderQueen.MODE_READ);
|
||||
mSpiderQueen = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.gallery
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.yorozuya.SimpleHandler
|
||||
import java.util.Locale
|
||||
|
||||
class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpiderListener {
|
||||
private lateinit var mSpiderQueen: SpiderQueen
|
||||
override fun start() {
|
||||
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
|
||||
mSpiderQueen.addOnSpiderListener(this)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
mSpiderQueen.removeOnSpiderListener(this)
|
||||
// Activity recreate may called, so wait 3000s
|
||||
SimpleHandler.getInstance().postDelayed(ReleaseTask(mSpiderQueen), 3000)
|
||||
}
|
||||
|
||||
override val startPage
|
||||
get() = mSpiderQueen.startPage
|
||||
|
||||
override fun getImageFilename(index: Int): String {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"%d-%s-%08d",
|
||||
mGalleryInfo.gid,
|
||||
mGalleryInfo.token,
|
||||
index + 1
|
||||
)
|
||||
}
|
||||
|
||||
override fun getImageFilenameWithExtension(index: Int): String {
|
||||
val extension = mSpiderQueen.getExtension(index)
|
||||
if (extension != null) {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"%d-%s-%08d.%s",
|
||||
mGalleryInfo.gid,
|
||||
mGalleryInfo.token,
|
||||
index + 1,
|
||||
extension
|
||||
)
|
||||
}
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"%d-%s-%08d",
|
||||
mGalleryInfo.gid,
|
||||
mGalleryInfo.token,
|
||||
index + 1
|
||||
)
|
||||
}
|
||||
|
||||
override fun save(index: Int, file: UniFile): Boolean {
|
||||
return mSpiderQueen.save(index, file)
|
||||
}
|
||||
|
||||
override fun save(index: Int, dir: UniFile, filename: String): UniFile? {
|
||||
return mSpiderQueen.save(index, dir, filename)
|
||||
}
|
||||
|
||||
override fun putStartPage(page: Int) {
|
||||
mSpiderQueen.putStartPage(page)
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = mSpiderQueen.size
|
||||
|
||||
override fun onRequest(index: Int) {
|
||||
mSpiderQueen.request(index)
|
||||
}
|
||||
|
||||
override fun onForceRequest(index: Int) {
|
||||
mSpiderQueen.forceRequest(index)
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
return mSpiderQueen.awaitReady()
|
||||
}
|
||||
|
||||
override val isReady: Boolean
|
||||
get() = mSpiderQueen.isReady
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
mSpiderQueen.cancelRequest(index)
|
||||
}
|
||||
|
||||
override fun onGetPages(pages: Int) {}
|
||||
|
||||
override fun onGet509(index: Int) {}
|
||||
|
||||
override fun onPageDownload(
|
||||
index: Int,
|
||||
contentLength: Long,
|
||||
receivedSize: Long,
|
||||
bytesRead: Int
|
||||
) {
|
||||
if (contentLength > 0) {
|
||||
notifyPagePercent(index, receivedSize.toFloat() / contentLength)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}
|
||||
override fun onPageFailure(
|
||||
index: Int,
|
||||
error: String?,
|
||||
finished: Int,
|
||||
downloaded: Int,
|
||||
total: Int
|
||||
) {
|
||||
notifyPageFailed(index, error)
|
||||
}
|
||||
|
||||
override fun onFinish(finished: Int, downloaded: Int, total: Int) {}
|
||||
override fun onGetImageSuccess(index: Int, image: Image?) {
|
||||
notifyPageSucceed(index, image!!)
|
||||
}
|
||||
|
||||
override fun onGetImageFailure(index: Int, error: String?) {
|
||||
notifyPageFailed(index, error)
|
||||
}
|
||||
|
||||
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
|
||||
mSpiderQueen.preloadPages(pages, pair)
|
||||
}
|
||||
|
||||
private class ReleaseTask(private var mSpiderQueen: SpiderQueen?) : Runnable {
|
||||
override fun run() {
|
||||
mSpiderQueen?.let { releaseSpiderQueen(it, SpiderQueen.MODE_READ) }
|
||||
mSpiderQueen = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +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.gallery;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.unifile.UniFile;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
public int getStartPage() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void putStartPage(int page) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return without extension
|
||||
*/
|
||||
@NonNull
|
||||
public abstract String getImageFilename(int index);
|
||||
|
||||
/**
|
||||
* @return with extension
|
||||
*/
|
||||
@NonNull
|
||||
public abstract String getImageFilenameWithExtension(int index);
|
||||
|
||||
public abstract boolean save(int index, @NonNull UniFile file);
|
||||
|
||||
/**
|
||||
* @param filename without extension
|
||||
*/
|
||||
@Nullable
|
||||
public abstract UniFile save(int index, @NonNull UniFile dir, @NonNull String filename);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.gallery
|
||||
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
|
||||
abstract class PageLoader2 : PageLoader() {
|
||||
open val startPage: Int
|
||||
get() = 0
|
||||
|
||||
open fun putStartPage(page: Int) {}
|
||||
|
||||
/**
|
||||
* @return without extension
|
||||
*/
|
||||
abstract fun getImageFilename(index: Int): String
|
||||
|
||||
/**
|
||||
* @return with extension
|
||||
*/
|
||||
abstract fun getImageFilenameWithExtension(index: Int): String
|
||||
abstract fun save(index: Int, file: UniFile): Boolean
|
||||
|
||||
/**
|
||||
* @param filename without extension
|
||||
*/
|
||||
abstract fun save(index: Int, dir: UniFile, filename: String): UniFile?
|
||||
}
|
||||
|
||||
val SUPPORT_IMAGE_EXTENSIONS = arrayOf(
|
||||
".jpg", // Joint Photographic Experts Group
|
||||
".png", // Portable Network Graphics
|
||||
".gif"
|
||||
)
|
||||
@@ -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(
|
||||
|
||||
@@ -1,118 +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.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.download.DownloadManager;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
public class CleanRedundancyPreference extends TaskPreference {
|
||||
|
||||
public CleanRedundancyPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CleanRedundancyPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CleanRedundancyPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Task onCreateTask() {
|
||||
return new ClearTask(getContext());
|
||||
}
|
||||
|
||||
private static class ClearTask extends Task {
|
||||
|
||||
private final DownloadManager mManager;
|
||||
|
||||
public ClearTask(@NonNull Context context) {
|
||||
super(context);
|
||||
mManager = EhApplication.getDownloadManager();
|
||||
}
|
||||
|
||||
// True for cleared
|
||||
private boolean clearFile(UniFile file) {
|
||||
String name = file.getName();
|
||||
if (name == null) {
|
||||
return false;
|
||||
}
|
||||
int index = name.indexOf('-');
|
||||
if (index >= 0) {
|
||||
name = name.substring(0, index);
|
||||
}
|
||||
long gid = NumberUtils.parseLongSafely(name, -1L);
|
||||
if (-1L == gid) {
|
||||
return false;
|
||||
}
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
return false;
|
||||
}
|
||||
file.delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doInBackground(Void... params) {
|
||||
UniFile dir = Settings.getDownloadLocation();
|
||||
if (null == dir) {
|
||||
return 0;
|
||||
}
|
||||
UniFile[] files = dir.listFiles();
|
||||
if (null == files) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (UniFile f : files) {
|
||||
if (clearFile(f)) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Object o) {
|
||||
int count;
|
||||
if (o instanceof Integer) {
|
||||
count = (Integer) o;
|
||||
} else {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
showTip(0 == count ?
|
||||
mApplication.getString(R.string.settings_download_clean_redundancy_no_redundancy) :
|
||||
mApplication.getString(R.string.settings_download_clean_redundancy_done, count), Snackbar.LENGTH_SHORT);
|
||||
super.onPostExecute(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private val NO_REDUNDANCY =
|
||||
GetText.getString(R.string.settings_download_clean_redundancy_no_redundancy)
|
||||
private val CLEAR_REDUNDANCY_DONE =
|
||||
{ cnt: Int -> GetText.getString(R.string.settings_download_clean_redundancy_done, cnt) }
|
||||
private val FINAL_CLEAR_REDUNDANCY_MSG =
|
||||
{ cnt: Int -> if (cnt == 0) NO_REDUNDANCY else CLEAR_REDUNDANCY_DONE(cnt) }
|
||||
|
||||
class CleanRedundancyPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : TaskPreference(context, attrs) {
|
||||
private fun clearFile(file: UniFile): Boolean {
|
||||
var name = file.name ?: return false
|
||||
val index = name.indexOf('-')
|
||||
if (index >= 0) {
|
||||
name = name.substring(0, index)
|
||||
}
|
||||
val gid = name.toLongOrNull() ?: return false
|
||||
if (downloadManager.containDownloadInfo(gid)) {
|
||||
return false
|
||||
}
|
||||
file.delete()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun doRealWork(): Int {
|
||||
return Settings.getDownloadLocation()?.listFiles()?.sumOf { clearFile(it).compareTo(false) } ?: 0
|
||||
}
|
||||
|
||||
override fun launchJob() {
|
||||
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
|
||||
launchUI {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
else singletonJob = launch {
|
||||
val cnt = doRealWork()
|
||||
withUIContext {
|
||||
showTip(FINAL_CLEAR_REDUNDANCY_MSG(cnt))
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var singletonJob: Job? = null
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,151 +15,111 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine.fillGalleryListByApi
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.spider.SpiderInfo
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.ehviewer.spider.readCompatFromUniFile
|
||||
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
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
class RestoreDownloadPreference constructor(
|
||||
context: Context, attrs: AttributeSet?
|
||||
private val RESTORE_NOT_FOUND = GetText.getString(R.string.settings_download_restore_not_found)
|
||||
private val RESTORE_FAILED = GetText.getString(R.string.settings_download_restore_failed)
|
||||
private val RESTORE_COUNT_MSG =
|
||||
{ cnt: Int -> if (cnt == 0) RESTORE_NOT_FOUND else GetText.getString(R.string.settings_download_restore_successfully, cnt) }
|
||||
|
||||
class RestoreDownloadPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : TaskPreference(context, attrs) {
|
||||
override fun onCreateTask(): Task {
|
||||
return RestoreTask(context)
|
||||
}
|
||||
|
||||
private class RestoreTask(context: Context) : Task(context) {
|
||||
private val mManager: DownloadManager = downloadManager
|
||||
private var restoreDirCount = 0
|
||||
private fun getRestoreItem(file: UniFile?): RestoreItem? {
|
||||
if (null == file || !file.isDirectory) {
|
||||
private val mManager: DownloadManager = downloadManager
|
||||
private var restoreDirCount = 0
|
||||
private fun getRestoreItem(file: UniFile?): RestoreItem? {
|
||||
if (null == file || !file.isDirectory) return null
|
||||
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
|
||||
return runCatching {
|
||||
val spiderInfo = readCompatFromUniFile(siFile) ?: return null
|
||||
var gid = spiderInfo.gid
|
||||
var dirname = file.name
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
// Restore download dir to avoid redownload
|
||||
val dbdirname = EhDB.getDownloadDirname(gid)
|
||||
if (null == dbdirname || dirname != dbdirname) {
|
||||
EhDB.putDownloadDirname(gid, dirname)
|
||||
restoreDirCount++
|
||||
}
|
||||
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 gid = spiderInfo.gid
|
||||
val dirname = file.name
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
// Restore download dir to avoid redownload
|
||||
val dbdirname = EhDB.getDownloadDirname(gid)
|
||||
if (null == dbdirname || dirname != dbdirname) {
|
||||
EhDB.putDownloadDirname(gid, dirname)
|
||||
restoreDirCount++
|
||||
}
|
||||
return null
|
||||
}
|
||||
val token = spiderInfo.token
|
||||
val restoreItem = RestoreItem()
|
||||
restoreItem.gid = gid
|
||||
restoreItem.token = token
|
||||
restoreItem.dirname = dirname
|
||||
restoreItem
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
} finally {
|
||||
IOUtils.closeQuietly(`is`)
|
||||
RestoreItem().apply {
|
||||
gid = spiderInfo.gid
|
||||
token = spiderInfo.token
|
||||
dirname = file.name
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg params: Void): Any? {
|
||||
val dir = Settings.getDownloadLocation() ?: return null
|
||||
val restoreItemList: MutableList<RestoreItem> = ArrayList()
|
||||
val files = dir.listFiles() ?: return null
|
||||
for (file in files) {
|
||||
val restoreItem = getRestoreItem(file)
|
||||
if (null != restoreItem) {
|
||||
restoreItemList.add(restoreItem)
|
||||
}
|
||||
}
|
||||
return if (0 == restoreItemList.size) {
|
||||
Collections.EMPTY_LIST
|
||||
} else try {
|
||||
runBlocking {
|
||||
fillGalleryListByApi(
|
||||
ArrayList(restoreItemList),
|
||||
EhUrl.referer
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostExecute(o: Any) {
|
||||
if (o !is List<*>) {
|
||||
mActivity.showTip(R.string.settings_download_restore_failed, BaseScene.LENGTH_SHORT)
|
||||
} else {
|
||||
val list = o as List<RestoreItem>
|
||||
if (list.isEmpty()) {
|
||||
mActivity.showTip(
|
||||
if (restoreDirCount == 0)
|
||||
mActivity.getString(R.string.settings_download_restore_not_found)
|
||||
else
|
||||
mActivity.getString(
|
||||
R.string.settings_download_restore_successfully,
|
||||
restoreDirCount
|
||||
),
|
||||
BaseScene.LENGTH_SHORT
|
||||
)
|
||||
} else {
|
||||
var count = 0
|
||||
var i = 0
|
||||
val n = list.size
|
||||
while (i < n) {
|
||||
val item = list[i]
|
||||
// Avoid failed gallery info
|
||||
if (null != item.title) {
|
||||
// Put to download
|
||||
mManager.addDownload(item, null)
|
||||
// Put download dir to DB
|
||||
EhDB.putDownloadDirname(item.gid, item.dirname)
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
showTip(
|
||||
mActivity.getString(
|
||||
R.string.settings_download_restore_successfully,
|
||||
count + restoreDirCount
|
||||
),
|
||||
BaseScene.LENGTH_SHORT
|
||||
)
|
||||
val preference: Preference? = preference
|
||||
if (null != preference) {
|
||||
val context = preference.context
|
||||
if (context is Activity) {
|
||||
context.setResult(Activity.RESULT_OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onPostExecute(o)
|
||||
}
|
||||
private suspend fun doRealWork(): List<GalleryInfo>? {
|
||||
val dir = Settings.getDownloadLocation() ?: return null
|
||||
val files = dir.listFiles() ?: return null
|
||||
val restoreItemList = files.mapNotNull { getRestoreItem(it) }
|
||||
return runSuspendCatching {
|
||||
fillGalleryListByApi(restoreItemList, EhUrl.referer)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private class RestoreItem : BaseGalleryInfo() {
|
||||
var dirname: String? = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun launchJob() {
|
||||
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
|
||||
launchUI {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
else singletonJob = launch {
|
||||
val result = doRealWork()
|
||||
withUIContext {
|
||||
if (result == null) {
|
||||
showTip(RESTORE_FAILED)
|
||||
} else {
|
||||
if (result.isEmpty()) {
|
||||
showTip(RESTORE_COUNT_MSG(restoreDirCount))
|
||||
} else {
|
||||
var count = 0
|
||||
var i = 0
|
||||
val n = result.size
|
||||
while (i < n) {
|
||||
val item = result[i]
|
||||
// Avoid failed gallery info
|
||||
if (null != item.title) {
|
||||
// Put to download
|
||||
mManager.addDownload(item, null)
|
||||
// Put download dir to DB
|
||||
EhDB.putDownloadDirname(item.gid, (item as RestoreItem).dirname)
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
showTip(RESTORE_COUNT_MSG(count + restoreDirCount))
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var singletonJob: Job? = null
|
||||
@@ -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,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.preference;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.customview.view.AbsSavedState;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.ui.SettingsActivity;
|
||||
import com.hippo.preference.DialogPreference;
|
||||
import com.hippo.util.IoThreadPoolExecutor;
|
||||
import com.hippo.yorozuya.IntIdGenerator;
|
||||
|
||||
public abstract class TaskPreference extends DialogPreference {
|
||||
|
||||
private Task mTask;
|
||||
private int mTaskId = IntIdGenerator.INVALID_ID;
|
||||
|
||||
public TaskPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public TaskPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TaskPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
builder.setTitle(null);
|
||||
builder.setView(R.layout.preference_dialog_task);
|
||||
builder.setCancelable(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogCreated(AlertDialog dialog) {
|
||||
if (null == mTask) {
|
||||
mTask = onCreateTask();
|
||||
mTask.setPreference(this);
|
||||
mTaskId = ((EhApplication) getContext().getApplicationContext()).putGlobalStuff(mTask);
|
||||
mTask.executeOnExecutor(IoThreadPoolExecutor.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
protected void onTaskEnd() {
|
||||
// Dismiss dialog
|
||||
Dialog dialog = getDialog();
|
||||
try {
|
||||
dialog.dismiss();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// Clear async
|
||||
mTask = null;
|
||||
mTaskId = IntIdGenerator.INVALID_ID;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected abstract Task onCreateTask();
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState myState = new SavedState(superState);
|
||||
myState.asyncTaskId = mTaskId;
|
||||
return myState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
if (state == null || !state.getClass().equals(SavedState.class)) {
|
||||
// Didn't save state for us in onSaveInstanceState
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
SavedState myState = (SavedState) state;
|
||||
mTaskId = myState.asyncTaskId;
|
||||
if (IntIdGenerator.INVALID_ID != mTaskId) {
|
||||
Object o = ((EhApplication) getContext().getApplicationContext()).getGlobalStuff(mTaskId);
|
||||
if (o instanceof Task) {
|
||||
mTask = (Task) o;
|
||||
mTask.setPreference(this);
|
||||
}
|
||||
}
|
||||
if (null == mTask) {
|
||||
mTaskId = IntIdGenerator.INVALID_ID;
|
||||
}
|
||||
|
||||
// TODO if not task, show not reopen dialog
|
||||
|
||||
super.onRestoreInstanceState(myState.getSuperState());
|
||||
}
|
||||
|
||||
private static class SavedState extends AbsSavedState {
|
||||
public static final Parcelable.Creator<SavedState> CREATOR =
|
||||
new Parcelable.Creator<SavedState>() {
|
||||
@Override
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
int asyncTaskId;
|
||||
|
||||
public SavedState(Parcel source) {
|
||||
super(source, SavedState.class.getClassLoader());
|
||||
asyncTaskId = source.readInt();
|
||||
}
|
||||
|
||||
public SavedState(Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(asyncTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class Task extends AsyncTask<Void, Void, Object> {
|
||||
|
||||
protected final EhApplication mApplication;
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
protected final SettingsActivity mActivity;
|
||||
@Nullable
|
||||
private TaskPreference mPreference;
|
||||
|
||||
public Task(@NonNull Context context) {
|
||||
mActivity = (SettingsActivity) context;
|
||||
mApplication = (EhApplication) context.getApplicationContext();
|
||||
}
|
||||
|
||||
public EhApplication getApplication() {
|
||||
return mApplication;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TaskPreference getPreference() {
|
||||
return mPreference;
|
||||
}
|
||||
|
||||
public void setPreference(@Nullable TaskPreference preference) {
|
||||
mPreference = preference;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onPostExecute(Object o) {
|
||||
mApplication.removeGlobalStuff(this);
|
||||
if (null != mPreference) {
|
||||
mPreference.onTaskEnd();
|
||||
}
|
||||
}
|
||||
|
||||
protected void showTip(@StringRes int id, int length) {
|
||||
try {
|
||||
mActivity.showTip(id, length);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(mApplication, id, length).show();
|
||||
}
|
||||
}
|
||||
|
||||
protected void showTip(CharSequence message, int length) {
|
||||
try {
|
||||
mActivity.showTip(message, length);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(mApplication, message, length).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.ui.SettingsActivity
|
||||
import com.hippo.preference.DialogPreference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
abstract class TaskPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : DialogPreference(context, attrs), CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
protected lateinit var dialog: AlertDialog
|
||||
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
builder.setTitle(null)
|
||||
builder.setView(R.layout.preference_dialog_task)
|
||||
builder.setCancelable(false)
|
||||
}
|
||||
|
||||
abstract fun launchJob()
|
||||
|
||||
override fun onDialogCreated(dialog: AlertDialog) {
|
||||
this.dialog = dialog
|
||||
launchJob()
|
||||
}
|
||||
|
||||
protected fun showTip(msg: String) {
|
||||
(context as SettingsActivity).showTip(
|
||||
msg,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,9 @@
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
class VersionPreference @JvmOverloads constructor(
|
||||
@@ -26,13 +26,7 @@ class VersionPreference @JvmOverloads constructor(
|
||||
) : Preference(context, attrs) {
|
||||
init {
|
||||
setTitle(R.string.settings_about_version)
|
||||
@Suppress("DEPRECATION") val version: String = try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
context.getString(R.string.error_unknown)
|
||||
}
|
||||
summary = version
|
||||
summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA})\n" +
|
||||
context.getString(R.string.settings_about_build_time, BuildConfig.BUILD_TIME)
|
||||
}
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.spider;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.Native;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.EhDB;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhCacheKeyFactory;
|
||||
import com.hippo.ehviewer.client.EhUtils;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.gallery.PageLoader2;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.io.UniFileInputStreamPipe;
|
||||
import com.hippo.io.UniFileOutputStreamPipe;
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
import com.hippo.unifile.RawFile;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
import com.hippo.yorozuya.Utilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Locale;
|
||||
|
||||
import coil.disk.DiskCache;
|
||||
import okio.BufferedSource;
|
||||
import okio.Okio;
|
||||
|
||||
public final class SpiderDen {
|
||||
|
||||
@Nullable
|
||||
private static DiskCache sCache; // We use data to store image file, and metadata for image type
|
||||
private final GalleryInfo mGalleryInfo;
|
||||
private final long mGid;
|
||||
@Nullable
|
||||
private UniFile mDownloadDir;
|
||||
private volatile int mMode = SpiderQueen.MODE_READ;
|
||||
|
||||
public SpiderDen(GalleryInfo galleryInfo) {
|
||||
mGalleryInfo = galleryInfo;
|
||||
mGid = galleryInfo.getGid();
|
||||
mDownloadDir = getGalleryDownloadDir(galleryInfo.getGid());
|
||||
}
|
||||
|
||||
public static void initialize(Context context) {
|
||||
sCache = new DiskCache.Builder().directory(new File(context.getCacheDir(), "gallery_image"))
|
||||
.maxSizeBytes((long) MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280) * 1024 * 1024).build();
|
||||
}
|
||||
|
||||
public static UniFile getGalleryDownloadDir(long gid) {
|
||||
UniFile dir = Settings.getDownloadLocation();
|
||||
// Read from DB
|
||||
String dirname = EhDB.getDownloadDirname(gid);
|
||||
if (dir != null && dirname != null) {
|
||||
// Some dirname may be invalid in some version
|
||||
dirname = FileUtils.sanitizeFilename(dirname);
|
||||
EhDB.putDownloadDirname(gid, dirname);
|
||||
return dir.subFile(dirname);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extension with dot
|
||||
*/
|
||||
public static String generateImageFilename(int index, String extension) {
|
||||
return String.format(Locale.US, "%08d%s", index + 1, extension);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static UniFile findImageFile(UniFile dir, int index) {
|
||||
for (String extension : PageLoader2.SUPPORT_IMAGE_EXTENSIONS) {
|
||||
String filename = generateImageFilename(index, extension);
|
||||
UniFile file = dir.findFile(filename);
|
||||
if (file != null) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setMode(@SpiderQueen.Mode int mode) {
|
||||
mMode = mode;
|
||||
|
||||
if (mode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
ensureDownloadDir();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean ensureDownloadDir() {
|
||||
if (mDownloadDir == null) {
|
||||
var dirname = FileUtils.sanitizeFilename(mGid + "-" + EhUtils.getSuitableTitle(mGalleryInfo));
|
||||
EhDB.putDownloadDirname(mGid, dirname);
|
||||
mDownloadDir = getGalleryDownloadDir(mGid);
|
||||
}
|
||||
return mDownloadDir != null && mDownloadDir.ensureDir();
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return switch (mMode) {
|
||||
case SpiderQueen.MODE_READ -> sCache != null;
|
||||
case SpiderQueen.MODE_DOWNLOAD -> mDownloadDir != null && mDownloadDir.isDirectory();
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UniFile getDownloadDir() {
|
||||
if (mDownloadDir == null) {
|
||||
mDownloadDir = getGalleryDownloadDir(mGid);
|
||||
}
|
||||
return mDownloadDir != null && mDownloadDir.isDirectory() ? mDownloadDir : null;
|
||||
}
|
||||
|
||||
private boolean containInCache(int index) {
|
||||
if (sCache == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
var snapshot = sCache.get(key);
|
||||
var exist = snapshot != null;
|
||||
if (exist) snapshot.close();
|
||||
return exist;
|
||||
}
|
||||
|
||||
private boolean containInDownloadDir(int index) {
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find image file in download dir
|
||||
return findImageFile(dir, index) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extension with dot
|
||||
*/
|
||||
private String fixExtension(String extension) {
|
||||
if (Utilities.contain(PageLoader2.SUPPORT_IMAGE_EXTENSIONS, extension)) {
|
||||
return extension;
|
||||
} else {
|
||||
return PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0];
|
||||
}
|
||||
}
|
||||
|
||||
private boolean copyFromCacheToDownloadDir(int index) {
|
||||
if (sCache == null) {
|
||||
return false;
|
||||
}
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return false;
|
||||
}
|
||||
// Find image file in cache
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
var snapshot = sCache.get(key);
|
||||
if (snapshot == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OutputStream os = null;
|
||||
try (snapshot; FileInputStream is = new FileInputStream(snapshot.getData().toFile()); BufferedSource buf = Okio.buffer(Okio.source(snapshot.getMetadata().toFile()))) {
|
||||
// Get extension
|
||||
String extension = fixExtension("." + buf.readUtf8Line());
|
||||
// Copy from cache to download dir
|
||||
UniFile file = dir.createFile(generateImageFilename(index, extension));
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
os = file.openOutputStream();
|
||||
IOUtils.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contain(int index) {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
return containInCache(index) || containInDownloadDir(index);
|
||||
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
return containInDownloadDir(index) || copyFromCacheToDownloadDir(index);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean removeFromCache(int index) {
|
||||
if (sCache == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
return sCache.remove(key);
|
||||
}
|
||||
|
||||
private boolean removeFromDownloadDir(int index) {
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result = false;
|
||||
for (int i = 0, n = PageLoader2.SUPPORT_IMAGE_EXTENSIONS.length; i < n; i++) {
|
||||
String filename = generateImageFilename(index, PageLoader2.SUPPORT_IMAGE_EXTENSIONS[i]);
|
||||
UniFile file = dir.subFile(filename);
|
||||
if (file != null) {
|
||||
result |= file.delete();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean remove(int index) {
|
||||
boolean result = removeFromCache(index);
|
||||
result |= removeFromDownloadDir(index);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private OutputStreamPipe openCacheOutputStreamPipe(int index, @Nullable String extension) {
|
||||
if (sCache == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
var editor = sCache.edit(key);
|
||||
if (editor == null) return null;
|
||||
try (var sink = Okio.buffer(Okio.sink(editor.getMetadata().toFile()))) {
|
||||
sink.writeUtf8(extension);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new OutputStreamPipe() {
|
||||
private OutputStream os = null;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public OutputStream open() throws IOException {
|
||||
if (os != null) IOUtils.closeQuietly(os);
|
||||
os = new FileOutputStream(editor.getData().toFile());
|
||||
return os;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (os != null) IOUtils.closeQuietly(os);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extension without dot
|
||||
*/
|
||||
@Nullable
|
||||
private OutputStreamPipe openDownloadOutputStreamPipe(int index, @Nullable String extension) {
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
extension = fixExtension('.' + extension);
|
||||
UniFile file = dir.createFile(generateImageFilename(index, extension));
|
||||
if (file != null) {
|
||||
return new UniFileOutputStreamPipe(file);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public OutputStreamPipe openOutputStreamPipe(int index, @Nullable String extension) {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
// Return the download pipe is the gallery has been downloaded
|
||||
OutputStreamPipe pipe = openDownloadOutputStreamPipe(index, extension);
|
||||
if (pipe == null) {
|
||||
pipe = openCacheOutputStreamPipe(index, extension);
|
||||
}
|
||||
return pipe;
|
||||
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
return openDownloadOutputStreamPipe(index, extension);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private InputStreamPipe openCacheInputStreamPipe(int index) {
|
||||
if (sCache == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
var snapshot = sCache.get(key);
|
||||
if (snapshot == null) return null;
|
||||
return new InputStreamPipe() {
|
||||
private InputStream is = null;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
snapshot.close();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InputStream open() throws IOException {
|
||||
if (is != null) IOUtils.closeQuietly(is);
|
||||
is = new FileInputStream(snapshot.getData().toFile());
|
||||
return is;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (is != null) IOUtils.closeQuietly(is);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InputStreamPipe openDownloadInputStreamPipe(int index) {
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
UniFile file = findImageFile(dir, index);
|
||||
if (file != null) {
|
||||
return new UniFileInputStreamPipe(file);
|
||||
} else if (!copyFromCacheToDownloadDir(index)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InputStreamPipe openInputStreamPipe(int index) {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
InputStreamPipe pipe = openCacheInputStreamPipe(index);
|
||||
if (pipe == null) {
|
||||
pipe = openDownloadInputStreamPipe(index);
|
||||
}
|
||||
return pipe;
|
||||
} else if (mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
return openDownloadInputStreamPipe(index);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Image.ByteBufferSource getImageSource(int index) {
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
String key = EhCacheKeyFactory.getImageKey(mGid, index);
|
||||
if (sCache != null) {
|
||||
var snapshot = sCache.get(key);
|
||||
if (snapshot != null) {
|
||||
try {
|
||||
var file = new RandomAccessFile(snapshot.getData().toFile(), "rw");
|
||||
var chan = file.getChannel();
|
||||
var map = chan.map(FileChannel.MapMode.PRIVATE, 0, file.length());
|
||||
return new Image.ByteBufferSource() {
|
||||
@NonNull
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
chan.close();
|
||||
file.close();
|
||||
snapshot.close();
|
||||
}
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UniFile dir = getDownloadDir();
|
||||
if (dir == null) {
|
||||
return null;
|
||||
}
|
||||
UniFile file = findImageFile(dir, index);
|
||||
if (file != null && mMode == SpiderQueen.MODE_DOWNLOAD) {
|
||||
if (copyFromCacheToDownloadDir(index)) {
|
||||
file = findImageFile(dir, index);
|
||||
}
|
||||
}
|
||||
if (file instanceof RawFile rawFile) {
|
||||
try {
|
||||
var file2 = new RandomAccessFile(rawFile.mFile, "rw");
|
||||
var chan = file2.getChannel();
|
||||
var map = chan.map(FileChannel.MapMode.PRIVATE, 0, file2.length());
|
||||
return new Image.ByteBufferSource() {
|
||||
@NonNull
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
chan.close();
|
||||
file2.close();
|
||||
}
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (file != null) {
|
||||
try {
|
||||
var pfd = EhApplication.getApplication().getContentResolver().openFileDescriptor(file.getUri(), "rw");
|
||||
var map = Native.mapFd(pfd.getFd(), pfd.getStatSize());
|
||||
if (map != null) {
|
||||
return new Image.ByteBufferSource() {
|
||||
@NonNull
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
Native.unmapDirectByteBuffer(map);
|
||||
pfd.close();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
pfd.close();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* 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 android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_WRITE
|
||||
import coil.decode.DecodeUtils
|
||||
import coil.decode.FrameDelayRewritingSource
|
||||
import coil.decode.isGif
|
||||
import coil.disk.DiskCache
|
||||
import com.hippo.copyTo
|
||||
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.coil.edit
|
||||
import com.hippo.ehviewer.coil.read
|
||||
import com.hippo.ehviewer.gallery.SUPPORT_IMAGE_EXTENSIONS
|
||||
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 moe.tarsin.coroutines.runSuspendCatching
|
||||
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 { SUPPORT_IMAGE_EXTENSIONS.contains(it) }
|
||||
?: SUPPORT_IMAGE_EXTENSIONS[0]
|
||||
}
|
||||
|
||||
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
|
||||
val dir = downloadDir ?: return false
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
return runCatching {
|
||||
sCache.read(key) {
|
||||
val extension = fixExtension("." + metadata.toFile().readText())
|
||||
val file = dir.createFile(generateImageFilename(index, extension)) ?: return false
|
||||
(file.openOutputStream() as FileOutputStream).use { outputStream ->
|
||||
ParcelFileDescriptor.open(data.toFile(), MODE_READ_WRITE).use {
|
||||
it copyTo outputStream.fd
|
||||
}
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
it.printStackTrace()
|
||||
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)?.runSuspendCatching {
|
||||
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)
|
||||
var received: Long = 0
|
||||
runSuspendCatching {
|
||||
sCache.edit(key) {
|
||||
metadata.toFile().writeText(extension)
|
||||
data.toFile().outputStream().use { outputStream ->
|
||||
outputStream.channel.use { received = doSave(it) }
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.onSuccess {
|
||||
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()
|
||||
}
|
||||
|
||||
private val COMPAT_IMAGE_EXTENSIONS = SUPPORT_IMAGE_EXTENSIONS + ".jpeg"
|
||||
|
||||
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 COMPAT_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,75 @@
|
||||
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.Serializable
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import java.io.File
|
||||
|
||||
@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
|
||||
)
|
||||
|
||||
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,830 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
* Rewrite with Kotlin coroutines, Tarsin Norbin 2023
|
||||
*
|
||||
* 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.util.Log
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhRequestBuilder
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl
|
||||
import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
|
||||
import com.hippo.ehviewer.client.EhUrl.referer
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePages
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewPages
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewSet
|
||||
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
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 okhttp3.executeAsync
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
|
||||
@Volatile
|
||||
lateinit var mPageStateArray: IntArray
|
||||
lateinit var mSpiderInfo: SpiderInfo
|
||||
|
||||
val mSpiderDen: SpiderDen = SpiderDen(galleryInfo)
|
||||
val mPagePercentMap = ConcurrentHashMap<Int, Float>()
|
||||
private val mPageStateLock = Any()
|
||||
private val mDownloadedPages = AtomicInteger(0)
|
||||
private val mFinishedPages = AtomicInteger(0)
|
||||
private val mPageErrorMap = ConcurrentHashMap<Int, String>()
|
||||
private val mSpiderListeners: MutableList<OnSpiderListener> = ArrayList()
|
||||
|
||||
private var mReadReference = 0
|
||||
private var mDownloadReference = 0
|
||||
|
||||
fun addOnSpiderListener(listener: OnSpiderListener) {
|
||||
synchronized(mSpiderListeners) { mSpiderListeners.add(listener) }
|
||||
}
|
||||
|
||||
fun removeOnSpiderListener(listener: OnSpiderListener) {
|
||||
synchronized(mSpiderListeners) { mSpiderListeners.remove(listener) }
|
||||
}
|
||||
|
||||
private fun notifyGetPages(pages: Int) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach { it.onGetPages(pages) }
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyGet509(index: Int) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach { it.onGet509(index) }
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onPageDownload(
|
||||
index,
|
||||
contentLength,
|
||||
receivedSize,
|
||||
bytesRead
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyPageSuccess(index: Int) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onPageSuccess(
|
||||
index,
|
||||
mFinishedPages.get(),
|
||||
mDownloadedPages.get(),
|
||||
mPageStateArray.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyPageFailure(index: Int, error: String?) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onPageFailure(
|
||||
index,
|
||||
error,
|
||||
mFinishedPages.get(),
|
||||
mDownloadedPages.get(),
|
||||
mPageStateArray.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyAllPageDownloaded() {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onFinish(
|
||||
mFinishedPages.get(),
|
||||
mDownloadedPages.get(),
|
||||
mPageStateArray.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyGetImageSuccess(index: Int, image: Image) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onGetImageSuccess(index, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyGetImageFailure(index: Int, error: String) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onGetImageFailure(index, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var downloadMode = false
|
||||
val isReady
|
||||
get() = this::mSpiderInfo.isInitialized && this::mPageStateArray.isInitialized
|
||||
|
||||
@Synchronized
|
||||
private fun updateMode() {
|
||||
if (!isReady) return
|
||||
val mode: Int = if (mDownloadReference > 0) {
|
||||
MODE_DOWNLOAD
|
||||
} else {
|
||||
MODE_READ
|
||||
}
|
||||
mSpiderDen.setMode(mode)
|
||||
|
||||
// Update download page
|
||||
val intoDownloadMode = mode == MODE_DOWNLOAD
|
||||
if (intoDownloadMode && !downloadMode) {
|
||||
// Clear download state
|
||||
synchronized(mPageStateLock) {
|
||||
val temp: IntArray = mPageStateArray
|
||||
var i = 0
|
||||
val n = temp.size
|
||||
while (i < n) {
|
||||
val oldState = temp[i]
|
||||
if (STATE_DOWNLOADING != oldState) {
|
||||
temp[i] = STATE_NONE
|
||||
}
|
||||
i++
|
||||
}
|
||||
mDownloadedPages.lazySet(0)
|
||||
mFinishedPages.lazySet(0)
|
||||
mPageErrorMap.clear()
|
||||
mPagePercentMap.clear()
|
||||
}
|
||||
mWorkerScope.enterDownloadMode()
|
||||
}
|
||||
downloadMode = intoDownloadMode
|
||||
}
|
||||
|
||||
private fun setMode(@Mode mode: Int) {
|
||||
when (mode) {
|
||||
MODE_READ -> mReadReference++
|
||||
MODE_DOWNLOAD -> mDownloadReference++
|
||||
}
|
||||
check(mDownloadReference <= 1) { "mDownloadReference can't more than 0" }
|
||||
}
|
||||
|
||||
private fun clearMode(@Mode mode: Int) {
|
||||
when (mode) {
|
||||
MODE_READ -> mReadReference--
|
||||
MODE_DOWNLOAD -> mDownloadReference--
|
||||
}
|
||||
check(!(mReadReference < 0 || mDownloadReference < 0)) { "Mode reference < 0" }
|
||||
}
|
||||
|
||||
private val prepareJob = launchIO { doPrepare() }
|
||||
|
||||
private suspend fun doPrepare() {
|
||||
mSpiderInfo = readSpiderInfoFromLocal() ?: readSpiderInfoFromInternet() ?: return
|
||||
mPageStateArray = IntArray(mSpiderInfo.pages)
|
||||
notifyGetPages(mSpiderInfo.pages)
|
||||
}
|
||||
|
||||
suspend fun awaitReady(): Boolean {
|
||||
prepareJob.join()
|
||||
return isReady
|
||||
}
|
||||
|
||||
suspend fun awaitStartPage(): Int {
|
||||
prepareJob.join()
|
||||
if (!isReady) return 0
|
||||
return mSpiderInfo.startPage
|
||||
}
|
||||
|
||||
private fun stop() {
|
||||
launchNonCancellable { runCatching { writeSpiderInfoToLocal() } }
|
||||
cancel()
|
||||
}
|
||||
|
||||
val size
|
||||
get() = mPageStateArray.size
|
||||
|
||||
val error: String?
|
||||
get() = null
|
||||
|
||||
fun forceRequest(index: Int) {
|
||||
request(index, true)
|
||||
}
|
||||
|
||||
fun request(index: Int) {
|
||||
request(index, false)
|
||||
}
|
||||
|
||||
private fun getPageState(index: Int): Int {
|
||||
synchronized(mPageStateLock) {
|
||||
return if (index >= 0 && index < mPageStateArray.size) {
|
||||
mPageStateArray[index]
|
||||
} else {
|
||||
STATE_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelRequest(index: Int) {
|
||||
mWorkerScope.cancelDecode(index)
|
||||
}
|
||||
|
||||
fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
|
||||
mWorkerScope.updateRAList(pages, pair)
|
||||
}
|
||||
|
||||
private fun request(index: Int, force: Boolean) {
|
||||
// Get page state
|
||||
val state = getPageState(index)
|
||||
|
||||
// Fix state for force
|
||||
if (force && (state == STATE_FINISHED || state == STATE_FAILED) || state == STATE_FAILED) {
|
||||
// Update state to none at once
|
||||
updatePageState(index, STATE_NONE)
|
||||
}
|
||||
mWorkerScope.launch(index, force)
|
||||
}
|
||||
|
||||
fun save(index: Int, file: UniFile): Boolean {
|
||||
val state = getPageState(index)
|
||||
return if (STATE_FINISHED != state) {
|
||||
false
|
||||
} else mSpiderDen.saveToUniFile(index, file)
|
||||
}
|
||||
|
||||
fun save(index: Int, dir: UniFile, filename: String): UniFile? {
|
||||
val state = getPageState(index)
|
||||
if (STATE_FINISHED != state) {
|
||||
return null
|
||||
}
|
||||
val ext = mSpiderDen.getExtension(index)
|
||||
val dst = dir.subFile(if (null != ext) "$filename.$ext" else filename) ?: return null
|
||||
return if (!mSpiderDen.saveToUniFile(index, dst)) null else dst
|
||||
}
|
||||
|
||||
fun getExtension(index: Int): String? {
|
||||
val state = getPageState(index)
|
||||
return if (STATE_FINISHED != state) {
|
||||
null
|
||||
} else mSpiderDen.getExtension(index)
|
||||
}
|
||||
|
||||
val startPage: Int
|
||||
get() = mSpiderInfo.startPage
|
||||
|
||||
fun putStartPage(page: Int) {
|
||||
mSpiderInfo.startPage = page
|
||||
}
|
||||
|
||||
private fun readSpiderInfoFromLocal(): SpiderInfo? {
|
||||
return mSpiderDen.downloadDir?.run {
|
||||
findFile(SPIDER_INFO_FILENAME)?.let { file ->
|
||||
readCompatFromUniFile(file)?.takeIf {
|
||||
it.gid == galleryInfo.gid && it.token == galleryInfo.token
|
||||
}
|
||||
}
|
||||
}
|
||||
?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
|
||||
}
|
||||
|
||||
@Throws(ParseException::class)
|
||||
private fun readPreviews(body: String, index: Int, spiderInfo: SpiderInfo) {
|
||||
spiderInfo.previewPages = parsePreviewPages(body)
|
||||
val previewSet = parsePreviewSet(body)
|
||||
if (previewSet.size() > 0) {
|
||||
if (index == 0) {
|
||||
spiderInfo.previewPerPage = previewSet.size()
|
||||
} else {
|
||||
spiderInfo.previewPerPage = previewSet.getPosition(0) / index
|
||||
}
|
||||
}
|
||||
var i = 0
|
||||
val n = previewSet.size()
|
||||
while (i < n) {
|
||||
val result = GalleryPageUrlParser.parse(previewSet.getPageUrlAt(i))
|
||||
if (result != null) {
|
||||
spiderInfo.pTokenMap[result.page] = result.pToken
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun readSpiderInfoFromInternet(): SpiderInfo? {
|
||||
val request = EhRequestBuilder(
|
||||
getGalleryDetailUrl(
|
||||
galleryInfo.gid, galleryInfo.token, 0, false
|
||||
), referer
|
||||
).build()
|
||||
return runSuspendCatching {
|
||||
okHttpClient.newCall(request).executeAsync().use { response ->
|
||||
val body = response.body.string()
|
||||
val pages = parsePages(body)
|
||||
val spiderInfo = SpiderInfo(galleryInfo.gid, pages)
|
||||
spiderInfo.token = galleryInfo.token
|
||||
readPreviews(body, 0, spiderInfo)
|
||||
spiderInfo
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun getPTokenFromMultiPageViewer(index: Int): String? {
|
||||
val spiderInfo = mSpiderInfo
|
||||
val url = getGalleryMultiPageViewerUrl(
|
||||
galleryInfo.gid, galleryInfo.token!!
|
||||
)
|
||||
val referer = referer
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
try {
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
val body = response.body.string()
|
||||
val list = GalleryMultiPageViewerPTokenParser.parse(body)
|
||||
for (i in list.indices) {
|
||||
spiderInfo.pTokenMap[i] = list[i]
|
||||
}
|
||||
return spiderInfo.pTokenMap[index]
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getPTokenFromInternet(index: Int): String? {
|
||||
val spiderInfo = mSpiderInfo
|
||||
|
||||
// Check previewIndex
|
||||
var previewIndex: Int
|
||||
previewIndex = if (spiderInfo.previewPerPage >= 0) {
|
||||
index / spiderInfo.previewPerPage
|
||||
} else {
|
||||
0
|
||||
}
|
||||
if (spiderInfo.previewPages > 0) {
|
||||
previewIndex = previewIndex.coerceAtMost(spiderInfo.previewPages - 1)
|
||||
}
|
||||
val url = getGalleryDetailUrl(
|
||||
galleryInfo.gid, galleryInfo.token, previewIndex, false
|
||||
)
|
||||
val referer = referer
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
try {
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
val body = response.body.string()
|
||||
readPreviews(body, previewIndex, spiderInfo)
|
||||
return spiderInfo.pTokenMap[index]
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun writeSpiderInfoToLocal() {
|
||||
if (!isReady) return
|
||||
mSpiderDen.downloadDir?.run { createFile(SPIDER_INFO_FILENAME).also { mSpiderInfo.write(it) } }
|
||||
mSpiderInfo.saveToCache()
|
||||
}
|
||||
|
||||
private fun isStateDone(state: Int): Boolean {
|
||||
return state == STATE_FINISHED || state == STATE_FAILED
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun updatePageState(index: Int, @State state: Int, error: String? = null) {
|
||||
var oldState: Int
|
||||
synchronized(mPageStateLock) {
|
||||
oldState = mPageStateArray[index]
|
||||
mPageStateArray[index] = state
|
||||
if (!isStateDone(oldState) && isStateDone(state)) {
|
||||
mDownloadedPages.incrementAndGet()
|
||||
} else if (isStateDone(oldState) && !isStateDone(state)) {
|
||||
mDownloadedPages.decrementAndGet()
|
||||
}
|
||||
if (oldState != STATE_FINISHED && state == STATE_FINISHED) {
|
||||
mFinishedPages.incrementAndGet()
|
||||
} else if (oldState == STATE_FINISHED && state != STATE_FINISHED) {
|
||||
mFinishedPages.decrementAndGet()
|
||||
}
|
||||
|
||||
// Clear
|
||||
if (state == STATE_DOWNLOADING) {
|
||||
mPageErrorMap.remove(index)
|
||||
} else if (isStateDone(state)) {
|
||||
mPagePercentMap.remove(index)
|
||||
}
|
||||
|
||||
// Get default error
|
||||
if (state == STATE_FAILED) {
|
||||
mPageErrorMap[index] = error ?: GetText.getString(R.string.error_unknown)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
if (state == STATE_FAILED) {
|
||||
notifyPageFailure(index, error)
|
||||
} else if (state == STATE_FINISHED) {
|
||||
notifyPageSuccess(index)
|
||||
}
|
||||
if (mDownloadedPages.get() == size) notifyAllPageDownloaded()
|
||||
}
|
||||
|
||||
@IntDef(MODE_READ, MODE_DOWNLOAD)
|
||||
@Retention
|
||||
annotation class Mode
|
||||
|
||||
@IntDef(STATE_NONE, STATE_DOWNLOADING, STATE_FINISHED, STATE_FAILED)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class State
|
||||
interface OnSpiderListener {
|
||||
fun onGetPages(pages: Int)
|
||||
fun onGet509(index: Int)
|
||||
fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int)
|
||||
fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int)
|
||||
fun onPageFailure(index: Int, error: String?, finished: Int, downloaded: Int, total: Int)
|
||||
fun onFinish(finished: Int, downloaded: Int, total: Int)
|
||||
fun onGetImageSuccess(index: Int, image: Image?)
|
||||
fun onGetImageFailure(index: Int, error: String?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MODE_READ = 0
|
||||
const val MODE_DOWNLOAD = 1
|
||||
const val STATE_NONE = 0
|
||||
const val STATE_DOWNLOADING = 1
|
||||
const val STATE_FINISHED = 2
|
||||
const val STATE_FAILED = 3
|
||||
const val SPIDER_INFO_FILENAME = ".ehviewer"
|
||||
private val sQueenMap = LongSparseArray<SpiderQueen>()
|
||||
|
||||
@JvmStatic
|
||||
fun obtainSpiderQueen(galleryInfo: GalleryInfo, @Mode mode: Int): SpiderQueen {
|
||||
val gid = galleryInfo.gid
|
||||
return (sQueenMap[gid] ?: SpiderQueen(galleryInfo).also { sQueenMap[gid] = it }).apply {
|
||||
setMode(mode)
|
||||
launchIO { if (awaitReady()) updateMode() }
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun releaseSpiderQueen(queen: SpiderQueen, @Mode mode: Int) {
|
||||
queen.run {
|
||||
clearMode(mode)
|
||||
if (mReadReference == 0 && mDownloadReference == 0) {
|
||||
stop()
|
||||
sQueenMap.remove(galleryInfo.gid)
|
||||
} else {
|
||||
launchIO { if (awaitReady()) updateMode() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val mWorkerScope = object {
|
||||
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
|
||||
|
||||
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 = 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)
|
||||
}
|
||||
}.apply {
|
||||
invokeOnCompletion {
|
||||
if (getPageState(index) == STATE_DOWNLOADING) {
|
||||
Log.d(WORKER_DEBUG_TAG, "Download image cancelled $index")
|
||||
mSpiderDen.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(index: Int, force: Boolean = false) {
|
||||
check(index in 0 until size)
|
||||
if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }
|
||||
if (force) decoder.cancel(index)
|
||||
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 mSpiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
|
||||
?: getPTokenFromInternet(index)
|
||||
?: getPTokenFromInternet(index)
|
||||
?: getPTokenFromMultiPageViewer(index)
|
||||
}
|
||||
updatePageState(index, STATE_DOWNLOADING)
|
||||
if (!force && index in mSpiderDen) {
|
||||
return updatePageState(index, STATE_FINISHED)
|
||||
}
|
||||
if (force) {
|
||||
pTokenLock.withLock {
|
||||
val pToken = mSpiderInfo.pTokenMap[index]
|
||||
if (pToken == TOKEN_FAILED) mSpiderInfo.pTokenMap.remove(index)
|
||||
}
|
||||
}
|
||||
val previousPToken: String?
|
||||
val pToken: String
|
||||
|
||||
pTokenLock.withLock {
|
||||
pToken = getPToken(index) ?: return updatePageState(
|
||||
index,
|
||||
STATE_FAILED,
|
||||
PTOKEN_FAILED_MESSAGE
|
||||
).also {
|
||||
mSpiderInfo.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 = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
|
||||
// Add skipHathKey
|
||||
if (skipHathKey != null) {
|
||||
pageUrl += if ("?" in pageUrl) {
|
||||
"&nl=$skipHathKey"
|
||||
} else {
|
||||
"?nl=$skipHathKey"
|
||||
}
|
||||
}
|
||||
runSuspendCatching {
|
||||
EhEngine.getGalleryPage(pageUrl, mSpiderInfo.gid, mSpiderInfo.token)
|
||||
.also {
|
||||
if (check509(it.imageUrl)) {
|
||||
// Get 509
|
||||
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 {
|
||||
EhEngine.getGalleryPageApi(
|
||||
mSpiderInfo.gid,
|
||||
index,
|
||||
pToken,
|
||||
localShowKey,
|
||||
previousPToken
|
||||
).also {
|
||||
if (check509(it.imageUrl)) {
|
||||
// Get 509
|
||||
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 = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
|
||||
} else {
|
||||
targetImageUrl = imageUrl
|
||||
referer = null
|
||||
}
|
||||
if (targetImageUrl == null) {
|
||||
error = "TargetImageUrl error"
|
||||
return@repeat
|
||||
}
|
||||
Log.d(WORKER_DEBUG_TAG, targetImageUrl)
|
||||
|
||||
runSuspendCatching {
|
||||
Log.d(WORKER_DEBUG_TAG, "Start download image $index")
|
||||
val success: Boolean = mSpiderDen.makeHttpCallAndSaveImage(
|
||||
index,
|
||||
targetImageUrl,
|
||||
referer
|
||||
) { contentLength: Long, receivedSize: Long, bytesRead: Int ->
|
||||
mPagePercentMap[index] = receivedSize.toFloat() / contentLength
|
||||
notifyPageDownload(index, contentLength, receivedSize, bytesRead)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Log.e(WORKER_DEBUG_TAG, "Can't download all of image data")
|
||||
error = "Incomplete"
|
||||
forceHtml = true
|
||||
return@repeat
|
||||
}
|
||||
|
||||
if (mSpiderDen.checkPlainText(index)) {
|
||||
error = ""
|
||||
forceHtml = true
|
||||
return@repeat
|
||||
}
|
||||
|
||||
Log.d(WORKER_DEBUG_TAG, "Download image succeed $index")
|
||||
|
||||
updatePageState(index, STATE_FINISHED)
|
||||
delay(mDownloadDelay.toLong())
|
||||
return
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
error = NETWORK_ERROR
|
||||
forceHtml = true
|
||||
}
|
||||
Log.d(WORKER_DEBUG_TAG, "End download image $index")
|
||||
}
|
||||
mSpiderDen.remove(index)
|
||||
updatePageState(index, STATE_FAILED, error)
|
||||
}
|
||||
|
||||
private val decoder = object {
|
||||
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 = mSpiderDen.getImageSource(index) ?: return
|
||||
val image = mSemaphore.withPermit { Image.decode(src) }
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image?.recycle()
|
||||
throw it
|
||||
}
|
||||
if (image == null) {
|
||||
notifyGetImageFailure(index, DECODE_ERROR)
|
||||
} else {
|
||||
notifyGetImageSuccess(index, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error)
|
||||
private val ERROR_509 = GetText.getString(R.string.error_509)
|
||||
private val NETWORK_ERROR = GetText.getString(R.string.error_socket)
|
||||
private val DECODE_ERROR = GetText.getString(R.string.error_decoding_failed)
|
||||
private val URL_509_ARRAY = arrayOf(
|
||||
"https://ehgt.org/g/509.gif",
|
||||
"https://ehgt.org/g/509s.gif",
|
||||
"https://exhentai.org/img/509.gif",
|
||||
"https://exhentai.org/img/509s.gif"
|
||||
)
|
||||
private const val WORKER_DEBUG_TAG = "SpiderQueenWorker"
|
||||
|
||||
private fun check509(url: String) = url in URL_509_ARRAY
|
||||
@@ -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.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhClient
|
||||
import com.hippo.ehviewer.client.EhRequest
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.download.DownloadService
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import com.hippo.yorozuya.collect.LongList
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
object CommonOperations {
|
||||
private fun doAddToFavorites(
|
||||
activity: Activity, galleryInfo: GalleryInfo,
|
||||
slot: Int, listener: EhClient.Callback<Void?>
|
||||
) {
|
||||
when (slot) {
|
||||
-1 -> {
|
||||
EhDB.putLocalFavorites(galleryInfo)
|
||||
listener.onSuccess(null)
|
||||
}
|
||||
|
||||
in 0..9 -> {
|
||||
val request = EhRequest()
|
||||
request.setMethod(EhClient.METHOD_ADD_FAVORITES)
|
||||
request.setArgs(galleryInfo.gid, galleryInfo.token, slot, "")
|
||||
request.setCallback(listener)
|
||||
request.enqueue(activity)
|
||||
}
|
||||
|
||||
else -> {
|
||||
listener.onFailure(Exception()) // TODO Add text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun addToFavorites(
|
||||
activity: Activity, galleryInfo: GalleryInfo,
|
||||
listener: EhClient.Callback<Void?>, select: Boolean = false
|
||||
) {
|
||||
val slot = Settings.getDefaultFavSlot()
|
||||
val localFav = activity.getString(R.string.local_favorites)
|
||||
val items = Settings.getFavCat().toMutableList().apply { add(0, localFav) }
|
||||
if (!select && slot >= -1 && slot <= 9) {
|
||||
val newFavoriteName = if (slot >= 0) items[slot + 1] else null
|
||||
doAddToFavorites(
|
||||
activity,
|
||||
galleryInfo,
|
||||
slot,
|
||||
DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot)
|
||||
)
|
||||
} else {
|
||||
ListCheckBoxDialogBuilder(
|
||||
activity, items,
|
||||
{ builder: ListCheckBoxDialogBuilder?, _: AlertDialog?, position: Int ->
|
||||
val slot1 = position - 1
|
||||
val newFavoriteName = if (slot1 in 0..9) items[slot1 + 1] else null
|
||||
doAddToFavorites(
|
||||
activity,
|
||||
galleryInfo,
|
||||
slot1,
|
||||
DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot1)
|
||||
)
|
||||
if (builder?.isChecked == true) {
|
||||
Settings.putDefaultFavSlot(slot1)
|
||||
} else {
|
||||
Settings.putDefaultFavSlot(Settings.INVALID_DEFAULT_FAV_SLOT)
|
||||
}
|
||||
}, activity.getString(R.string.remember_favorite_collection), false
|
||||
)
|
||||
.setTitle(R.string.add_favorites_dialog_title)
|
||||
.setOnCancelListener { listener.onCancel() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromFavorites(
|
||||
activity: Activity?, galleryInfo: GalleryInfo,
|
||||
listener: EhClient.Callback<Void?>
|
||||
) {
|
||||
EhDB.removeLocalFavorites(galleryInfo.gid)
|
||||
val request = EhRequest()
|
||||
request.setMethod(EhClient.METHOD_ADD_FAVORITES)
|
||||
request.setArgs(galleryInfo.gid, galleryInfo.token, -1, "")
|
||||
request.setCallback(DelegateFavoriteCallback(listener, galleryInfo, null, -2))
|
||||
request.enqueue(activity!!)
|
||||
}
|
||||
|
||||
fun startDownload(activity: MainActivity?, galleryInfo: GalleryInfo, forceDefault: Boolean) {
|
||||
startDownload(activity!!, listOf(galleryInfo), forceDefault)
|
||||
}
|
||||
|
||||
fun startDownload(
|
||||
activity: MainActivity,
|
||||
galleryInfos: List<GalleryInfo>,
|
||||
forceDefault: Boolean
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
application.topActivity?.checkAndRequestNotificationPermission()
|
||||
}
|
||||
doStartDownload(activity, galleryInfos, forceDefault)
|
||||
}
|
||||
|
||||
private fun doStartDownload(
|
||||
activity: MainActivity,
|
||||
galleryInfos: List<GalleryInfo>,
|
||||
forceDefault: Boolean
|
||||
) {
|
||||
val dm = downloadManager
|
||||
val toStart = LongList()
|
||||
val toAdd: MutableList<GalleryInfo> = ArrayList()
|
||||
for (gi in galleryInfos) {
|
||||
if (dm.containDownloadInfo(gi.gid)) {
|
||||
toStart.add(gi.gid)
|
||||
} else {
|
||||
toAdd.add(gi)
|
||||
}
|
||||
}
|
||||
if (!toStart.isEmpty()) {
|
||||
val intent = Intent(activity, DownloadService::class.java)
|
||||
intent.action = DownloadService.ACTION_START_RANGE
|
||||
intent.putExtra(DownloadService.KEY_GID_LIST, toStart)
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
}
|
||||
if (toAdd.isEmpty()) {
|
||||
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)
|
||||
return
|
||||
}
|
||||
var justStart = forceDefault
|
||||
var label: String? = null
|
||||
// Get default download label
|
||||
if (!justStart && Settings.getHasDefaultDownloadLabel()) {
|
||||
label = Settings.getDefaultDownloadLabel()
|
||||
justStart = label == null || dm.containLabel(label)
|
||||
}
|
||||
// If there is no other label, just use null label
|
||||
if (!justStart && dm.labelList.isEmpty()) {
|
||||
justStart = true
|
||||
label = null
|
||||
}
|
||||
if (justStart) {
|
||||
// Got default label
|
||||
for (gi in toAdd) {
|
||||
val intent = Intent(activity, DownloadService::class.java)
|
||||
intent.action = DownloadService.ACTION_START
|
||||
intent.putExtra(DownloadService.KEY_LABEL, label)
|
||||
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
}
|
||||
// Notify
|
||||
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)
|
||||
} else {
|
||||
// Let use chose label
|
||||
val list = dm.labelList
|
||||
val items = mutableListOf<String>()
|
||||
items.add(activity.getString(R.string.default_download_label_name))
|
||||
items.addAll(list.mapNotNull { it.label })
|
||||
ListCheckBoxDialogBuilder(
|
||||
activity, items,
|
||||
{ builder: ListCheckBoxDialogBuilder?, _: AlertDialog?, position: Int ->
|
||||
var label1: String?
|
||||
if (position == 0) {
|
||||
label1 = null
|
||||
} else {
|
||||
label1 = items[position]
|
||||
if (!dm.containLabel(label1)) {
|
||||
label1 = null
|
||||
}
|
||||
}
|
||||
// Start download
|
||||
for (gi in toAdd) {
|
||||
val intent = Intent(activity, DownloadService::class.java)
|
||||
intent.action = DownloadService.ACTION_START
|
||||
intent.putExtra(DownloadService.KEY_LABEL, label1)
|
||||
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
}
|
||||
// Save settings
|
||||
if (builder?.isChecked == true) {
|
||||
Settings.putHasDefaultDownloadLabel(true)
|
||||
Settings.putDefaultDownloadLabel(label1)
|
||||
} else {
|
||||
Settings.putHasDefaultDownloadLabel(false)
|
||||
}
|
||||
// Notify
|
||||
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)
|
||||
}, activity.getString(R.string.remember_download_label), false
|
||||
)
|
||||
.setTitle(R.string.download)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun ensureNoMediaFile(file: UniFile?) {
|
||||
if (null == file) {
|
||||
return
|
||||
}
|
||||
val noMedia = file.createFile(".nomedia") ?: return
|
||||
var `is`: InputStream? = null
|
||||
try {
|
||||
`is` = noMedia.openInputStream()
|
||||
} catch (e: IOException) {
|
||||
// Ignore
|
||||
} finally {
|
||||
IOUtils.closeQuietly(`is`)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun removeNoMediaFile(file: UniFile?) {
|
||||
if (null == file) {
|
||||
return
|
||||
}
|
||||
val noMedia = file.subFile(".nomedia")
|
||||
if (null != noMedia && noMedia.isFile) {
|
||||
noMedia.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private class DelegateFavoriteCallback(
|
||||
private val delegate: EhClient.Callback<Void?>, private val info: GalleryInfo,
|
||||
private val newFavoriteName: String?, private val slot: Int
|
||||
) : EhClient.Callback<Void?> {
|
||||
override fun onSuccess(result: Void?) {
|
||||
info.favoriteName = newFavoriteName
|
||||
info.favoriteSlot = slot
|
||||
delegate.onSuccess(result)
|
||||
favouriteStatusRouter.modifyFavourites(info.gid, slot)
|
||||
}
|
||||
|
||||
override fun onFailure(e: Exception) {
|
||||
delegate.onFailure(e)
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
delegate.onCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.content.pm.PackageManager
|
||||
import android.content.pm.verify.domain.DomainVerificationManager
|
||||
import android.content.pm.verify.domain.DomainVerificationUserState
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
@@ -35,6 +36,7 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
@@ -69,11 +71,10 @@ import com.hippo.ehviewer.ui.scene.GalleryDetailScene
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
|
||||
import com.hippo.ehviewer.ui.scene.ProgressScene
|
||||
import com.hippo.io.UniFileInputStreamPipe
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.util.BitmapUtils
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.util.addTextToClipboard
|
||||
import com.hippo.util.getClipboardManager
|
||||
import com.hippo.util.getParcelableExtraCompat
|
||||
import com.hippo.util.getUrlFromClipboard
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
@@ -88,28 +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 val connectivityManager by lazy { getSystemService<ConnectivityManager>()!! }
|
||||
private val availableNetworks = mutableListOf<Network>()
|
||||
|
||||
private fun saveImageToTempFile(file: UniFile?): File? {
|
||||
file ?: return null
|
||||
var bitmap: Bitmap? = null
|
||||
try {
|
||||
bitmap = BitmapUtils.decodeStream(
|
||||
UniFileInputStreamPipe(file),
|
||||
-1,
|
||||
-1,
|
||||
500 * 500,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
// Ignore
|
||||
}
|
||||
if (null == bitmap) {
|
||||
return null
|
||||
}
|
||||
private fun saveImageToTempFile(uri: Uri): File? {
|
||||
val src = ImageDecoder.createSource(contentResolver, uri)
|
||||
val bitmap = runCatching {
|
||||
ImageDecoder.decodeBitmap(src, Image.imageSearchDecoderSampleListener)
|
||||
}.getOrNull() ?: return null
|
||||
val temp = AppConfig.createTempFile() ?: return null
|
||||
var os: OutputStream? = null
|
||||
return try {
|
||||
@@ -198,10 +185,9 @@ class MainActivity : EhActivity() {
|
||||
)
|
||||
return true
|
||||
} else if (type != null && type.startsWith("image/")) {
|
||||
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
||||
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
||||
if (null != uri) {
|
||||
val file = UniFile.fromUri(this, uri)
|
||||
val temp = saveImageToTempFile(file)
|
||||
val temp = saveImageToTempFile(uri)
|
||||
if (null != temp) {
|
||||
val builder = ListUrlBuilder()
|
||||
builder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
|
||||
@@ -223,19 +209,7 @@ class MainActivity : EhActivity() {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
connectivityManager = getSystemService()!!
|
||||
if (Settings.getDF() && Settings.getBypassVPN()) {
|
||||
bypassVPN()
|
||||
}
|
||||
if (EhUtils.needSignedIn())
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||
navController = navHostFragment.navController
|
||||
private fun setNavGraph() {
|
||||
navController.apply {
|
||||
graph = navInflater.inflate(R.navigation.nav_graph).apply {
|
||||
when (Settings.getLaunchPageGalleryListSceneAction()) {
|
||||
@@ -246,6 +220,19 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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()) setNavGraph()
|
||||
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
|
||||
binding.navView.setupWithNavController(navController)
|
||||
|
||||
@@ -287,7 +274,7 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun bypassVPN() {
|
||||
private fun bypassVpn() {
|
||||
val network = connectivityManager.activeNetwork
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
capabilities?.let {
|
||||
@@ -377,7 +364,19 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val loginLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (availableNetworks.isNotEmpty()) {
|
||||
connectivityManager.bindProcessToNetwork(availableNetworks.last())
|
||||
}
|
||||
setNavGraph()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (EhUtils.needSignedIn()) {
|
||||
connectivityManager.bindProcessToNetwork(null)
|
||||
loginLauncher.launch(Intent(this, LoginActivity::class.java))
|
||||
}
|
||||
super.onResume()
|
||||
lifecycleScope.launch {
|
||||
delay(100)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package com.hippo.ehviewer.ui.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -41,10 +40,11 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.client.parser.FavoritesParser
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.IoThreadPoolExecutor
|
||||
import com.hippo.util.LogCat
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -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>(
|
||||
@@ -78,10 +79,9 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
.setCancelable(false)
|
||||
.setView(R.layout.preference_dialog_task)
|
||||
.show()
|
||||
IoThreadPoolExecutor.getInstance().execute {
|
||||
lifecycleScope.launchIO {
|
||||
val success = EhDB.exportDB(requireActivity(), uri)
|
||||
val activity: Activity? = activity
|
||||
activity?.runOnUiThread {
|
||||
withUIContext {
|
||||
if (alertDialog.isShowing) {
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
@@ -203,10 +203,9 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
.setCancelable(false)
|
||||
.setView(R.layout.preference_dialog_task)
|
||||
.show()
|
||||
IoThreadPoolExecutor.getInstance().execute {
|
||||
lifecycleScope.launchIO {
|
||||
val error = EhDB.importDB(requireActivity(), uri)
|
||||
val activity: Activity? = activity
|
||||
activity?.runOnUiThread {
|
||||
withUIContext {
|
||||
if (alertDialog.isShowing) {
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
@@ -235,6 +234,8 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
val exportData = findPreference<Preference>(KEY_EXPORT_DATA)
|
||||
val backupFavorite = findPreference<Preference>(KEY_BACKUP_FAVORITE)
|
||||
val openByDefault = findPreference<Preference>(KEY_OPEN_BY_DEFAULT)
|
||||
val domainFronting = findPreference<Preference>(AppSettings.KEY_DOMAIN_FRONTING)
|
||||
val bypassVpn = findPreference<Preference>(AppSettings.KEY_BYPASS_VPN)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
openByDefault!!.isVisible = false
|
||||
} else {
|
||||
@@ -245,6 +246,11 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
exportData!!.onPreferenceClickListener = this
|
||||
backupFavorite!!.onPreferenceClickListener = this
|
||||
appLanguage!!.onPreferenceChangeListener = this
|
||||
bypassVpn!!.isVisible = AppSettings.getDF()
|
||||
domainFronting!!.setOnPreferenceChangeListener { _, newValue ->
|
||||
bypassVpn.isVisible = newValue as Boolean
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceClick(preference: Preference): Boolean {
|
||||
@@ -336,7 +342,7 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
if (result.next != null) {
|
||||
try {
|
||||
runBlocking {
|
||||
delay(com.hippo.ehviewer.Settings.getDownloadDelay().toLong())
|
||||
delay(AppSettings.getDownloadDelay().toLong())
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
|
||||
@@ -20,14 +20,21 @@ 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.google.android.material.timepicker.MaterialTimePicker
|
||||
import com.google.android.material.timepicker.TimeFormat.CLOCK_12H
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import com.hippo.ehviewer.dailycheck.schedHour
|
||||
import com.hippo.ehviewer.dailycheck.schedMinute
|
||||
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
|
||||
class EhFragment : BasePreferenceFragment() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
@@ -42,7 +49,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
|
||||
@@ -55,10 +62,38 @@ class EhFragment : BasePreferenceFragment() {
|
||||
thumbResolution!!.setSummaryProvider {
|
||||
getString(R.string.settings_eh_thumb_resolution_summary, (it as ListPreference).entry)
|
||||
}
|
||||
|
||||
if (!EhTagDatabase.isTranslatable(requireActivity())) {
|
||||
preferenceScreen.removePreference(showTagTranslations)
|
||||
preferenceScreen.removePreference(tagTranslationsSource!!)
|
||||
}
|
||||
if (!EhCookieStore.hasSignedIn()) {
|
||||
Settings.SIGN_IN_REQUIRED.forEach {
|
||||
val preference = findPreference<Preference>(it)
|
||||
preferenceScreen.removePreference(preference!!)
|
||||
}
|
||||
} else {
|
||||
findPreference<Preference>(Settings.KEY_REQUEST_NEWS_TIMER)!!.apply {
|
||||
setOnPreferenceClickListener {
|
||||
MaterialTimePicker.Builder()
|
||||
.apply {
|
||||
schedHour?.let { setHour(it) }
|
||||
schedMinute?.let { setMinute(it) }
|
||||
}
|
||||
.setTimeFormat(CLOCK_12H)
|
||||
.build()
|
||||
.apply {
|
||||
addOnPositiveButtonClickListener {
|
||||
Settings.putInt(Settings.KEY_REQUEST_NEWS_TIMER_HOUR, hour)
|
||||
Settings.putInt(Settings.KEY_REQUEST_NEWS_TIMER_MINUTE, minute)
|
||||
updateDailyCheckWork(requireContext())
|
||||
}
|
||||
}
|
||||
.show(childFragmentManager, null)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
@@ -69,7 +104,13 @@ class EhFragment : BasePreferenceFragment() {
|
||||
return true
|
||||
} else if (Settings.KEY_GALLERY_SITE == key) {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
launchIO { runCatching { EhEngine.getUConfig() } }
|
||||
lifecycleScope.launchNonCancellable {
|
||||
runCatching {
|
||||
EhEngine.getUConfig()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else if (Settings.KEY_LIST_MODE == key) {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
@@ -91,6 +132,10 @@ class EhFragment : BasePreferenceFragment() {
|
||||
EhApplication.application.recreateAllActivity()
|
||||
}
|
||||
return true
|
||||
} else if (Settings.KEY_REQUEST_NEWS == key) {
|
||||
updateDailyCheckWork(requireContext())
|
||||
} else if (Settings.KEY_REQUEST_NEWS_TIMER == key) {
|
||||
updateDailyCheckWork(requireContext())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
@@ -415,7 +415,7 @@ class FilterFragment : BaseFragment() {
|
||||
mEhFilter.deleteFilter(filter)
|
||||
}
|
||||
|
||||
fun trigger(filter: Filter?) {
|
||||
fun trigger(filter: Filter) {
|
||||
mEhFilter.triggerFilter(filter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
@@ -46,7 +45,6 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
@@ -55,15 +53,16 @@ import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Cookie
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CookieSignInScene(navController: NavController) {
|
||||
fun CookieSignInScene() {
|
||||
val navController = LocalNavController.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
@@ -92,12 +91,11 @@ fun CookieSignInScene(navController: NavController) {
|
||||
fun storeCookie(id: String, hash: String, igneous: String) {
|
||||
EhUtils.signOut()
|
||||
val store = EhApplication.ehCookieStore
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_EX))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_EX))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_EX))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_EX))
|
||||
if (igneous.isBlank()) return
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_E))
|
||||
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX))
|
||||
}
|
||||
|
||||
@@ -122,6 +120,9 @@ fun CookieSignInScene(navController: NavController) {
|
||||
runCatching {
|
||||
EhEngine.getProfile()
|
||||
}.onSuccess {
|
||||
withNonCancellableContext {
|
||||
postLogin()
|
||||
}
|
||||
withUIContext {
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,24 @@ package com.hippo.ehviewer.ui.login
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.ui.EhActivity
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LoginActivity : EhActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -16,22 +28,29 @@ class LoginActivity : EhActivity() {
|
||||
Mdc3Theme {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(navController = navController, startDestination = SIGN_IN_ROUTE_NAME) {
|
||||
composable(SIGN_IN_ROUTE_NAME) {
|
||||
SignInScreen(navController)
|
||||
}
|
||||
CompositionLocalProvider(LocalNavController provides navController) {
|
||||
NavHost(navController = navController, startDestination = SIGN_IN_ROUTE_NAME) {
|
||||
composable(SIGN_IN_ROUTE_NAME) {
|
||||
SignInScreen()
|
||||
}
|
||||
|
||||
composable(WEBVIEW_SIGN_IN_ROUTE_NAME) {
|
||||
WebviewSignInScreen(navController)
|
||||
}
|
||||
composable(WEBVIEW_SIGN_IN_ROUTE_NAME) {
|
||||
WebviewSignInScreen()
|
||||
}
|
||||
|
||||
composable(COOKIE_SIGN_IN_ROUTE_NAME) {
|
||||
CookieSignInScene(navController)
|
||||
}
|
||||
composable(COOKIE_SIGN_IN_ROUTE_NAME) {
|
||||
CookieSignInScene()
|
||||
}
|
||||
|
||||
composable(SELECT_SITE_ROUTE_NAME) {
|
||||
SelectSiteScreen {
|
||||
finish()
|
||||
composable(SELECT_SITE_ROUTE_NAME) {
|
||||
val selectSite by remember { mutableStateOf(Settings.getSelectSite()) }
|
||||
if (selectSite) {
|
||||
SelectSiteScreen()
|
||||
} else {
|
||||
SideEffect {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,10 +60,47 @@ class LoginActivity : EhActivity() {
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
Settings.putNeedSignIn(false)
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun postLogin() = coroutineScope {
|
||||
launch {
|
||||
runCatching {
|
||||
EhEngine.getProfile().run {
|
||||
Settings.putDisplayName(displayName)
|
||||
Settings.putAvatar(avatar)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
runCatching {
|
||||
// For the `star` cookie
|
||||
EhEngine.getNews(false)
|
||||
|
||||
// Sad panda check
|
||||
Settings.putGallerySite(EhUrl.SITE_EX)
|
||||
EhEngine.getUConfig()
|
||||
}.onFailure {
|
||||
Settings.putSelectSite(false)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
launch {
|
||||
runCatching {
|
||||
EhEngine.getUConfig()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.join()
|
||||
}
|
||||
|
||||
val LocalNavController =
|
||||
compositionLocalOf<NavController> { error("CompositionLocal LocalNavController not present!") }
|
||||
|
||||
const val SIGN_IN_ROUTE_NAME = "SignIn"
|
||||
const val WEBVIEW_SIGN_IN_ROUTE_NAME = "WebViewSignIn"
|
||||
const val COOKIE_SIGN_IN_ROUTE_NAME = "CookieSignIn"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -24,24 +25,26 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
|
||||
@Composable
|
||||
fun SelectSiteScreen(finish: () -> Unit) {
|
||||
var site by remember { mutableStateOf(EhApplication.ehCookieStore.hasSignedIn()) } // false for e, true for ex
|
||||
fun SelectSiteScreen() {
|
||||
val activity = LocalContext.current as Activity
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var siteEx by remember { mutableStateOf(true) }
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
@@ -70,8 +73,8 @@ fun SelectSiteScreen(finish: () -> Unit) {
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(if (!site) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.clickable { site = false }
|
||||
.background(if (!siteEx) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.clickable { siteEx = false }
|
||||
.size(100.dp, 40.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -89,8 +92,8 @@ fun SelectSiteScreen(finish: () -> Unit) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(if (site) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.clickable { site = true }
|
||||
.background(if (siteEx) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.clickable { siteEx = true }
|
||||
.size(100.dp, 40.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -116,28 +119,23 @@ fun SelectSiteScreen(finish: () -> Unit) {
|
||||
) {
|
||||
Button(onClick = {
|
||||
Settings.putSelectSite(false)
|
||||
Settings.putNeedSignIn(false)
|
||||
if (!site)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
else
|
||||
Settings.putGallerySite(EhUrl.SITE_EX)
|
||||
GlobalScope.launchIO {
|
||||
getProfile()
|
||||
coroutineScope.launchNonCancellable {
|
||||
runCatching {
|
||||
if (!siteEx) {
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
EhEngine.getUConfig()
|
||||
} else {
|
||||
// Get cookies for image limits
|
||||
EhEngine.getUConfig(EhUrl.URL_UCONFIG_E)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
finish()
|
||||
activity.finish()
|
||||
}, Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getProfile() {
|
||||
runCatching {
|
||||
EhEngine.getProfile().run {
|
||||
Settings.putDisplayName(displayName)
|
||||
Settings.putAvatar(avatar)
|
||||
}
|
||||
EhEngine.getUConfig()
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -51,21 +50,22 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.UrlOpener
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import rikka.core.util.ContextUtils.requireActivity
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SignInScreen(navController: NavController) {
|
||||
fun SignInScreen() {
|
||||
val navController = LocalNavController.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
var isProgressIndicatorVisible by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -110,6 +110,9 @@ fun SignInScreen(navController: NavController) {
|
||||
showErrorDialog = true
|
||||
}
|
||||
}.onSuccess {
|
||||
withNonCancellableContext {
|
||||
postLogin()
|
||||
}
|
||||
withUIContext {
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
}
|
||||
@@ -248,7 +251,11 @@ fun SignInScreen(navController: NavController) {
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = { navController.navigate(SELECT_SITE_ROUTE_NAME) },
|
||||
onClick = {
|
||||
Settings.putSelectSite(false)
|
||||
Settings.putGallerySite(EhUrl.SITE_E)
|
||||
navController.navigate(SELECT_SITE_ROUTE_NAME)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
|
||||
Loaded 100 of 208 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user