Compare commits
107
Commits
1.11.6-RC1
...
1.11.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b538bbd3 | ||
|
|
d00d700a4b | ||
|
|
ae66318843 | ||
|
|
1b9aa6a762 | ||
|
|
306ce83de4 | ||
|
|
89f9735ab9 | ||
|
|
843e1cc6a0 | ||
|
|
1a53d0d207 | ||
|
|
45c02d4fd6 | ||
|
|
862dee0845 | ||
|
|
809bbfeba7 | ||
|
|
959c740175 | ||
|
|
a718510c89 | ||
|
|
f7c2fd8af3 | ||
|
|
c76440f167 | ||
|
|
c3bd959ee5 | ||
|
|
0a25643b87 | ||
|
|
c805ca8aff | ||
|
|
699a1ab801 | ||
|
|
ee091aee0f | ||
|
|
673c1837e9 | ||
|
|
3998913c17 | ||
|
|
1e83d7128e | ||
|
|
355289a576 | ||
|
|
ba3a41f0ee | ||
|
|
fc6d0bd6ef | ||
|
|
15ef0ae362 | ||
|
|
a598f8562d | ||
|
|
2860fed5ab | ||
|
|
f247544439 | ||
|
|
fdc3d94d5c | ||
|
|
afc2ff7478 | ||
|
|
83072d4eee | ||
|
|
7e7be8d40a | ||
|
|
90d7555333 | ||
|
|
ba268cfe6d | ||
|
|
0a7a9beb84 | ||
|
|
b18b89db29 | ||
|
|
cb2d99bdc1 | ||
|
|
4dd6d68163 | ||
|
|
28ecc9d479 | ||
|
|
c917117db3 | ||
|
|
5c0d65dd60 | ||
|
|
dfc59aa643 | ||
|
|
6ab77e541f | ||
|
|
6224f8f4ab | ||
|
|
3d269c4220 | ||
|
|
176a802e96 | ||
|
|
417a06e140 | ||
|
|
5c4f034532 | ||
|
|
1d50855f52 | ||
|
|
0280450797 | ||
|
|
d0c907c7ff | ||
|
|
1302e17c01 | ||
|
|
33f83b7325 | ||
|
|
d2c49f5022 | ||
|
|
084883a308 | ||
|
|
67a73da720 | ||
|
|
f2447fd122 | ||
|
|
8ddcb003e0 | ||
|
|
0bff011f72 | ||
|
|
f41d1d7306 | ||
|
|
a30aa23b67 | ||
|
|
583d30eb5f | ||
|
|
c53b0986a9 | ||
|
|
85703c8c47 | ||
|
|
025ba0bf5b | ||
|
|
ac2def22d2 | ||
|
|
895cfc7bc6 | ||
|
|
49eb9d5077 | ||
|
|
5a3c165ff1 | ||
|
|
3946428df6 | ||
|
|
471d9784d6 | ||
|
|
0ba0afc0d0 | ||
|
|
5049b791fb | ||
|
|
9a3715c516 | ||
|
|
4ee276f636 | ||
|
|
d528b2d5d0 | ||
|
|
b3ee69d193 | ||
|
|
f882b78ff6 | ||
|
|
7e58101a3f | ||
|
|
5450eb9b2b | ||
|
|
69695e3c99 | ||
|
|
5b15059c04 | ||
|
|
f50d69cd60 | ||
|
|
df6f783a69 | ||
|
|
f0291d94f9 | ||
|
|
593f24b86f | ||
|
|
5bce898f99 | ||
|
|
2b444c262d | ||
|
|
e40a7c75d9 | ||
|
|
4a7c057260 | ||
|
|
229a00cd53 | ||
|
|
d2d0d880f1 | ||
|
|
c5e17c1d5b | ||
|
|
33ff2ebe06 | ||
|
|
5c13e188de | ||
|
|
60b03dcd0e | ||
|
|
4604a25f58 | ||
|
|
99029b8b04 | ||
|
|
70f46442c6 | ||
|
|
13cf42437f | ||
|
|
1e29f6dcb4 | ||
|
|
f25981792f | ||
|
|
934c8be3e4 | ||
|
|
b9994eacbc | ||
|
|
704ab0ee7e |
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
*.sh eol=lf
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: Baseline profile generation
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 0 * * 0'
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'benchmark/**'
|
||||||
|
- '.github/workflows/baseline-profile.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
baseline-profile:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Free disk space
|
||||||
|
run: rm -rf ${ANDROID_SDK_ROOT}/ndk
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||||
|
|
||||||
|
- name: Install Rust Toolchain
|
||||||
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: "./app/src/main/rust"
|
||||||
|
shared-key: ${{ runner.os }}
|
||||||
|
save-if: "false"
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
with:
|
||||||
|
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||||
|
cache-read-only: true
|
||||||
|
|
||||||
|
- name: CMake Cache
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
path: app/.cxx
|
||||||
|
key: cmake-
|
||||||
|
|
||||||
|
- name: Enable KVM group perms
|
||||||
|
run: |
|
||||||
|
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo udevadm trigger --name-match=kvm
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew generateBaselineProfile
|
||||||
|
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||||
|
|
||||||
|
- name: Update baseline profile
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config push.autoSetupRemote true
|
||||||
|
git switch -c chore/baseline-profile
|
||||||
|
if git commit -am "Update baseline profile"; then
|
||||||
|
git push
|
||||||
|
gh pr create -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: reports
|
||||||
|
path: benchmark/build/reports
|
||||||
@@ -43,11 +43,14 @@ jobs:
|
|||||||
cache-read-only: true
|
cache-read-only: true
|
||||||
|
|
||||||
- name: Install NDK
|
- name: Install NDK
|
||||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014"
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||||
|
|
||||||
- name: Spotless Check
|
- name: Spotless Check
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
||||||
|
- name: Lint Check
|
||||||
|
run: ./gradlew lintMarshmallowRelease
|
||||||
|
|
||||||
default:
|
default:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -61,7 +64,7 @@ jobs:
|
|||||||
java-version: 21
|
java-version: 21
|
||||||
|
|
||||||
- name: Install NDK
|
- name: Install NDK
|
||||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014"
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||||
@@ -87,6 +90,8 @@ jobs:
|
|||||||
restore-keys: cmake-default-
|
restore-keys: cmake-default-
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GithubToken }}
|
||||||
run: ./gradlew assembleDefaultRelease
|
run: ./gradlew assembleDefaultRelease
|
||||||
|
|
||||||
- name: Upload ARM64
|
- name: Upload ARM64
|
||||||
@@ -111,7 +116,7 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: default-mapping-${{ github.sha }}
|
name: default-mapping-${{ github.sha }}
|
||||||
path: app/build/outputs/mapping/defaultRelease/mapping.txt
|
path: app/build/outputs/mapping/defaultRelease
|
||||||
|
|
||||||
- name: Upload native debug symbols
|
- name: Upload native debug symbols
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -132,7 +137,7 @@ jobs:
|
|||||||
java-version: 21
|
java-version: 21
|
||||||
|
|
||||||
- name: Install NDK
|
- name: Install NDK
|
||||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014"
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||||
@@ -158,6 +163,8 @@ jobs:
|
|||||||
restore-keys: cmake-marshmallow-
|
restore-keys: cmake-marshmallow-
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GithubToken }}
|
||||||
run: ./gradlew assembleMarshmallowRelease
|
run: ./gradlew assembleMarshmallowRelease
|
||||||
|
|
||||||
- name: Upload ARM64
|
- name: Upload ARM64
|
||||||
@@ -182,7 +189,7 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: marshmallow-mapping-${{ github.sha }}
|
name: marshmallow-mapping-${{ github.sha }}
|
||||||
path: app/build/outputs/mapping/marshmallowRelease/mapping.txt
|
path: app/build/outputs/mapping/marshmallowRelease
|
||||||
|
|
||||||
- name: Upload native debug symbols
|
- name: Upload native debug symbols
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: 'Lock Threads'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
- cron: '0 8 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
java-version: 21
|
java-version: 21
|
||||||
|
|
||||||
- name: Install NDK
|
- name: Install NDK
|
||||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014"
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android
|
||||||
@@ -38,6 +38,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Gradle Build
|
- name: Gradle Build
|
||||||
uses: gradle/actions/setup-gradle@v3
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GithubToken }}
|
||||||
with:
|
with:
|
||||||
arguments: assembleRelease
|
arguments: assembleRelease
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,35 @@
|
|||||||
# 变更日志
|
# 变更日志
|
||||||
|
|
||||||
|
## [1.11.6] - 2024-06-24
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
* 某些设备上无法加载长图时可尝试减小 设置-高级 中的硬件位图阈值 [#1321](https://github.com/FooIbar/EhViewer/issues/1321)
|
||||||
|
* 清理下载冗余时显示确认对话框 [#1369](https://github.com/FooIbar/EhViewer/issues/1369)
|
||||||
|
* 修改收藏时显示收藏备注 [#1392](https://github.com/FooIbar/EhViewer/issues/1392)
|
||||||
|
|
||||||
|
### Bug 修复
|
||||||
|
|
||||||
|
* 图片完整性校验误报 [#1286](https://github.com/FooIbar/EhViewer/issues/1286)
|
||||||
|
* 某些情况下应用崩溃 [#1290](https://github.com/FooIbar/EhViewer/issues/1290) [#1302](https://github.com/FooIbar/EhViewer/issues/1302)
|
||||||
|
* 某些情况下渲染问题 [#1284](https://github.com/FooIbar/EhViewer/issues/1284)
|
||||||
|
* 无法下载标题过长的画廊 [#1340](https://github.com/FooIbar/EhViewer/issues/1340)
|
||||||
|
* 某些界面拖动滚动条时黑屏 [#1329](https://github.com/FooIbar/EhViewer/issues/1329)
|
||||||
|
* 搜索栏多行文本遮挡其他内容 [#1365](https://github.com/FooIbar/EhViewer/issues/1365)
|
||||||
|
* CIFS Documents Provider 兼容问题 [#1356](https://github.com/FooIbar/EhViewer/issues/1356)
|
||||||
|
* 清理下载冗余未按预期运行 [#1369](https://github.com/FooIbar/EhViewer/issues/1369)
|
||||||
|
* 读取压缩包失败时崩溃
|
||||||
|
* 某些设备上崩溃 [#1276](https://github.com/FooIbar/EhViewer/issues/1276)
|
||||||
|
* 检查更新失效
|
||||||
|
|
||||||
|
|
||||||
|
## [1.11.6-RC2] - 2024-06-06
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
* 下载图片时执行完整性校验以防止文件损坏/被篡改
|
||||||
|
|
||||||
|
|
||||||
## [1.11.6-RC1] - 2024-06-02
|
## [1.11.6-RC1] - 2024-06-02
|
||||||
|
|
||||||
### 改进
|
### 改进
|
||||||
|
|||||||
+36
-11
@@ -1,3 +1,4 @@
|
|||||||
|
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
import com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
import com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
||||||
|
|
||||||
@@ -14,13 +15,15 @@ plugins {
|
|||||||
alias(libs.plugins.spotless)
|
alias(libs.plugins.spotless)
|
||||||
alias(libs.plugins.aboutlibrariesPlugin)
|
alias(libs.plugins.aboutlibrariesPlugin)
|
||||||
alias(libs.plugins.composeCompilerReportGenerator)
|
alias(libs.plugins.composeCompilerReportGenerator)
|
||||||
|
alias(libs.plugins.baselineprofile)
|
||||||
}
|
}
|
||||||
|
|
||||||
val supportedAbis = arrayOf("arm64-v8a", "x86_64", "armeabi-v7a")
|
val supportedAbis = arrayOf("arm64-v8a", "x86_64", "armeabi-v7a")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 34
|
compileSdk = if (isRelease) 35 else 34
|
||||||
ndkVersion = "27.0.11718014-beta1"
|
buildToolsVersion = "35.0.0"
|
||||||
|
ndkVersion = "27.0.11902837-rc1"
|
||||||
androidResources.generateLocaleConfig = true
|
androidResources.generateLocaleConfig = true
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
@@ -61,13 +64,16 @@ android {
|
|||||||
val chromeVersion = rootProject.layout.projectDirectory.file("chrome-for-testing/LATEST_RELEASE_STABLE").asFile
|
val chromeVersion = rootProject.layout.projectDirectory.file("chrome-for-testing/LATEST_RELEASE_STABLE").asFile
|
||||||
.readText().substringBefore('.')
|
.readText().substringBefore('.')
|
||||||
|
|
||||||
|
val githubToken = gradleLocalProperties(rootDir, providers)["GITHUB_TOKEN"] as? String
|
||||||
|
?: System.getenv("GITHUB_TOKEN").orEmpty()
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "moe.tarsin.ehviewer"
|
applicationId = "moe.tarsin.ehviewer"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
versionCode = 180056
|
versionCode = 180057
|
||||||
versionName = "1.11.6"
|
versionName = "1.11.6"
|
||||||
versionNameSuffix = "-RC1"
|
// versionNameSuffix = "-SNAPSHOT"
|
||||||
resourceConfigurations.addAll(
|
resourceConfigurations.addAll(
|
||||||
listOf(
|
listOf(
|
||||||
"zh",
|
"zh",
|
||||||
@@ -89,6 +95,7 @@ android {
|
|||||||
buildConfigField("long", "COMMIT_TIME", commitTime)
|
buildConfigField("long", "COMMIT_TIME", commitTime)
|
||||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||||
buildConfigField("String", "CHROME_VERSION", "\"$chromeVersion\"")
|
buildConfigField("String", "CHROME_VERSION", "\"$chromeVersion\"")
|
||||||
|
buildConfigField("String", "GITHUB_TOKEN", "\"$githubToken\"")
|
||||||
ndk {
|
ndk {
|
||||||
if (isRelease) {
|
if (isRelease) {
|
||||||
abiFilters.addAll(supportedAbis)
|
abiFilters.addAll(supportedAbis)
|
||||||
@@ -124,8 +131,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
|
checkReleaseBuilds = false
|
||||||
disable += setOf("MissingTranslation", "MissingQuantity")
|
disable += setOf("MissingTranslation", "MissingQuantity")
|
||||||
fatal += setOf("NewApi", "InlinedApi")
|
error += setOf("InlinedApi")
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
@@ -149,6 +157,13 @@ android {
|
|||||||
abortOnError = false
|
abortOnError = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
create("benchmarkRelease") {
|
||||||
|
initWith(buildTypes.getByName("release"))
|
||||||
|
matchingFallbacks += listOf("release")
|
||||||
|
applicationIdSuffix = ".benchmark"
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
isDebuggable = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
@@ -176,6 +191,10 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baselineProfile {
|
||||||
|
mergeIntoMain = true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
@@ -205,14 +224,14 @@ dependencies {
|
|||||||
implementation(libs.androidx.paging.compose)
|
implementation(libs.androidx.paging.compose)
|
||||||
|
|
||||||
implementation(libs.androidx.recyclerview)
|
implementation(libs.androidx.recyclerview)
|
||||||
|
implementation(libs.androidx.viewpager2)
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/room
|
// https://developer.android.com/jetpack/androidx/releases/room
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
implementation(libs.androidx.room.paging)
|
implementation(libs.androidx.room.paging)
|
||||||
|
|
||||||
implementation(libs.androidx.work.runtime)
|
implementation(libs.androidx.work.runtime)
|
||||||
implementation(libs.photoview) // Dead Dependency
|
implementation(libs.photoview)
|
||||||
implementation(libs.directionalviewpager) // Dead Dependency
|
|
||||||
implementation(libs.material.motion.core)
|
implementation(libs.material.motion.core)
|
||||||
|
|
||||||
implementation(libs.bundles.splitties)
|
implementation(libs.bundles.splitties)
|
||||||
@@ -229,10 +248,11 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.insetter) // Dead Dependency
|
implementation(libs.insetter) // Dead Dependency
|
||||||
|
|
||||||
implementation(libs.reorderable)
|
// implementation(libs.reorderable)
|
||||||
|
|
||||||
implementation(platform(libs.arrow.stack))
|
implementation(platform(libs.arrow.stack))
|
||||||
implementation(libs.arrow.fx.coroutines)
|
implementation(libs.arrow.fx.coroutines)
|
||||||
|
implementation(libs.arrow.resilience)
|
||||||
|
|
||||||
// https://coil-kt.github.io/coil/changelog/
|
// https://coil-kt.github.io/coil/changelog/
|
||||||
implementation(platform(libs.coil.bom))
|
implementation(platform(libs.coil.bom))
|
||||||
@@ -252,6 +272,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.cronet.embedded)
|
implementation(libs.cronet.embedded)
|
||||||
|
|
||||||
|
implementation(libs.androidx.profileinstaller)
|
||||||
|
"baselineProfile"(project(":benchmark"))
|
||||||
|
|
||||||
debugImplementation(libs.compose.ui.tooling)
|
debugImplementation(libs.compose.ui.tooling)
|
||||||
implementation(libs.compose.ui.tooling.preview)
|
implementation(libs.compose.ui.tooling.preview)
|
||||||
}
|
}
|
||||||
@@ -293,13 +316,15 @@ aboutLibraries {
|
|||||||
duplicationRule = GROUP
|
duplicationRule = GROUP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val ktlintVersion = libs.ktlint.get().version
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
kotlin {
|
kotlin {
|
||||||
// https://github.com/diffplug/spotless/issues/111
|
// https://github.com/diffplug/spotless/issues/111
|
||||||
target("src/**/*.kt")
|
target("src/**/*.kt")
|
||||||
ktlint()
|
ktlint(ktlintVersion)
|
||||||
}
|
}
|
||||||
kotlinGradle {
|
kotlinGradle {
|
||||||
ktlint()
|
ktlint(ktlintVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+5
@@ -11,6 +11,11 @@
|
|||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# https://issuetracker.google.com/347963245
|
||||||
|
-keep class * extends androidx.room.RoomDatabase {
|
||||||
|
<init>();
|
||||||
|
}
|
||||||
|
|
||||||
# Ktor logger
|
# Ktor logger
|
||||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
From 2fa97f75b64c05da506968fa9a1711716f2d45a0 Mon Sep 17 00:00:00 2001
|
|
||||||
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
|
||||||
Date: Tue, 26 Sep 2023 00:08:03 +0800
|
|
||||||
Subject: [PATCH 1/2] Hide symbols
|
|
||||||
|
|
||||||
---
|
|
||||||
CMakeLists.txt | 4 ++--
|
|
||||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
||||||
index ec97e4c7..4d859341 100644
|
|
||||||
--- a/CMakeLists.txt
|
|
||||||
+++ b/CMakeLists.txt
|
|
||||||
@@ -2129,10 +2129,10 @@ SET(OLD_CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS}")
|
|
||||||
SET(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -fvisibility=hidden -Werror")
|
|
||||||
CHECK_C_SOURCE_COMPILES("void __attribute__((visibility(\"default\"))) foo(void);
|
|
||||||
int main() { return 0; }" HAVE_VISIBILITY_ATTR)
|
|
||||||
-IF (HAVE_VISIBILITY_ATTR)
|
|
||||||
+IF (0)
|
|
||||||
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
|
|
||||||
ADD_DEFINITIONS(-D__LIBARCHIVE_ENABLE_VISIBILITY)
|
|
||||||
-ENDIF(HAVE_VISIBILITY_ATTR)
|
|
||||||
+ENDIF(0)
|
|
||||||
SET(CMAKE_REQUIRED_FLAGS "${OLD_CMAKE_REQUIRED_FLAGS}")
|
|
||||||
|
|
||||||
# Generate "config.h" from "build/cmake/config.h.in"
|
|
||||||
--
|
|
||||||
2.34.1
|
|
||||||
|
|
||||||
+3
-3
@@ -1,14 +1,14 @@
|
|||||||
From 2dcc8be937499022c37678eb6bf4bd026b6fcbb0 Mon Sep 17 00:00:00 2001
|
From aee8f7147ddb262fd9e5feee8d5b17094cf3470f Mon Sep 17 00:00:00 2001
|
||||||
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||||
Date: Tue, 26 Sep 2023 00:09:25 +0800
|
Date: Tue, 26 Sep 2023 00:09:25 +0800
|
||||||
Subject: [PATCH 2/2] Insert link libs
|
Subject: [PATCH] Insert link libs
|
||||||
|
|
||||||
---
|
---
|
||||||
CMakeLists.txt | 2 +-
|
CMakeLists.txt | 2 +-
|
||||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||||
index 4d859341..1cd1cd47 100644
|
index ec97e4c7..420f204c 100644
|
||||||
--- a/CMakeLists.txt
|
--- a/CMakeLists.txt
|
||||||
+++ b/CMakeLists.txt
|
+++ b/CMakeLists.txt
|
||||||
@@ -438,7 +438,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
|
@@ -438,7 +438,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 3.14)
|
||||||
project(ehviewer C)
|
project(ehviewer C)
|
||||||
|
include(ExternalProject)
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
@@ -18,31 +19,47 @@ endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
|||||||
|
|
||||||
option(BUILD_TESTING OFF)
|
option(BUILD_TESTING OFF)
|
||||||
|
|
||||||
|
set(XZ_PATCH ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch)
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
liblzma
|
liblzma
|
||||||
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
|
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
|
||||||
GIT_TAG v5.6.2
|
GIT_TAG v5.6.2
|
||||||
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch || cd .
|
PATCH_COMMAND git apply --check -R ${XZ_PATCH} || git apply ${XZ_PATCH}
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(liblzma)
|
FetchContent_MakeAvailable(liblzma)
|
||||||
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
|
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
|
||||||
|
|
||||||
# Build GNUTLS libnettle
|
# Build GNUTLS libnettle
|
||||||
FetchContent_Declare(
|
if (CMAKE_HOST_SYSTEM_NAME STREQUAL Windows)
|
||||||
|
execute_process(
|
||||||
|
COMMAND wsl wslpath ${CMAKE_CURRENT_LIST_DIR}
|
||||||
|
OUTPUT_VARIABLE CURRENT_DIR
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
)
|
||||||
|
else ()
|
||||||
|
set(CURRENT_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||||
|
endif ()
|
||||||
|
set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}")
|
||||||
|
ExternalProject_Add(
|
||||||
nettle
|
nettle
|
||||||
URL https://ftp.gnu.org/gnu/nettle/nettle-3.9.1.tar.gz
|
PREFIX nettle
|
||||||
URL_MD5 29fcd2dec6bf5b48e5e3ffb3cbc4779e
|
URL https://ftp.gnu.org/gnu/nettle/nettle-3.10.tar.gz
|
||||||
SOURCE_DIR nettle-src/nettle
|
URL_MD5 c61453139d5fb44e9cdcc5b684b26e55
|
||||||
|
CONFIGURE_COMMAND bash ${CURRENT_DIR}/configure_nettle.sh ${NDK_VERSION} ${CMAKE_C_COMPILER_TARGET}
|
||||||
|
BUILD_COMMAND bash -c "make libnettle.a CFLAGS='${CMAKE_C_FLAGS}'"
|
||||||
|
INSTALL_COMMAND ""
|
||||||
|
BUILD_IN_SOURCE 1
|
||||||
|
BUILD_BYPRODUCTS nettle/src/nettle/libnettle.a
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(nettle)
|
ExternalProject_Get_Property(nettle SOURCE_DIR)
|
||||||
set(NETTLE_DIR ${nettle_SOURCE_DIR})
|
include_directories(${SOURCE_DIR}/..)
|
||||||
add_subdirectory(nettle)
|
add_library(libnettle STATIC IMPORTED)
|
||||||
|
set_property(TARGET libnettle PROPERTY IMPORTED_LOCATION ${SOURCE_DIR}/libnettle.a)
|
||||||
|
add_dependencies(libnettle nettle)
|
||||||
|
|
||||||
# Configure libnettle support for libarchive
|
# Configure libnettle support for libarchive
|
||||||
include_directories(${CMAKE_CURRENT_BINARY_DIR}/nettle-src)
|
|
||||||
include_directories(.)
|
|
||||||
set(HAVE_LIBNETTLE 1)
|
set(HAVE_LIBNETTLE 1)
|
||||||
set(HAVE_NETTLE_AES_H 1)
|
set(HAVE_NETTLE_AES_H 1)
|
||||||
set(HAVE_NETTLE_HMAC_H 1)
|
set(HAVE_NETTLE_HMAC_H 1)
|
||||||
@@ -58,6 +75,7 @@ SET(HAVE_LZMA_STREAM_ENCODER_MT 1)
|
|||||||
SET(HAVE_LZMADEC_H 1)
|
SET(HAVE_LZMADEC_H 1)
|
||||||
SET(HAVE_LIBLZMADEC 1)
|
SET(HAVE_LIBLZMADEC 1)
|
||||||
|
|
||||||
|
option(ENABLE_OPENSSL OFF)
|
||||||
option(ENABLE_TAR OFF)
|
option(ENABLE_TAR OFF)
|
||||||
option(ENABLE_CPIO OFF)
|
option(ENABLE_CPIO OFF)
|
||||||
option(ENABLE_CAT OFF)
|
option(ENABLE_CAT OFF)
|
||||||
@@ -65,13 +83,14 @@ option(ENABLE_UNZIP OFF)
|
|||||||
option(ENABLE_TEST OFF)
|
option(ENABLE_TEST OFF)
|
||||||
|
|
||||||
# Configure libarchive link's static lib
|
# Configure libarchive link's static lib
|
||||||
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
|
SET(LIBARCHIVE_CUSTOM_LIBS "libnettle" "liblzma")
|
||||||
|
|
||||||
|
set(LIBARCHIVE_PATCH ${CMAKE_CURRENT_LIST_DIR}/0001-Insert-link-libs.patch)
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
libarchive
|
libarchive
|
||||||
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
||||||
GIT_TAG v3.7.4
|
GIT_TAG v3.7.4
|
||||||
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Hide-symbols.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || cd .
|
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(libarchive)
|
FetchContent_MakeAvailable(libarchive)
|
||||||
@@ -88,5 +107,5 @@ corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml)
|
|||||||
corrosion_add_target_local_rustflags(ehviewer_rust "-Clink-args=-Wl,-z,max-page-size=16384")
|
corrosion_add_target_local_rustflags(ehviewer_rust "-Clink-args=-Wl,-z,max-page-size=16384")
|
||||||
|
|
||||||
# Build and link our app's native lib
|
# Build and link our app's native lib
|
||||||
add_library(${PROJECT_NAME} SHARED archive.c border.c gifutils.c natsort/strnatcmp.c)
|
add_library(${PROJECT_NAME} SHARED archive.c border.c gifutils.c hash.c natsort/strnatcmp.c)
|
||||||
target_link_libraries(${PROJECT_NAME} ehviewer_rust archive_static log jnigraphics)
|
target_link_libraries(${PROJECT_NAME} ehviewer_rust archive_static log jnigraphics -Wl,--exclude-libs,ALL)
|
||||||
@@ -49,7 +49,7 @@ typedef struct {
|
|||||||
} entry;
|
} entry;
|
||||||
|
|
||||||
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static archive_ctx **ctx_pool;
|
static archive_ctx **ctx_pool = NULL;
|
||||||
#define CTX_POOL_SIZE 20
|
#define CTX_POOL_SIZE 20
|
||||||
|
|
||||||
static void *mempool = NULL;
|
static void *mempool = NULL;
|
||||||
@@ -403,6 +403,7 @@ JNIEXPORT void JNICALL
|
|||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
|
if (!ctx_pool) return;
|
||||||
for (int i = 0; i < CTX_POOL_SIZE; i++)
|
for (int i = 0; i < CTX_POOL_SIZE; i++)
|
||||||
archive_release_ctx(ctx_pool[i]);
|
archive_release_ctx(ctx_pool[i]);
|
||||||
free(passwd);
|
free(passwd);
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
TOOLCHAIN=$ANDROID_HOME/ndk/$1/toolchains/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64/bin
|
||||||
|
TARGET=$2
|
||||||
|
./configure --host=$TARGET --disable-public-key --disable-dependency-tracking \
|
||||||
|
AR=$TOOLCHAIN/llvm-ar \
|
||||||
|
CC="$TOOLCHAIN/clang -target $TARGET" \
|
||||||
|
RANLIB=$TOOLCHAIN/llvm-ranlib
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
#include <nettle/sha1.h>
|
||||||
|
|
||||||
|
#include "ehviewer.h"
|
||||||
|
|
||||||
|
#define BUFFER_SIZE 8192
|
||||||
|
|
||||||
|
typedef uint8_t byte;
|
||||||
|
|
||||||
|
const char hex_digits[] = "0123456789abcdef";
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL
|
||||||
|
Java_com_hippo_ehviewer_jni_HashKt_sha1(JNIEnv *env, jclass clazz, jint fd) {
|
||||||
|
EH_UNUSED(clazz);
|
||||||
|
struct sha1_ctx ctx;
|
||||||
|
sha1_init(&ctx);
|
||||||
|
|
||||||
|
size_t bytes_read;
|
||||||
|
byte buffer[BUFFER_SIZE];
|
||||||
|
while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) {
|
||||||
|
sha1_update(&ctx, bytes_read, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte digest[SHA1_DIGEST_SIZE];
|
||||||
|
sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest);
|
||||||
|
|
||||||
|
byte byte;
|
||||||
|
char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
|
||||||
|
for (int i = 0; i < SHA1_DIGEST_SIZE; i++) {
|
||||||
|
byte = digest[i];
|
||||||
|
hex_digest[2 * i] = hex_digits[byte >> 4 & 0xF];
|
||||||
|
hex_digest[2 * i + 1] = hex_digits[byte & 0xF];
|
||||||
|
}
|
||||||
|
hex_digest[2 * SHA1_DIGEST_SIZE] = '\0';
|
||||||
|
|
||||||
|
return (*env)->NewStringUTF(env, hex_digest);
|
||||||
|
}
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.4.1)
|
|
||||||
project(nettle C)
|
|
||||||
|
|
||||||
set(LIBNETTLE_DEFINITIONS
|
|
||||||
-DHAVE_CONFIG_H)
|
|
||||||
|
|
||||||
set(LIBNETTLE_SOURCES
|
|
||||||
${NETTLE_DIR}/aes-decrypt-internal.c
|
|
||||||
${NETTLE_DIR}/aes-decrypt.c
|
|
||||||
${NETTLE_DIR}/aes-decrypt-table.c
|
|
||||||
${NETTLE_DIR}/aes128-decrypt.c
|
|
||||||
${NETTLE_DIR}/aes192-decrypt.c
|
|
||||||
${NETTLE_DIR}/aes256-decrypt.c
|
|
||||||
${NETTLE_DIR}/aes-encrypt-internal.c
|
|
||||||
${NETTLE_DIR}/aes-encrypt.c
|
|
||||||
${NETTLE_DIR}/aes-encrypt-table.c
|
|
||||||
${NETTLE_DIR}/aes128-encrypt.c
|
|
||||||
${NETTLE_DIR}/aes192-encrypt.c
|
|
||||||
${NETTLE_DIR}/aes256-encrypt.c
|
|
||||||
${NETTLE_DIR}/aes-invert-internal.c
|
|
||||||
${NETTLE_DIR}/aes-set-key-internal.c
|
|
||||||
${NETTLE_DIR}/aes-set-encrypt-key.c
|
|
||||||
${NETTLE_DIR}/aes-set-decrypt-key.c
|
|
||||||
${NETTLE_DIR}/aes128-set-encrypt-key.c
|
|
||||||
${NETTLE_DIR}/aes128-set-decrypt-key.c
|
|
||||||
${NETTLE_DIR}/aes128-meta.c
|
|
||||||
${NETTLE_DIR}/aes192-set-encrypt-key.c
|
|
||||||
${NETTLE_DIR}/aes192-set-decrypt-key.c
|
|
||||||
${NETTLE_DIR}/aes192-meta.c
|
|
||||||
${NETTLE_DIR}/aes256-set-encrypt-key.c
|
|
||||||
${NETTLE_DIR}/aes256-set-decrypt-key.c
|
|
||||||
${NETTLE_DIR}/aes256-meta.c
|
|
||||||
${NETTLE_DIR}/nist-keywrap.c
|
|
||||||
${NETTLE_DIR}/arcfour.c
|
|
||||||
${NETTLE_DIR}/arctwo.c
|
|
||||||
${NETTLE_DIR}/arctwo-meta.c
|
|
||||||
${NETTLE_DIR}/blowfish.c
|
|
||||||
${NETTLE_DIR}/blowfish-bcrypt.c
|
|
||||||
${NETTLE_DIR}/base16-encode.c
|
|
||||||
${NETTLE_DIR}/base16-decode.c
|
|
||||||
${NETTLE_DIR}/base16-meta.c
|
|
||||||
${NETTLE_DIR}/base64-encode.c
|
|
||||||
${NETTLE_DIR}/base64-decode.c
|
|
||||||
${NETTLE_DIR}/base64-meta.c
|
|
||||||
${NETTLE_DIR}/base64url-encode.c
|
|
||||||
${NETTLE_DIR}/base64url-decode.c
|
|
||||||
${NETTLE_DIR}/base64url-meta.c
|
|
||||||
${NETTLE_DIR}/buffer.c
|
|
||||||
${NETTLE_DIR}/buffer-init.c
|
|
||||||
${NETTLE_DIR}/camellia-crypt-internal.c
|
|
||||||
${NETTLE_DIR}/camellia-table.c
|
|
||||||
${NETTLE_DIR}/camellia-absorb.c
|
|
||||||
${NETTLE_DIR}/camellia-invert-key.c
|
|
||||||
${NETTLE_DIR}/camellia128-set-encrypt-key.c
|
|
||||||
${NETTLE_DIR}/camellia128-crypt.c
|
|
||||||
${NETTLE_DIR}/camellia128-set-decrypt-key.c
|
|
||||||
${NETTLE_DIR}/camellia128-meta.c
|
|
||||||
${NETTLE_DIR}/camellia192-meta.c
|
|
||||||
${NETTLE_DIR}/camellia256-set-encrypt-key.c
|
|
||||||
${NETTLE_DIR}/camellia256-crypt.c
|
|
||||||
${NETTLE_DIR}/camellia256-set-decrypt-key.c
|
|
||||||
${NETTLE_DIR}/camellia256-meta.c
|
|
||||||
${NETTLE_DIR}/cast128.c
|
|
||||||
${NETTLE_DIR}/cast128-meta.c
|
|
||||||
${NETTLE_DIR}/cbc.c
|
|
||||||
${NETTLE_DIR}/cbc-aes128-encrypt.c
|
|
||||||
${NETTLE_DIR}/cbc-aes192-encrypt.c
|
|
||||||
${NETTLE_DIR}/cbc-aes256-encrypt.c
|
|
||||||
${NETTLE_DIR}/ccm.c
|
|
||||||
${NETTLE_DIR}/ccm-aes128.c
|
|
||||||
${NETTLE_DIR}/ccm-aes192.c
|
|
||||||
${NETTLE_DIR}/ccm-aes256.c
|
|
||||||
${NETTLE_DIR}/cfb.c
|
|
||||||
${NETTLE_DIR}/siv-cmac.c
|
|
||||||
${NETTLE_DIR}/siv-cmac-aes128.c
|
|
||||||
${NETTLE_DIR}/siv-cmac-aes256.c
|
|
||||||
${NETTLE_DIR}/cnd-memcpy.c
|
|
||||||
${NETTLE_DIR}/chacha-crypt.c
|
|
||||||
${NETTLE_DIR}/chacha-core-internal.c
|
|
||||||
${NETTLE_DIR}/chacha-poly1305.c
|
|
||||||
${NETTLE_DIR}/chacha-poly1305-meta.c
|
|
||||||
${NETTLE_DIR}/chacha-set-key.c
|
|
||||||
${NETTLE_DIR}/chacha-set-nonce.c
|
|
||||||
${NETTLE_DIR}/ctr.c
|
|
||||||
${NETTLE_DIR}/ctr16.c
|
|
||||||
${NETTLE_DIR}/des.c
|
|
||||||
${NETTLE_DIR}/des3.c
|
|
||||||
${NETTLE_DIR}/eax.c
|
|
||||||
${NETTLE_DIR}/eax-aes128.c
|
|
||||||
${NETTLE_DIR}/eax-aes128-meta.c
|
|
||||||
${NETTLE_DIR}/ghash-set-key.c
|
|
||||||
${NETTLE_DIR}/ghash-update.c
|
|
||||||
${NETTLE_DIR}/gcm.c
|
|
||||||
${NETTLE_DIR}/gcm-aes.c
|
|
||||||
${NETTLE_DIR}/gcm-aes128.c
|
|
||||||
${NETTLE_DIR}/gcm-aes128-meta.c
|
|
||||||
${NETTLE_DIR}/gcm-aes192.c
|
|
||||||
${NETTLE_DIR}/gcm-aes192-meta.c
|
|
||||||
${NETTLE_DIR}/gcm-aes256.c
|
|
||||||
${NETTLE_DIR}/gcm-aes256-meta.c
|
|
||||||
${NETTLE_DIR}/gcm-camellia128.c
|
|
||||||
${NETTLE_DIR}/gcm-camellia128-meta.c
|
|
||||||
${NETTLE_DIR}/gcm-camellia256.c
|
|
||||||
${NETTLE_DIR}/gcm-camellia256-meta.c
|
|
||||||
${NETTLE_DIR}/cmac.c
|
|
||||||
${NETTLE_DIR}/cmac64.c
|
|
||||||
${NETTLE_DIR}/cmac-aes128.c
|
|
||||||
${NETTLE_DIR}/cmac-aes256.c
|
|
||||||
${NETTLE_DIR}/cmac-des3.c
|
|
||||||
${NETTLE_DIR}/cmac-aes128-meta.c
|
|
||||||
${NETTLE_DIR}/cmac-aes256-meta.c
|
|
||||||
${NETTLE_DIR}/cmac-des3-meta.c
|
|
||||||
${NETTLE_DIR}/gost28147.c
|
|
||||||
${NETTLE_DIR}/gosthash94.c
|
|
||||||
${NETTLE_DIR}/gosthash94-meta.c
|
|
||||||
${NETTLE_DIR}/hmac.c
|
|
||||||
${NETTLE_DIR}/hmac-gosthash94.c
|
|
||||||
${NETTLE_DIR}/hmac-md5.c
|
|
||||||
${NETTLE_DIR}/hmac-ripemd160.c
|
|
||||||
${NETTLE_DIR}/hmac-sha1.c
|
|
||||||
${NETTLE_DIR}/hmac-sha224.c
|
|
||||||
${NETTLE_DIR}/hmac-sha256.c
|
|
||||||
${NETTLE_DIR}/hmac-sha384.c
|
|
||||||
${NETTLE_DIR}/hmac-sha512.c
|
|
||||||
${NETTLE_DIR}/hmac-streebog.c
|
|
||||||
${NETTLE_DIR}/hmac-sm3.c
|
|
||||||
${NETTLE_DIR}/hmac-md5-meta.c
|
|
||||||
${NETTLE_DIR}/hmac-ripemd160-meta.c
|
|
||||||
${NETTLE_DIR}/hmac-sha1-meta.c
|
|
||||||
${NETTLE_DIR}/hmac-sha224-meta.c
|
|
||||||
${NETTLE_DIR}/hmac-sha256-meta.c
|
|
||||||
${NETTLE_DIR}/hmac-sha384-meta.c
|
|
||||||
${NETTLE_DIR}/hmac-sha512-meta.c
|
|
||||||
${NETTLE_DIR}/hmac-streebog-meta.c
|
|
||||||
${NETTLE_DIR}/hmac-sm3-meta.c
|
|
||||||
${NETTLE_DIR}/knuth-lfib.c
|
|
||||||
${NETTLE_DIR}/hkdf.c
|
|
||||||
${NETTLE_DIR}/md2.c
|
|
||||||
${NETTLE_DIR}/md2-meta.c
|
|
||||||
${NETTLE_DIR}/md4.c
|
|
||||||
${NETTLE_DIR}/md4-meta.c
|
|
||||||
${NETTLE_DIR}/md5.c
|
|
||||||
${NETTLE_DIR}/md5-compress.c
|
|
||||||
${NETTLE_DIR}/md5-compat.c
|
|
||||||
${NETTLE_DIR}/md5-meta.c
|
|
||||||
${NETTLE_DIR}/memeql-sec.c
|
|
||||||
${NETTLE_DIR}/memxor.c
|
|
||||||
${NETTLE_DIR}/memxor3.c
|
|
||||||
${NETTLE_DIR}/nettle-lookup-hash.c
|
|
||||||
${NETTLE_DIR}/nettle-meta-aeads.c
|
|
||||||
${NETTLE_DIR}/nettle-meta-armors.c
|
|
||||||
${NETTLE_DIR}/nettle-meta-ciphers.c
|
|
||||||
${NETTLE_DIR}/nettle-meta-hashes.c
|
|
||||||
${NETTLE_DIR}/nettle-meta-macs.c
|
|
||||||
${NETTLE_DIR}/pbkdf2.c
|
|
||||||
${NETTLE_DIR}/pbkdf2-hmac-gosthash94.c
|
|
||||||
${NETTLE_DIR}/pbkdf2-hmac-sha1.c
|
|
||||||
${NETTLE_DIR}/pbkdf2-hmac-sha256.c
|
|
||||||
${NETTLE_DIR}/pbkdf2-hmac-sha384.c
|
|
||||||
${NETTLE_DIR}/pbkdf2-hmac-sha512.c
|
|
||||||
${NETTLE_DIR}/poly1305-aes.c
|
|
||||||
${NETTLE_DIR}/poly1305-internal.c
|
|
||||||
${NETTLE_DIR}/realloc.c
|
|
||||||
${NETTLE_DIR}/ripemd160.c
|
|
||||||
${NETTLE_DIR}/ripemd160-compress.c
|
|
||||||
${NETTLE_DIR}/ripemd160-meta.c
|
|
||||||
${NETTLE_DIR}/salsa20-core-internal.c
|
|
||||||
${NETTLE_DIR}/salsa20-crypt-internal.c
|
|
||||||
${NETTLE_DIR}/salsa20-crypt.c
|
|
||||||
${NETTLE_DIR}/salsa20r12-crypt.c
|
|
||||||
${NETTLE_DIR}/salsa20-set-key.c
|
|
||||||
${NETTLE_DIR}/salsa20-set-nonce.c
|
|
||||||
${NETTLE_DIR}/salsa20-128-set-key.c
|
|
||||||
${NETTLE_DIR}/salsa20-256-set-key.c
|
|
||||||
${NETTLE_DIR}/sha1.c
|
|
||||||
${NETTLE_DIR}/sha1-compress.c
|
|
||||||
${NETTLE_DIR}/sha1-meta.c
|
|
||||||
${NETTLE_DIR}/sha256.c
|
|
||||||
${NETTLE_DIR}/sha256-compress-n.c
|
|
||||||
${NETTLE_DIR}/sha224-meta.c
|
|
||||||
${NETTLE_DIR}/sha256-meta.c
|
|
||||||
${NETTLE_DIR}/sha512.c
|
|
||||||
${NETTLE_DIR}/sha512-compress.c
|
|
||||||
${NETTLE_DIR}/sha384-meta.c
|
|
||||||
${NETTLE_DIR}/sha512-meta.c
|
|
||||||
${NETTLE_DIR}/sha512-224-meta.c
|
|
||||||
${NETTLE_DIR}/sha512-256-meta.c
|
|
||||||
${NETTLE_DIR}/sha3.c
|
|
||||||
${NETTLE_DIR}/sha3-permute.c
|
|
||||||
${NETTLE_DIR}/sha3-224.c
|
|
||||||
${NETTLE_DIR}/sha3-224-meta.c
|
|
||||||
${NETTLE_DIR}/sha3-256.c
|
|
||||||
${NETTLE_DIR}/sha3-256-meta.c
|
|
||||||
${NETTLE_DIR}/sha3-384.c
|
|
||||||
${NETTLE_DIR}/sha3-384-meta.c
|
|
||||||
${NETTLE_DIR}/sha3-512.c
|
|
||||||
${NETTLE_DIR}/sha3-512-meta.c
|
|
||||||
${NETTLE_DIR}/shake256.c
|
|
||||||
${NETTLE_DIR}/sm3.c
|
|
||||||
${NETTLE_DIR}/sm3-meta.c
|
|
||||||
${NETTLE_DIR}/serpent-set-key.c
|
|
||||||
${NETTLE_DIR}/serpent-encrypt.c
|
|
||||||
${NETTLE_DIR}/serpent-decrypt.c
|
|
||||||
${NETTLE_DIR}/serpent-meta.c
|
|
||||||
${NETTLE_DIR}/streebog.c
|
|
||||||
${NETTLE_DIR}/streebog-meta.c
|
|
||||||
${NETTLE_DIR}/twofish.c
|
|
||||||
${NETTLE_DIR}/twofish-meta.c
|
|
||||||
${NETTLE_DIR}/umac-nh.c
|
|
||||||
${NETTLE_DIR}/umac-nh-n.c
|
|
||||||
${NETTLE_DIR}/umac-l2.c
|
|
||||||
${NETTLE_DIR}/umac-l3.c
|
|
||||||
${NETTLE_DIR}/umac-poly64.c
|
|
||||||
${NETTLE_DIR}/umac-poly128.c
|
|
||||||
${NETTLE_DIR}/umac-set-key.c
|
|
||||||
${NETTLE_DIR}/umac32.c
|
|
||||||
${NETTLE_DIR}/umac64.c
|
|
||||||
${NETTLE_DIR}/umac96.c
|
|
||||||
${NETTLE_DIR}/umac128.c
|
|
||||||
${NETTLE_DIR}/version.c
|
|
||||||
${NETTLE_DIR}/write-be32.c
|
|
||||||
${NETTLE_DIR}/write-le32.c
|
|
||||||
${NETTLE_DIR}/write-le64.c
|
|
||||||
${NETTLE_DIR}/yarrow256.c
|
|
||||||
${NETTLE_DIR}/yarrow_key_event.c
|
|
||||||
${NETTLE_DIR}/xts.c
|
|
||||||
${NETTLE_DIR}/xts-aes128.c
|
|
||||||
${NETTLE_DIR}/xts-aes256.c
|
|
||||||
)
|
|
||||||
|
|
||||||
set(LIBNETTLE_INCLUDES
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
|
||||||
${NETTLE_DIR}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_library(nettle STATIC ${LIBNETTLE_SOURCES})
|
|
||||||
target_include_directories(nettle PUBLIC ${LIBNETTLE_INCLUDES})
|
|
||||||
target_compile_definitions(nettle PRIVATE ${LIBNETTLE_DEFINITIONS})
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
/* config.h. Generated from config.h.in by configure. */
|
|
||||||
/* config.h.in. Generated from configure.ac by autoheader. */
|
|
||||||
|
|
||||||
/* Define if building universal (internal helper macro) */
|
|
||||||
/* #undef AC_APPLE_UNIVERSAL_BUILD */
|
|
||||||
|
|
||||||
/* Define to one of `_getb67', `GETB67', `getb67' for Cray-2 and Cray-YMP
|
|
||||||
systems. This function is required for `alloca.c' support on those systems.
|
|
||||||
*/
|
|
||||||
/* #undef CRAY_STACKSEG_END */
|
|
||||||
|
|
||||||
/* Define to 1 if using `alloca.c'. */
|
|
||||||
/* #undef C_ALLOCA */
|
|
||||||
|
|
||||||
/* Define to 1 if you have `alloca', as a function or macro. */
|
|
||||||
#define HAVE_ALLOCA 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have <alloca.h> and it should be used (not on Ultrix).
|
|
||||||
*/
|
|
||||||
#define HAVE_ALLOCA_H 1
|
|
||||||
|
|
||||||
/* Define if __builtin_bswap64 is available */
|
|
||||||
#define HAVE_BUILTIN_BSWAP64 1
|
|
||||||
|
|
||||||
/* Define if clock_gettime is available */
|
|
||||||
#define HAVE_CLOCK_GETTIME 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <dlfcn.h> header file. */
|
|
||||||
#define HAVE_DLFCN_H 1
|
|
||||||
|
|
||||||
/* Define if fcntl file locking is available */
|
|
||||||
#define HAVE_FCNTL_LOCKING 1
|
|
||||||
|
|
||||||
/* Define if the compiler understands __attribute__ */
|
|
||||||
#define HAVE_GCC_ATTRIBUTE 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the `getline' function. */
|
|
||||||
#define HAVE_GETLINE 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <inttypes.h> header file. */
|
|
||||||
#define HAVE_INTTYPES_H 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have dlopen (with -ldl). */
|
|
||||||
#define HAVE_LIBDL 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the `gmp' library (-lgmp). */
|
|
||||||
/* #undef HAVE_LIBGMP */
|
|
||||||
|
|
||||||
/* Define if compiler and linker supports __attribute__ ifunc */
|
|
||||||
#define HAVE_LINK_IFUNC 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <malloc.h> header file. */
|
|
||||||
#define HAVE_MALLOC_H 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <memory.h> header file. */
|
|
||||||
#define HAVE_MEMORY_H 1
|
|
||||||
|
|
||||||
/* Define to 1 each of the following for which a native (ie. CPU specific)
|
|
||||||
implementation of the corresponding routine exists. */
|
|
||||||
/* #undef HAVE_NATIVE_memxor3 */
|
|
||||||
/* #undef HAVE_NATIVE_aes_decrypt */
|
|
||||||
/* #undef HAVE_NATIVE_aes_encrypt */
|
|
||||||
/* #undef HAVE_NATIVE_aes128_decrypt */
|
|
||||||
/* #undef HAVE_NATIVE_aes128_encrypt */
|
|
||||||
/* #undef HAVE_NATIVE_aes128_invert_key */
|
|
||||||
/* #undef HAVE_NATIVE_aes128_set_decrypt_key */
|
|
||||||
/* #undef HAVE_NATIVE_aes128_set_encrypt_key */
|
|
||||||
/* #undef HAVE_NATIVE_aes192_decrypt */
|
|
||||||
/* #undef HAVE_NATIVE_aes192_encrypt */
|
|
||||||
/* #undef HAVE_NATIVE_aes192_invert_key */
|
|
||||||
/* #undef HAVE_NATIVE_aes192_set_decrypt_key */
|
|
||||||
/* #undef HAVE_NATIVE_aes192_set_encrypt_key */
|
|
||||||
/* #undef HAVE_NATIVE_aes256_decrypt */
|
|
||||||
/* #undef HAVE_NATIVE_aes256_encrypt */
|
|
||||||
/* #undef HAVE_NATIVE_aes256_invert_key */
|
|
||||||
/* #undef HAVE_NATIVE_aes256_set_decrypt_key */
|
|
||||||
/* #undef HAVE_NATIVE_aes256_set_encrypt_key */
|
|
||||||
/* #undef HAVE_NATIVE_cbc_aes128_encrypt */
|
|
||||||
/* #undef HAVE_NATIVE_cbc_aes192_encrypt */
|
|
||||||
/* #undef HAVE_NATIVE_cbc_aes256_encrypt */
|
|
||||||
/* #undef HAVE_NATIVE_chacha_core */
|
|
||||||
/* #undef HAVE_NATIVE_chacha_2core */
|
|
||||||
#define HAVE_NATIVE_chacha_3core 1
|
|
||||||
/* #undef HAVE_NATIVE_chacha_4core */
|
|
||||||
/* #undef HAVE_NATIVE_fat_chacha_2core */
|
|
||||||
/* #undef HAVE_NATIVE_fat_chacha_3core */
|
|
||||||
/* #undef HAVE_NATIVE_fat_chacha_4core */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_curve25519_modp */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_curve448_modp */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp192r1_modp */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp192r1_redc */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp224r1_modp */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp224r1_redc */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp256r1_modp */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp256r1_redc */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp384r1_modp */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp384r1_redc */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp521r1_modp */
|
|
||||||
/* #undef HAVE_NATIVE_ecc_secp521r1_redc */
|
|
||||||
/* #undef HAVE_NATIVE_poly1305_set_key */
|
|
||||||
/* #undef HAVE_NATIVE_poly1305_block */
|
|
||||||
/* #undef HAVE_NATIVE_poly1305_digest */
|
|
||||||
/* #undef HAVE_NATIVE_poly1305_blocks */
|
|
||||||
/* #undef HAVE_NATIVE_fat_poly1305_blocks */
|
|
||||||
/* #undef HAVE_NATIVE_ghash_set_key */
|
|
||||||
/* #undef HAVE_NATIVE_ghash_update */
|
|
||||||
/* #undef HAVE_NATIVE_salsa20_core */
|
|
||||||
#define HAVE_NATIVE_salsa20_2core 1
|
|
||||||
/* #undef HAVE_NATIVE_fat_salsa20_2core */
|
|
||||||
/* #undef HAVE_NATIVE_sha1_compress */
|
|
||||||
/* #undef HAVE_NATIVE_sha256_compress_n */
|
|
||||||
/* #undef HAVE_NATIVE_sha512_compress */
|
|
||||||
/* #undef HAVE_NATIVE_sha3_permute */
|
|
||||||
/* #undef HAVE_NATIVE_umac_nh */
|
|
||||||
/* #undef HAVE_NATIVE_umac_nh_n */
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <openssl/ecdsa.h> header file. */
|
|
||||||
/* #undef HAVE_OPENSSL_ECDSA_H */
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <openssl/evp.h> header file. */
|
|
||||||
/* #undef HAVE_OPENSSL_EVP_H */
|
|
||||||
|
|
||||||
/* Define to 1 if you have the `secure_getenv' function. */
|
|
||||||
#define HAVE_SECURE_GETENV 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <stdint.h> header file. */
|
|
||||||
#define HAVE_STDINT_H 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <stdlib.h> header file. */
|
|
||||||
#define HAVE_STDLIB_H 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the `strerror' function. */
|
|
||||||
#define HAVE_STRERROR 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <strings.h> header file. */
|
|
||||||
#define HAVE_STRINGS_H 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <string.h> header file. */
|
|
||||||
#define HAVE_STRING_H 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <sys/stat.h> header file. */
|
|
||||||
#define HAVE_SYS_STAT_H 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <sys/types.h> header file. */
|
|
||||||
#define HAVE_SYS_TYPES_H 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <unistd.h> header file. */
|
|
||||||
#define HAVE_UNISTD_H 1
|
|
||||||
|
|
||||||
/* Define to 1 if you have the <valgrind/memcheck.h> header file. */
|
|
||||||
/* #undef HAVE_VALGRIND_MEMCHECK_H */
|
|
||||||
|
|
||||||
/* Define to the address where bug reports for this package should be sent. */
|
|
||||||
#define PACKAGE_BUGREPORT "nettle-bugs@lists.lysator.liu.se"
|
|
||||||
|
|
||||||
/* Define to the full name of this package. */
|
|
||||||
#define PACKAGE_NAME "nettle"
|
|
||||||
|
|
||||||
/* Define to the full name and version of this package. */
|
|
||||||
#define PACKAGE_STRING "nettle 3.9.1"
|
|
||||||
|
|
||||||
/* Define to the one symbol short name of this package. */
|
|
||||||
#define PACKAGE_TARNAME "nettle"
|
|
||||||
|
|
||||||
/* Define to the home page for this package. */
|
|
||||||
#define PACKAGE_URL ""
|
|
||||||
|
|
||||||
/* Define to the version of this package. */
|
|
||||||
#define PACKAGE_VERSION "3.9.1"
|
|
||||||
|
|
||||||
/* The size of `long', as computed by sizeof. */
|
|
||||||
#define SIZEOF_LONG __SIZEOF_LONG__
|
|
||||||
|
|
||||||
/* The size of `size_t', as computed by sizeof. */
|
|
||||||
#define SIZEOF_SIZE_T __SIZEOF_SIZE_T__
|
|
||||||
|
|
||||||
/* If using the C implementation of alloca, define if you know the
|
|
||||||
direction of stack growth for your system; otherwise it will be
|
|
||||||
automatically deduced at runtime.
|
|
||||||
STACK_DIRECTION > 0 => grows toward higher addresses
|
|
||||||
STACK_DIRECTION < 0 => grows toward lower addresses
|
|
||||||
STACK_DIRECTION = 0 => direction of growth unknown */
|
|
||||||
/* #undef STACK_DIRECTION */
|
|
||||||
|
|
||||||
/* Define to 1 if you have the ANSI C header files. */
|
|
||||||
#define STDC_HEADERS 1
|
|
||||||
|
|
||||||
/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. */
|
|
||||||
#define TIME_WITH_SYS_TIME 1
|
|
||||||
|
|
||||||
/* Defined if public key features are enabled */
|
|
||||||
/* #undef WITH_HOGWEED */
|
|
||||||
|
|
||||||
/* Define if you have openssl's libcrypto (used for benchmarking) */
|
|
||||||
/* #undef WITH_OPENSSL */
|
|
||||||
|
|
||||||
/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
|
|
||||||
significant byte first (like Motorola and SPARC, unlike Intel). */
|
|
||||||
#if defined AC_APPLE_UNIVERSAL_BUILD
|
|
||||||
# if defined __BIG_ENDIAN__
|
|
||||||
# define WORDS_BIGENDIAN 1
|
|
||||||
# endif
|
|
||||||
#else
|
|
||||||
# ifndef WORDS_BIGENDIAN
|
|
||||||
/* # undef WORDS_BIGENDIAN */
|
|
||||||
# endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Define to empty if `const' does not conform to ANSI C. */
|
|
||||||
/* #undef const */
|
|
||||||
|
|
||||||
/* Define to `int' if <sys/types.h> doesn't define. */
|
|
||||||
/* #undef gid_t */
|
|
||||||
|
|
||||||
/* Define to `__inline__' or `__inline' if that's what the C compiler
|
|
||||||
calls it, or to nothing if 'inline' is not supported under any name. */
|
|
||||||
#ifndef __cplusplus
|
|
||||||
/* #undef inline */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Define to `unsigned int' if <sys/types.h> does not define. */
|
|
||||||
/* #undef size_t */
|
|
||||||
|
|
||||||
/* Define to `int' if <sys/types.h> doesn't define. */
|
|
||||||
/* #undef uid_t */
|
|
||||||
|
|
||||||
/* AIX requires this to be the first thing in the file. */
|
|
||||||
#ifndef __GNUC__
|
|
||||||
# if HAVE_ALLOCA_H
|
|
||||||
# include <alloca.h>
|
|
||||||
# else
|
|
||||||
# ifdef _AIX
|
|
||||||
#pragma alloca
|
|
||||||
# else
|
|
||||||
# ifndef alloca /* predefined by HP cc +Olibcalls */
|
|
||||||
char *alloca ();
|
|
||||||
# endif
|
|
||||||
# endif
|
|
||||||
/* Needed for alloca on windows */
|
|
||||||
# if HAVE_MALLOC_H
|
|
||||||
# include <malloc.h>
|
|
||||||
# endif
|
|
||||||
# endif
|
|
||||||
#else /* defined __GNUC__ */
|
|
||||||
# if HAVE_ALLOCA_H
|
|
||||||
# include <alloca.h>
|
|
||||||
# else
|
|
||||||
/* Needed for alloca on windows, also with gcc */
|
|
||||||
# if HAVE_MALLOC_H
|
|
||||||
# include <malloc.h>
|
|
||||||
# endif
|
|
||||||
# endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
#if HAVE_STRERROR
|
|
||||||
#define STRERROR strerror
|
|
||||||
#else
|
|
||||||
#define STRERROR(x) (sys_errlist[x])
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
#if __GNUC__ && HAVE_GCC_ATTRIBUTE
|
|
||||||
# define NORETURN __attribute__ ((__noreturn__))
|
|
||||||
# define PRINTF_STYLE(f, a) __attribute__ ((__format__ (__printf__, f, a)))
|
|
||||||
# define UNUSED __attribute__ ((__unused__))
|
|
||||||
#else
|
|
||||||
# define NORETURN
|
|
||||||
# define PRINTF_STYLE(f, a)
|
|
||||||
# define UNUSED
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
#if defined(__x86_64__) || defined(__arch64__)
|
|
||||||
# define HAVE_NATIVE_64_BIT 1
|
|
||||||
#else
|
|
||||||
/* Needs include of <limits.h> before use. */
|
|
||||||
# define HAVE_NATIVE_64_BIT (SIZEOF_LONG * CHAR_BIT >= 64)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/* version.h
|
|
||||||
|
|
||||||
Information about library version.
|
|
||||||
|
|
||||||
Copyright (C) 2015 Red Hat, Inc.
|
|
||||||
Copyright (C) 2015 Niels Möller
|
|
||||||
|
|
||||||
This file is part of GNU Nettle.
|
|
||||||
|
|
||||||
GNU Nettle is free software: you can redistribute it and/or
|
|
||||||
modify it under the terms of either:
|
|
||||||
|
|
||||||
* the GNU Lesser General Public License as published by the Free
|
|
||||||
Software Foundation; either version 3 of the License, or (at your
|
|
||||||
option) any later version.
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
* the GNU General Public License as published by the Free
|
|
||||||
Software Foundation; either version 2 of the License, or (at your
|
|
||||||
option) any later version.
|
|
||||||
|
|
||||||
or both in parallel, as here.
|
|
||||||
|
|
||||||
GNU Nettle 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 copies of the GNU General Public License and
|
|
||||||
the GNU Lesser General Public License along with this program. If
|
|
||||||
not, see http://www.gnu.org/licenses/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef NETTLE_VERSION_H_INCLUDED
|
|
||||||
#define NETTLE_VERSION_H_INCLUDED
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Individual version numbers in decimal */
|
|
||||||
#define NETTLE_VERSION_MAJOR 3
|
|
||||||
#define NETTLE_VERSION_MINOR 9
|
|
||||||
|
|
||||||
#define NETTLE_USE_MINI_GMP 0
|
|
||||||
|
|
||||||
/* We need a preprocessor constant for GMP_NUMB_BITS, simply using
|
|
||||||
sizeof(mp_limb_t) * CHAR_BIT is not good enough. */
|
|
||||||
#if NETTLE_USE_MINI_GMP
|
|
||||||
# define GMP_NUMB_BITS n/a
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int
|
|
||||||
nettle_version_major (void);
|
|
||||||
|
|
||||||
int
|
|
||||||
nettle_version_minor (void);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* NETTLE_VERSION_H_INCLUDED */
|
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+197
-15
@@ -16,8 +16,14 @@
|
|||||||
|
|
||||||
package androidx.compose.foundation.gestures
|
package androidx.compose.foundation.gestures
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
|
import androidx.compose.animation.core.DecayAnimationSpec
|
||||||
|
import androidx.compose.animation.core.FloatDecayAnimationSpec
|
||||||
|
import androidx.compose.animation.core.generateDecayAnimationSpec
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragDelta
|
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragDelta
|
||||||
|
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
|
||||||
|
import androidx.compose.foundation.gestures.snapping.snapFlingBehavior
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
@@ -27,15 +33,23 @@ import androidx.compose.ui.layout.MeasureResult
|
|||||||
import androidx.compose.ui.layout.MeasureScope
|
import androidx.compose.ui.layout.MeasureScope
|
||||||
import androidx.compose.ui.node.LayoutModifierNode
|
import androidx.compose.ui.node.LayoutModifierNode
|
||||||
import androidx.compose.ui.node.ModifierNodeElement
|
import androidx.compose.ui.node.ModifierNodeElement
|
||||||
|
import androidx.compose.ui.node.ObserverModifierNode
|
||||||
|
import androidx.compose.ui.node.currentValueOf
|
||||||
|
import androidx.compose.ui.node.observeReads
|
||||||
import androidx.compose.ui.node.requireLayoutDirection
|
import androidx.compose.ui.node.requireLayoutDirection
|
||||||
import androidx.compose.ui.platform.InspectorInfo
|
import androidx.compose.ui.platform.InspectorInfo
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.debugInspectorInfo
|
import androidx.compose.ui.platform.debugInspectorInfo
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.sign
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
@@ -45,31 +59,31 @@ fun <T> Modifier.anchoredHorizontalDraggable(
|
|||||||
enableDragFromEndToStart: Boolean = true,
|
enableDragFromEndToStart: Boolean = true,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
startDragImmediately: Boolean = state.isAnimationRunning,
|
startDragImmediately: Boolean = state.isAnimationRunning,
|
||||||
|
flingBehavior: FlingBehavior? = null,
|
||||||
): Modifier = this then AnchoredHorizontalDraggableElement(
|
): Modifier = this then AnchoredHorizontalDraggableElement(
|
||||||
state = state,
|
state = state,
|
||||||
canDrag = { (x, y) ->
|
canDrag = { (x, y) ->
|
||||||
if (x > 0) {
|
val enableDrag = if (x > 0) enableDragFromStartToEnd else enableDragFromEndToStart
|
||||||
enableDragFromStartToEnd
|
enableDrag && x.absoluteValue > y.absoluteValue * 2f
|
||||||
} else {
|
|
||||||
enableDragFromEndToStart
|
|
||||||
} && x.absoluteValue > y.absoluteValue * 2f
|
|
||||||
},
|
},
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
startDragImmediately = startDragImmediately,
|
startDragImmediately = startDragImmediately,
|
||||||
|
flingBehavior = flingBehavior,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
private class AnchoredHorizontalDraggableElement<T>(
|
private class AnchoredHorizontalDraggableElement<T>(
|
||||||
private val state: AnchoredDraggableState<T>,
|
private val state: AnchoredDraggableState<T>,
|
||||||
private val canDrag: (Offset) -> Boolean,
|
private val canDrag: (Offset) -> Boolean,
|
||||||
private val enabled: Boolean,
|
private val enabled: Boolean,
|
||||||
private val startDragImmediately: Boolean,
|
private val startDragImmediately: Boolean,
|
||||||
|
private val flingBehavior: FlingBehavior?,
|
||||||
) : ModifierNodeElement<AnchoredHorizontalDraggableNode<T>>() {
|
) : ModifierNodeElement<AnchoredHorizontalDraggableNode<T>>() {
|
||||||
override fun create() = AnchoredHorizontalDraggableNode(
|
override fun create() = AnchoredHorizontalDraggableNode(
|
||||||
state,
|
state,
|
||||||
canDrag,
|
canDrag,
|
||||||
enabled,
|
enabled,
|
||||||
startDragImmediately,
|
startDragImmediately,
|
||||||
|
flingBehavior,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun update(node: AnchoredHorizontalDraggableNode<T>) {
|
override fun update(node: AnchoredHorizontalDraggableNode<T>) {
|
||||||
@@ -78,6 +92,7 @@ private class AnchoredHorizontalDraggableElement<T>(
|
|||||||
canDrag,
|
canDrag,
|
||||||
enabled,
|
enabled,
|
||||||
startDragImmediately,
|
startDragImmediately,
|
||||||
|
flingBehavior,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +101,7 @@ private class AnchoredHorizontalDraggableElement<T>(
|
|||||||
result = 31 * result + canDrag.hashCode()
|
result = 31 * result + canDrag.hashCode()
|
||||||
result = 31 * result + enabled.hashCode()
|
result = 31 * result + enabled.hashCode()
|
||||||
result = 31 * result + startDragImmediately.hashCode()
|
result = 31 * result + startDragImmediately.hashCode()
|
||||||
|
result = 31 * result + flingBehavior.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +114,7 @@ private class AnchoredHorizontalDraggableElement<T>(
|
|||||||
if (canDrag != other.canDrag) return false
|
if (canDrag != other.canDrag) return false
|
||||||
if (enabled != other.enabled) return false
|
if (enabled != other.enabled) return false
|
||||||
if (startDragImmediately != other.startDragImmediately) return false
|
if (startDragImmediately != other.startDragImmediately) return false
|
||||||
|
if (flingBehavior != other.flingBehavior) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -108,26 +125,56 @@ private class AnchoredHorizontalDraggableElement<T>(
|
|||||||
properties["canDrag"] = canDrag
|
properties["canDrag"] = canDrag
|
||||||
properties["enabled"] = enabled
|
properties["enabled"] = enabled
|
||||||
properties["startDragImmediately"] = startDragImmediately
|
properties["startDragImmediately"] = startDragImmediately
|
||||||
|
properties["flingBehavior"] = flingBehavior
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
private class AnchoredHorizontalDraggableNode<T>(
|
private class AnchoredHorizontalDraggableNode<T>(
|
||||||
private var state: AnchoredDraggableState<T>,
|
private var state: AnchoredDraggableState<T>,
|
||||||
private var canDrag: (Offset) -> Boolean,
|
private var canDrag: (Offset) -> Boolean,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
private var startDragImmediately: Boolean,
|
private var startDragImmediately: Boolean,
|
||||||
) : HorizontalDragGestureNode(
|
private var flingBehavior: FlingBehavior?,
|
||||||
enabled = enabled,
|
) : HorizontalDragGestureNode(enabled = enabled),
|
||||||
) {
|
ObserverModifierNode {
|
||||||
|
|
||||||
|
lateinit var resolvedFlingBehavior: FlingBehavior
|
||||||
|
lateinit var density: Density
|
||||||
|
|
||||||
private val isReverseDirection: Boolean
|
private val isReverseDirection: Boolean
|
||||||
get() = requireLayoutDirection() == LayoutDirection.Rtl
|
get() = requireLayoutDirection() == LayoutDirection.Rtl
|
||||||
|
|
||||||
override fun canDrag(change: PointerInputChange): Boolean {
|
override fun onAttach() {
|
||||||
return canDrag(change.positionChange().reverseIfNeeded())
|
updateFlingBehavior(flingBehavior)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onObservedReadsChanged() {
|
||||||
|
val newDensity = currentValueOf(LocalDensity)
|
||||||
|
if (density != newDensity) {
|
||||||
|
density = newDensity
|
||||||
|
updateFlingBehavior(flingBehavior)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFlingBehavior(newFlingBehavior: FlingBehavior?) {
|
||||||
|
// Fall back to default fling behavior if the new fling behavior is null
|
||||||
|
this.resolvedFlingBehavior =
|
||||||
|
if (newFlingBehavior == null) {
|
||||||
|
// Only register for LocalDensity snapshot updates if we are creating a decay
|
||||||
|
observeReads { density = currentValueOf(LocalDensity) }
|
||||||
|
anchoredDraggableFlingBehavior(
|
||||||
|
snapAnimationSpec = AnchoredDraggableDefaults.SnapAnimationSpec,
|
||||||
|
positionalThreshold = AnchoredDraggableDefaults.PositionalThreshold,
|
||||||
|
density = density,
|
||||||
|
state = state,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
newFlingBehavior
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canDrag(change: PointerInputChange): Boolean = canDrag(change.positionChange().reverseIfNeeded())
|
||||||
|
|
||||||
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
|
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
|
||||||
state.anchoredDrag {
|
state.anchoredDrag {
|
||||||
forEachDelta { dragDelta ->
|
forEachDelta { dragDelta ->
|
||||||
@@ -141,10 +188,29 @@ private class AnchoredHorizontalDraggableNode<T>(
|
|||||||
override fun onDragStopped(velocity: Velocity) {
|
override fun onDragStopped(velocity: Velocity) {
|
||||||
if (!isAttached) return
|
if (!isAttached) return
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
state.settle(velocity.reverseIfNeeded().x)
|
fling(velocity.reverseIfNeeded().x)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun fling(velocity: Float): Float {
|
||||||
|
var leftoverVelocity = velocity
|
||||||
|
state.anchoredDrag {
|
||||||
|
val scrollScope =
|
||||||
|
object : ScrollScope {
|
||||||
|
override fun scrollBy(pixels: Float): Float {
|
||||||
|
val newOffset = state.newOffsetForDelta(pixels)
|
||||||
|
val consumed = newOffset - state.offset
|
||||||
|
dragTo(newOffset)
|
||||||
|
return consumed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with(resolvedFlingBehavior) {
|
||||||
|
leftoverVelocity = scrollScope.performFling(velocity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return leftoverVelocity
|
||||||
|
}
|
||||||
|
|
||||||
override fun startDragImmediately(): Boolean = startDragImmediately
|
override fun startDragImmediately(): Boolean = startDragImmediately
|
||||||
|
|
||||||
fun update(
|
fun update(
|
||||||
@@ -152,11 +218,15 @@ private class AnchoredHorizontalDraggableNode<T>(
|
|||||||
canDrag: (Offset) -> Boolean,
|
canDrag: (Offset) -> Boolean,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
startDragImmediately: Boolean,
|
startDragImmediately: Boolean,
|
||||||
|
flingBehavior: FlingBehavior?,
|
||||||
) {
|
) {
|
||||||
|
this.flingBehavior = flingBehavior
|
||||||
|
|
||||||
var resetPointerInputHandling = false
|
var resetPointerInputHandling = false
|
||||||
|
|
||||||
if (this.state != state) {
|
if (this.state != state) {
|
||||||
this.state = state
|
this.state = state
|
||||||
|
updateFlingBehavior(flingBehavior)
|
||||||
resetPointerInputHandling = true
|
resetPointerInputHandling = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +245,118 @@ private class AnchoredHorizontalDraggableNode<T>(
|
|||||||
|
|
||||||
internal fun <T> AnchoredDraggableState<T>.newOffsetForDelta(delta: Float) =
|
internal fun <T> AnchoredDraggableState<T>.newOffsetForDelta(delta: Float) =
|
||||||
((if (offset.isNaN()) 0f else offset) + delta)
|
((if (offset.isNaN()) 0f else offset) + delta)
|
||||||
.coerceIn(anchors.minAnchor(), anchors.maxAnchor())
|
.coerceIn(anchors.minPosition(), anchors.maxPosition())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the target anchor based on the [currentOffset], [velocity] and [positionalThreshold] and
|
||||||
|
* [velocityThreshold].
|
||||||
|
*
|
||||||
|
* @return The suggested target anchor
|
||||||
|
*/
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
private fun <T> DraggableAnchors<T>.computeTarget(
|
||||||
|
currentOffset: Float,
|
||||||
|
currentValue: T,
|
||||||
|
velocity: Float,
|
||||||
|
positionalThreshold: (totalDistance: Float) -> Float,
|
||||||
|
velocityThreshold: () -> Float,
|
||||||
|
): T {
|
||||||
|
val currentAnchors = this
|
||||||
|
val currentAnchorPosition = currentAnchors.positionOf(currentValue)
|
||||||
|
val velocityThresholdPx = velocityThreshold()
|
||||||
|
return if (currentAnchorPosition == currentOffset || currentAnchorPosition.isNaN()) {
|
||||||
|
currentValue
|
||||||
|
} else {
|
||||||
|
if (abs(velocity) >= abs(velocityThresholdPx)) {
|
||||||
|
currentAnchors.closestAnchor(currentOffset, sign(velocity) > 0)!!
|
||||||
|
} else {
|
||||||
|
val neighborAnchor =
|
||||||
|
currentAnchors.closestAnchor(
|
||||||
|
currentOffset,
|
||||||
|
currentOffset - currentAnchorPosition > 0,
|
||||||
|
)!!
|
||||||
|
val neighborAnchorPosition = currentAnchors.positionOf(neighborAnchor)
|
||||||
|
val distance = abs(currentAnchorPosition - neighborAnchorPosition)
|
||||||
|
val relativeThreshold = abs(positionalThreshold(distance))
|
||||||
|
val relativePosition = abs(currentAnchorPosition - currentOffset)
|
||||||
|
if (relativePosition <= relativeThreshold) currentValue else neighborAnchor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a [FlingBehavior] for use with [Modifier.anchoredDraggable].
|
||||||
|
*
|
||||||
|
* @param state The [AnchoredDraggableState] that will be used for the fling animation
|
||||||
|
* @param positionalThreshold A positional threshold that needs to be crossed in order to reach the
|
||||||
|
* next anchor when flinging, in pixels. This can be a derived from the distance that the lambda
|
||||||
|
* is invoked with.
|
||||||
|
* @param snapAnimationSpec The animation spec that will be used to snap to a new state.
|
||||||
|
*/
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
internal fun <T> anchoredDraggableFlingBehavior(
|
||||||
|
state: AnchoredDraggableState<T>,
|
||||||
|
density: Density,
|
||||||
|
positionalThreshold: (totalDistance: Float) -> Float,
|
||||||
|
snapAnimationSpec: AnimationSpec<Float>,
|
||||||
|
): TargetedFlingBehavior =
|
||||||
|
snapFlingBehavior(
|
||||||
|
decayAnimationSpec = NoOpDecayAnimationSpec,
|
||||||
|
snapAnimationSpec = snapAnimationSpec,
|
||||||
|
snapLayoutInfoProvider =
|
||||||
|
AnchoredDraggableLayoutInfoProvider(
|
||||||
|
state = state,
|
||||||
|
positionalThreshold = positionalThreshold,
|
||||||
|
velocityThreshold = { with(density) { 125.dp.toPx() } },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
private fun <T> AnchoredDraggableLayoutInfoProvider(
|
||||||
|
state: AnchoredDraggableState<T>,
|
||||||
|
positionalThreshold: (totalDistance: Float) -> Float,
|
||||||
|
velocityThreshold: () -> Float,
|
||||||
|
): SnapLayoutInfoProvider =
|
||||||
|
object : SnapLayoutInfoProvider {
|
||||||
|
|
||||||
|
// We never decay in AnchoredDraggable's fling
|
||||||
|
override fun calculateApproachOffset(velocity: Float, decayOffset: Float) = 0f
|
||||||
|
|
||||||
|
override fun calculateSnapOffset(velocity: Float): Float {
|
||||||
|
val currentOffset = state.requireOffset()
|
||||||
|
val target =
|
||||||
|
state.anchors.computeTarget(
|
||||||
|
currentOffset = currentOffset,
|
||||||
|
currentValue = state.currentValue,
|
||||||
|
velocity = velocity,
|
||||||
|
positionalThreshold = positionalThreshold,
|
||||||
|
velocityThreshold = velocityThreshold,
|
||||||
|
)
|
||||||
|
return state.anchors.positionOf(target) - currentOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val NoOpDecayAnimationSpec: DecayAnimationSpec<Float> =
|
||||||
|
object : FloatDecayAnimationSpec {
|
||||||
|
override val absVelocityThreshold = 0f
|
||||||
|
|
||||||
|
override fun getValueFromNanos(
|
||||||
|
playTimeNanos: Long,
|
||||||
|
initialValue: Float,
|
||||||
|
initialVelocity: Float,
|
||||||
|
) = 0f
|
||||||
|
|
||||||
|
override fun getDurationNanos(initialValue: Float, initialVelocity: Float) = 0L
|
||||||
|
|
||||||
|
override fun getVelocityFromNanos(
|
||||||
|
playTimeNanos: Long,
|
||||||
|
initialValue: Float,
|
||||||
|
initialVelocity: Float,
|
||||||
|
) = 0f
|
||||||
|
|
||||||
|
override fun getTargetValue(initialValue: Float, initialVelocity: Float) = 0f
|
||||||
|
}.generateDecayAnimationSpec()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This Modifier allows configuring an [AnchoredDraggableState]'s anchors based on this layout
|
* This Modifier allows configuring an [AnchoredDraggableState]'s anchors based on this layout
|
||||||
@@ -240,7 +421,8 @@ private class DraggableAnchorsNode<T>(
|
|||||||
var state: AnchoredDraggableState<T>,
|
var state: AnchoredDraggableState<T>,
|
||||||
var anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
var anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||||
var orientation: Orientation,
|
var orientation: Orientation,
|
||||||
) : Modifier.Node(), LayoutModifierNode {
|
) : Modifier.Node(),
|
||||||
|
LayoutModifierNode {
|
||||||
private var didLookahead: Boolean = false
|
private var didLookahead: Boolean = false
|
||||||
|
|
||||||
override fun onDetach() {
|
override fun onDetach() {
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ import kotlinx.coroutines.launch
|
|||||||
*/
|
*/
|
||||||
internal abstract class HorizontalDragGestureNode(
|
internal abstract class HorizontalDragGestureNode(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
|
) : DelegatingNode(),
|
||||||
|
PointerInputModifierNode,
|
||||||
|
CompositionLocalConsumerModifierNode {
|
||||||
|
|
||||||
protected var enabled = enabled
|
protected var enabled = enabled
|
||||||
private set
|
private set
|
||||||
@@ -132,65 +134,63 @@ internal abstract class HorizontalDragGestureNode(
|
|||||||
pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds)
|
pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializePointerInputNode(): SuspendingPointerInputModifierNode {
|
private fun initializePointerInputNode(): SuspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
|
||||||
return SuspendingPointerInputModifierNode {
|
// re-create tracker when pointer input block restarts. This lazily creates the tracker
|
||||||
// re-create tracker when pointer input block restarts. This lazily creates the tracker
|
// only when it is need.
|
||||||
// only when it is need.
|
val velocityTracker = VelocityTracker()
|
||||||
val velocityTracker = VelocityTracker()
|
val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
|
||||||
val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
|
{ startEvent, initialDelta ->
|
||||||
{ startEvent, initialDelta ->
|
if (!isListeningForEvents) {
|
||||||
if (!isListeningForEvents) {
|
if (channel == null) {
|
||||||
if (channel == null) {
|
channel = Channel(capacity = Channel.UNLIMITED)
|
||||||
channel = Channel(capacity = Channel.UNLIMITED)
|
|
||||||
}
|
|
||||||
startListeningForEvents()
|
|
||||||
}
|
}
|
||||||
val xSign = sign(startEvent.position.x)
|
startListeningForEvents()
|
||||||
val ySign = sign(startEvent.position.y)
|
|
||||||
val adjustedStart = startEvent.position -
|
|
||||||
Offset(initialDelta.x * xSign, initialDelta.y * ySign)
|
|
||||||
|
|
||||||
channel?.trySend(DragStarted(adjustedStart))
|
|
||||||
}
|
}
|
||||||
|
val xSign = sign(startEvent.position.x)
|
||||||
|
val ySign = sign(startEvent.position.y)
|
||||||
|
val adjustedStart = startEvent.position -
|
||||||
|
Offset(initialDelta.x * xSign, initialDelta.y * ySign)
|
||||||
|
|
||||||
val onDragEnd: () -> Unit = {
|
channel?.trySend(DragStarted(adjustedStart))
|
||||||
val maximumVelocity = currentValueOf(LocalViewConfiguration)
|
}
|
||||||
.maximumFlingVelocity
|
|
||||||
val velocity = velocityTracker.calculateVelocity(
|
val onDragEnd: () -> Unit = {
|
||||||
Velocity(maximumVelocity, maximumVelocity),
|
val maximumVelocity = currentValueOf(LocalViewConfiguration)
|
||||||
|
.maximumFlingVelocity
|
||||||
|
val velocity = velocityTracker.calculateVelocity(
|
||||||
|
Velocity(maximumVelocity, maximumVelocity),
|
||||||
|
)
|
||||||
|
velocityTracker.resetTracking()
|
||||||
|
channel?.trySend(DragStopped(velocity))
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDragCancel: () -> Unit = {
|
||||||
|
channel?.trySend(DragCancelled)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shouldAwaitTouchSlop: () -> Boolean = {
|
||||||
|
!startDragImmediately()
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
|
||||||
|
{ change, delta ->
|
||||||
|
velocityTracker.addPointerInputChange(change)
|
||||||
|
channel?.trySend(DragDelta(delta))
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope {
|
||||||
|
try {
|
||||||
|
detectHorizontalDragGestures(
|
||||||
|
canDrag = ::canDrag,
|
||||||
|
onDragStart = onDragStart,
|
||||||
|
onDragEnd = onDragEnd,
|
||||||
|
onDragCancel = onDragCancel,
|
||||||
|
shouldAwaitTouchSlop = shouldAwaitTouchSlop,
|
||||||
|
onDrag = onDrag,
|
||||||
)
|
)
|
||||||
velocityTracker.resetTracking()
|
} catch (cancellation: CancellationException) {
|
||||||
channel?.trySend(DragStopped(velocity))
|
|
||||||
}
|
|
||||||
|
|
||||||
val onDragCancel: () -> Unit = {
|
|
||||||
channel?.trySend(DragCancelled)
|
channel?.trySend(DragCancelled)
|
||||||
}
|
if (!isActive) throw cancellation
|
||||||
|
|
||||||
val shouldAwaitTouchSlop: () -> Boolean = {
|
|
||||||
!startDragImmediately()
|
|
||||||
}
|
|
||||||
|
|
||||||
val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
|
|
||||||
{ change, delta ->
|
|
||||||
velocityTracker.addPointerInputChange(change)
|
|
||||||
channel?.trySend(DragDelta(delta))
|
|
||||||
}
|
|
||||||
|
|
||||||
coroutineScope {
|
|
||||||
try {
|
|
||||||
detectHorizontalDragGestures(
|
|
||||||
canDrag = ::canDrag,
|
|
||||||
onDragStart = onDragStart,
|
|
||||||
onDragEnd = onDragEnd,
|
|
||||||
onDragCancel = onDragCancel,
|
|
||||||
shouldAwaitTouchSlop = shouldAwaitTouchSlop,
|
|
||||||
onDrag = onDrag,
|
|
||||||
)
|
|
||||||
} catch (cancellation: CancellationException) {
|
|
||||||
channel?.trySend(DragCancelled)
|
|
||||||
if (!isActive) throw cancellation
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package androidx.compose.foundation.scrollbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
|
||||||
|
internal class LazyStaggeredGridScrollbarAdapter(
|
||||||
|
private val scrollState: LazyStaggeredGridState,
|
||||||
|
) : LazyLineContentAdapter() {
|
||||||
|
|
||||||
|
override val viewportSize: Double
|
||||||
|
get() = with(scrollState.layoutInfo) {
|
||||||
|
if (isVertical) viewportSize.height else viewportSize.width
|
||||||
|
}.toDouble()
|
||||||
|
|
||||||
|
private val isVertical by lazy {
|
||||||
|
scrollState.layoutInfo.orientation == Orientation.Vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyStaggeredGridItemInfo.mainAxisSize() = with(size) {
|
||||||
|
if (isVertical) height else width
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyStaggeredGridItemInfo.mainAxisOffset() = with(offset) {
|
||||||
|
if (isVertical) y else x
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun firstVisibleLine(): VisibleLine? {
|
||||||
|
val first = scrollState.layoutInfo.visibleItemsInfo.firstOrNull()
|
||||||
|
return first?.run { VisibleLine(index, mainAxisOffset()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun totalLineCount() = scrollState.layoutInfo.totalItemsCount
|
||||||
|
|
||||||
|
override fun contentPadding() = with(scrollState.layoutInfo) {
|
||||||
|
beforeContentPadding + afterContentPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun snapToLine(lineIndex: Int, scrollOffset: Int) {
|
||||||
|
scrollState.scrollToItem(lineIndex, scrollOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun scrollBy(value: Float) {
|
||||||
|
scrollState.scrollBy(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun averageVisibleLineSize() = with(scrollState.layoutInfo.visibleItemsInfo) {
|
||||||
|
val first = firstOrNull() ?: return@with 0.0
|
||||||
|
val last = last()
|
||||||
|
(last.mainAxisOffset() + last.mainAxisSize() - first.mainAxisOffset()).toDouble() / size
|
||||||
|
}
|
||||||
|
|
||||||
|
override val lineSpacing get() = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberScrollbarAdapter(
|
||||||
|
scrollState: LazyStaggeredGridState,
|
||||||
|
): ScrollbarAdapter = remember(scrollState) {
|
||||||
|
LazyStaggeredGridScrollbarAdapter(scrollState)
|
||||||
|
}
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.compose.foundation.scrollbar
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.TweenSpec
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.gestures.drag
|
||||||
|
import androidx.compose.foundation.hoverable
|
||||||
|
import androidx.compose.foundation.interaction.DragInteraction
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocal
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.input.pointer.positionChange
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.MeasurePolicy
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.constrainHeight
|
||||||
|
import androidx.compose.ui.unit.constrainWidth
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [CompositionLocal] used to pass [ScrollbarStyle] down the tree.
|
||||||
|
* This value is typically set in some "Theme" composable function
|
||||||
|
* (DesktopTheme, MaterialTheme)
|
||||||
|
*/
|
||||||
|
val LocalScrollbarStyle = staticCompositionLocalOf { defaultScrollbarStyle() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines visual style of scrollbars (thickness, shapes, colors, etc).
|
||||||
|
* Can be passed as a parameter of scrollbar through [LocalScrollbarStyle]
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
data class ScrollbarStyle(
|
||||||
|
val minimalHeight: Dp,
|
||||||
|
val thickness: Dp,
|
||||||
|
val shape: Shape,
|
||||||
|
val hoverDurationMillis: Int,
|
||||||
|
val unhoverColor: Color,
|
||||||
|
val hoverColor: Color,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple default [ScrollbarStyle] without applying MaterialTheme.
|
||||||
|
*/
|
||||||
|
fun defaultScrollbarStyle() = ScrollbarStyle(
|
||||||
|
minimalHeight = 16.dp,
|
||||||
|
thickness = 8.dp,
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
hoverDurationMillis = 300,
|
||||||
|
unhoverColor = Color.Black.copy(alpha = 0.12f),
|
||||||
|
hoverColor = Color.Black.copy(alpha = 0.50f),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vertical scrollbar that can be attached to some scrollable
|
||||||
|
* component (ScrollableColumn, LazyColumn) and share common state with it.
|
||||||
|
*
|
||||||
|
* Can be placed independently.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* val state = rememberScrollState(0)
|
||||||
|
*
|
||||||
|
* Box(Modifier.fillMaxSize()) {
|
||||||
|
* Box(modifier = Modifier.verticalScroll(state)) {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* VerticalScrollbar(
|
||||||
|
* adapter = rememberScrollbarAdapter(state)
|
||||||
|
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param adapter [ScrollbarAdapter] that will be used to
|
||||||
|
* communicate with scrollable component
|
||||||
|
* @param modifier the modifier to apply to this layout
|
||||||
|
* @param reverseLayout reverse the direction of scrolling and layout, when `true`
|
||||||
|
* and [LazyListState.firstVisibleItemIndex] == 0 then scrollbar
|
||||||
|
* will be at the bottom of the container.
|
||||||
|
* It is usually used in pair with `LazyColumn(reverseLayout = true)`
|
||||||
|
* @param style [ScrollbarStyle] to define visual style of scrollbar
|
||||||
|
* @param interactionSource [MutableInteractionSource] that will be used to dispatch
|
||||||
|
* [DragInteraction.Start] when this Scrollbar is being dragged.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun VerticalScrollbar(
|
||||||
|
adapter: ScrollbarAdapter,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
reverseLayout: Boolean = false,
|
||||||
|
style: ScrollbarStyle = LocalScrollbarStyle.current,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) = Scrollbar(
|
||||||
|
adapter,
|
||||||
|
modifier,
|
||||||
|
reverseLayout,
|
||||||
|
style,
|
||||||
|
interactionSource,
|
||||||
|
isVertical = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal scrollbar that can be attached to some scrollable
|
||||||
|
* component (Modifier.verticalScroll(), LazyRow) and share common state with it.
|
||||||
|
*
|
||||||
|
* Can be placed independently.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* val state = rememberScrollState(0)
|
||||||
|
*
|
||||||
|
* Box(Modifier.fillMaxSize()) {
|
||||||
|
* Box(modifier = Modifier.verticalScroll(state)) {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* HorizontalScrollbar(
|
||||||
|
* adapter = rememberScrollbarAdapter(state)
|
||||||
|
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxWidth(),
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param adapter [ScrollbarAdapter] that will be used to
|
||||||
|
* communicate with scrollable component
|
||||||
|
* @param modifier the modifier to apply to this layout
|
||||||
|
* @param reverseLayout reverse the direction of scrolling and layout, when `true`
|
||||||
|
* and [LazyListState.firstVisibleItemIndex] == 0 then scrollbar
|
||||||
|
* will be at the end of the container.
|
||||||
|
* It is usually used in pair with `LazyRow(reverseLayout = true)`
|
||||||
|
* @param style [ScrollbarStyle] to define visual style of scrollbar
|
||||||
|
* @param interactionSource [MutableInteractionSource] that will be used to dispatch
|
||||||
|
* [DragInteraction.Start] when this Scrollbar is being dragged.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun HorizontalScrollbar(
|
||||||
|
adapter: ScrollbarAdapter,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
reverseLayout: Boolean = false,
|
||||||
|
style: ScrollbarStyle = LocalScrollbarStyle.current,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) = Scrollbar(
|
||||||
|
adapter,
|
||||||
|
modifier,
|
||||||
|
if (LocalLayoutDirection.current == LayoutDirection.Rtl) !reverseLayout else reverseLayout,
|
||||||
|
style,
|
||||||
|
interactionSource,
|
||||||
|
isVertical = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Scrollbar(
|
||||||
|
adapter: ScrollbarAdapter,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
reverseLayout: Boolean,
|
||||||
|
style: ScrollbarStyle,
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
isVertical: Boolean,
|
||||||
|
) = with(LocalDensity.current) {
|
||||||
|
val dragInteraction = remember<MutableState<DragInteraction.Start?>> { mutableStateOf(null) }
|
||||||
|
DisposableEffect(key1 = interactionSource) {
|
||||||
|
onDispose {
|
||||||
|
dragInteraction.value?.let { interaction ->
|
||||||
|
interactionSource.tryEmit(DragInteraction.Cancel(interaction))
|
||||||
|
dragInteraction.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var containerSize by remember { mutableIntStateOf(0) }
|
||||||
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
|
|
||||||
|
val isHighlighted by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
isHovered || dragInteraction.value is DragInteraction.Start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val minimalHeight = style.minimalHeight.toPx()
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val sliderAdapter = remember(
|
||||||
|
adapter,
|
||||||
|
containerSize,
|
||||||
|
minimalHeight,
|
||||||
|
reverseLayout,
|
||||||
|
isVertical,
|
||||||
|
coroutineScope,
|
||||||
|
) {
|
||||||
|
SliderAdapter(
|
||||||
|
adapter,
|
||||||
|
containerSize,
|
||||||
|
minimalHeight,
|
||||||
|
reverseLayout = reverseLayout,
|
||||||
|
isVertical = isVertical,
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollThickness = style.thickness.roundToPx()
|
||||||
|
val measurePolicy = if (isVertical) {
|
||||||
|
remember(sliderAdapter, scrollThickness) {
|
||||||
|
verticalMeasurePolicy(sliderAdapter, { containerSize = it }, scrollThickness)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remember(sliderAdapter, scrollThickness) {
|
||||||
|
horizontalMeasurePolicy(sliderAdapter, { containerSize = it }, scrollThickness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val color by animateColorAsState(
|
||||||
|
if (isHighlighted) style.hoverColor else style.unhoverColor,
|
||||||
|
animationSpec = TweenSpec(durationMillis = style.hoverDurationMillis),
|
||||||
|
label = "Scrollbar color",
|
||||||
|
)
|
||||||
|
|
||||||
|
val isVisible = sliderAdapter.thumbSize < containerSize
|
||||||
|
|
||||||
|
Layout(
|
||||||
|
{
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.background(if (isVisible) color else Color.Transparent, style.shape)
|
||||||
|
.scrollbarDrag(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
draggedInteraction = dragInteraction,
|
||||||
|
sliderAdapter = sliderAdapter,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier.hoverable(interactionSource = interactionSource),
|
||||||
|
measurePolicy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and [remember] [ScrollbarAdapter] for
|
||||||
|
* scrollable container with the given instance [ScrollState].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberScrollbarAdapter(
|
||||||
|
scrollState: ScrollState,
|
||||||
|
): ScrollbarAdapter = remember(scrollState) {
|
||||||
|
ScrollbarAdapter(scrollState)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and [remember] [ScrollbarAdapter] for
|
||||||
|
* lazy scrollable container with the given instance [LazyListState].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberScrollbarAdapter(
|
||||||
|
scrollState: LazyListState,
|
||||||
|
): ScrollbarAdapter = remember(scrollState) {
|
||||||
|
ScrollbarAdapter(scrollState)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and [remember] [ScrollbarAdapter] for lazy grid with
|
||||||
|
* the given instance of [LazyGridState].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberScrollbarAdapter(
|
||||||
|
scrollState: LazyGridState,
|
||||||
|
): ScrollbarAdapter = remember(scrollState) {
|
||||||
|
ScrollbarAdapter(scrollState)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollbarAdapter for Modifier.verticalScroll and Modifier.horizontalScroll
|
||||||
|
*
|
||||||
|
* [scrollState] is instance of [ScrollState] which is used by scrollable component
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* val state = rememberScrollState(0)
|
||||||
|
*
|
||||||
|
* Box(Modifier.fillMaxSize()) {
|
||||||
|
* Box(modifier = Modifier.verticalScroll(state)) {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* VerticalScrollbar(
|
||||||
|
* adapter = rememberScrollbarAdapter(state)
|
||||||
|
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
fun ScrollbarAdapter(
|
||||||
|
scrollState: ScrollState,
|
||||||
|
): ScrollbarAdapter = ScrollableScrollbarAdapter(scrollState)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollbarAdapter for lazy lists.
|
||||||
|
*
|
||||||
|
* [scrollState] is instance of [LazyListState] which is used by scrollable component
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* Box(Modifier.fillMaxSize()) {
|
||||||
|
* val state = rememberLazyListState()
|
||||||
|
*
|
||||||
|
* LazyColumn(state = state) {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* VerticalScrollbar(
|
||||||
|
* adapter = rememberScrollbarAdapter(state)
|
||||||
|
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
fun ScrollbarAdapter(
|
||||||
|
scrollState: LazyListState,
|
||||||
|
): ScrollbarAdapter = LazyListScrollbarAdapter(scrollState)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollbarAdapter for lazy grids.
|
||||||
|
*
|
||||||
|
* [scrollState] is instance of [LazyGridState] which is used by scrollable component
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* Box(Modifier.fillMaxSize()) {
|
||||||
|
* val state = rememberLazyGridState()
|
||||||
|
*
|
||||||
|
* LazyVerticalGrid(columns = ..., state = state) {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* VerticalScrollbar(
|
||||||
|
* adapter = rememberScrollbarAdapter(state)
|
||||||
|
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
fun ScrollbarAdapter(
|
||||||
|
scrollState: LazyGridState,
|
||||||
|
): ScrollbarAdapter = LazyGridScrollbarAdapter(scrollState)
|
||||||
|
|
||||||
|
private val SliderAdapter.thumbPixelRange: IntRange
|
||||||
|
get() {
|
||||||
|
val start = position.roundToInt()
|
||||||
|
val endExclusive = start + thumbSize.roundToInt()
|
||||||
|
|
||||||
|
return (start until endExclusive)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val IntRange.size get() = last + 1 - first
|
||||||
|
|
||||||
|
private fun verticalMeasurePolicy(
|
||||||
|
sliderAdapter: SliderAdapter,
|
||||||
|
setContainerSize: (Int) -> Unit,
|
||||||
|
scrollThickness: Int,
|
||||||
|
) = MeasurePolicy { measurables, constraints ->
|
||||||
|
setContainerSize(constraints.maxHeight)
|
||||||
|
val pixelRange = sliderAdapter.thumbPixelRange
|
||||||
|
val placeable = measurables.first().measure(
|
||||||
|
Constraints.fixed(
|
||||||
|
constraints.constrainWidth(scrollThickness),
|
||||||
|
pixelRange.size,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
layout(placeable.width, constraints.maxHeight) {
|
||||||
|
placeable.place(0, pixelRange.first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun horizontalMeasurePolicy(
|
||||||
|
sliderAdapter: SliderAdapter,
|
||||||
|
setContainerSize: (Int) -> Unit,
|
||||||
|
scrollThickness: Int,
|
||||||
|
) = MeasurePolicy { measurables, constraints ->
|
||||||
|
setContainerSize(constraints.maxWidth)
|
||||||
|
val pixelRange = sliderAdapter.thumbPixelRange
|
||||||
|
val placeable = measurables.first().measure(
|
||||||
|
Constraints.fixed(
|
||||||
|
pixelRange.size,
|
||||||
|
constraints.constrainHeight(scrollThickness),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
layout(constraints.maxWidth, placeable.height) {
|
||||||
|
placeable.place(pixelRange.first, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.scrollbarDrag(
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
draggedInteraction: MutableState<DragInteraction.Start?>,
|
||||||
|
sliderAdapter: SliderAdapter,
|
||||||
|
): Modifier = composed {
|
||||||
|
val currentInteractionSource by rememberUpdatedState(interactionSource)
|
||||||
|
val currentDraggedInteraction by rememberUpdatedState(draggedInteraction)
|
||||||
|
val currentSliderAdapter by rememberUpdatedState(sliderAdapter)
|
||||||
|
|
||||||
|
pointerInput(Unit) {
|
||||||
|
awaitEachGesture {
|
||||||
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
val interaction = DragInteraction.Start()
|
||||||
|
currentInteractionSource.tryEmit(interaction)
|
||||||
|
currentDraggedInteraction.value = interaction
|
||||||
|
currentSliderAdapter.onDragStarted()
|
||||||
|
val isSuccess = drag(down.id) { change ->
|
||||||
|
currentSliderAdapter.onDragDelta(change.positionChange())
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
val finishInteraction = if (isSuccess) {
|
||||||
|
DragInteraction.Stop(interaction)
|
||||||
|
} else {
|
||||||
|
DragInteraction.Cancel(interaction)
|
||||||
|
}
|
||||||
|
currentInteractionSource.tryEmit(finishInteraction)
|
||||||
|
currentDraggedInteraction.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.compose.foundation.scrollbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines how to scroll the scrollable component and how to display a scrollbar for it.
|
||||||
|
*
|
||||||
|
* The values of this interface are typically in pixels, but do not have to be.
|
||||||
|
* It's possible to create an adapter with any scroll range of `Double` values.
|
||||||
|
*/
|
||||||
|
interface ScrollbarAdapter {
|
||||||
|
|
||||||
|
// We use `Double` values here in order to allow scrolling both very large (think LazyList with
|
||||||
|
// millions of items) and very small (think something whose natural coordinates are less than 1)
|
||||||
|
// content.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll offset of the content inside the scrollable component.
|
||||||
|
*
|
||||||
|
* For example, a value of `100` could mean the content is scrolled by 100 pixels from the
|
||||||
|
* start.
|
||||||
|
*/
|
||||||
|
val scrollOffset: Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the scrollable content, on the scrollable axis.
|
||||||
|
*/
|
||||||
|
val contentSize: Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the viewport, on the scrollable axis.
|
||||||
|
*/
|
||||||
|
val viewportSize: Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantly jump to [scrollOffset].
|
||||||
|
*
|
||||||
|
* @param scrollOffset target offset to jump to, value will be coerced to the valid
|
||||||
|
* scroll range.
|
||||||
|
*/
|
||||||
|
suspend fun scrollTo(scrollOffset: Double)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum scroll offset of the scrollable content.
|
||||||
|
*/
|
||||||
|
val ScrollbarAdapter.maxScrollOffset: Double
|
||||||
|
get() = (contentSize - viewportSize).coerceAtLeast(0.0)
|
||||||
|
|
||||||
|
internal class ScrollableScrollbarAdapter(
|
||||||
|
private val scrollState: ScrollState,
|
||||||
|
) : ScrollbarAdapter {
|
||||||
|
|
||||||
|
override val scrollOffset: Double get() = scrollState.value.toDouble()
|
||||||
|
|
||||||
|
override suspend fun scrollTo(scrollOffset: Double) {
|
||||||
|
scrollState.scrollTo(scrollOffset.roundToInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
override val contentSize: Double
|
||||||
|
// This isn't strictly correct, as the actual content can be smaller
|
||||||
|
// than the viewport when scrollState.maxValue is 0, but the scrollbar
|
||||||
|
// doesn't really care as long as contentSize <= viewportSize; it's
|
||||||
|
// just not showing itself
|
||||||
|
get() = scrollState.maxValue + viewportSize
|
||||||
|
|
||||||
|
override val viewportSize: Double
|
||||||
|
get() = scrollState.viewportSize.toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for [LazyListScrollbarAdapter] and [LazyGridScrollbarAdapter],
|
||||||
|
* and in the future maybe other lazy widgets that lay out their content in lines.
|
||||||
|
*/
|
||||||
|
internal abstract class LazyLineContentAdapter : ScrollbarAdapter {
|
||||||
|
|
||||||
|
// Implement the adapter in terms of "lines", which means either rows,
|
||||||
|
// (for a vertically scrollable widget) or columns (for a horizontally
|
||||||
|
// scrollable one).
|
||||||
|
// For LazyList this translates directly to items; for LazyGrid, it
|
||||||
|
// translates to rows/columns of items.
|
||||||
|
|
||||||
|
class VisibleLine(
|
||||||
|
val index: Int,
|
||||||
|
val offset: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the first visible line, if any.
|
||||||
|
*/
|
||||||
|
protected abstract fun firstVisibleLine(): VisibleLine?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the total number of lines.
|
||||||
|
*/
|
||||||
|
protected abstract fun totalLineCount(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sum of content padding (before+after) on the scrollable axis.
|
||||||
|
*/
|
||||||
|
protected abstract fun contentPadding(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll immediately to the given line, and offset it by [scrollOffset] pixels.
|
||||||
|
*/
|
||||||
|
protected abstract suspend fun snapToLine(lineIndex: Int, scrollOffset: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll from the current position by the given amount of pixels.
|
||||||
|
*/
|
||||||
|
protected abstract suspend fun scrollBy(value: Float)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the average size (on the scrollable axis) of the visible lines.
|
||||||
|
*/
|
||||||
|
protected abstract fun averageVisibleLineSize(): Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The spacing between lines.
|
||||||
|
*/
|
||||||
|
protected abstract val lineSpacing: Int
|
||||||
|
|
||||||
|
private val averageVisibleLineSize by derivedStateOf {
|
||||||
|
if (totalLineCount() == 0) {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
averageVisibleLineSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val averageVisibleLineSizeWithSpacing get() = averageVisibleLineSize + lineSpacing
|
||||||
|
|
||||||
|
override val scrollOffset: Double
|
||||||
|
get() {
|
||||||
|
val firstVisibleLine = firstVisibleLine()
|
||||||
|
return if (firstVisibleLine == null) {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
firstVisibleLine.index * averageVisibleLineSizeWithSpacing - firstVisibleLine.offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val contentSize: Double
|
||||||
|
get() {
|
||||||
|
val totalLineCount = totalLineCount()
|
||||||
|
return averageVisibleLineSize * totalLineCount +
|
||||||
|
lineSpacing * (totalLineCount - 1).coerceAtLeast(0) +
|
||||||
|
contentPadding()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun scrollTo(scrollOffset: Double) {
|
||||||
|
val distance = scrollOffset - this@LazyLineContentAdapter.scrollOffset
|
||||||
|
|
||||||
|
// if we scroll less than viewport we need to use scrollBy function to avoid
|
||||||
|
// undesirable scroll jumps (when an item size is different)
|
||||||
|
//
|
||||||
|
// if we scroll more than viewport we should immediately jump to this position
|
||||||
|
// without recreating all items between the current and the new position
|
||||||
|
if (abs(distance) <= viewportSize) {
|
||||||
|
scrollBy(distance.toFloat())
|
||||||
|
} else {
|
||||||
|
snapTo(scrollOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun snapTo(scrollOffset: Double) {
|
||||||
|
val scrollOffsetCoerced = scrollOffset.coerceIn(0.0, maxScrollOffset)
|
||||||
|
|
||||||
|
val index = (scrollOffsetCoerced / averageVisibleLineSizeWithSpacing)
|
||||||
|
.toInt()
|
||||||
|
.coerceAtLeast(0)
|
||||||
|
.coerceAtMost(totalLineCount() - 1)
|
||||||
|
|
||||||
|
val offset = (scrollOffsetCoerced - index * averageVisibleLineSizeWithSpacing)
|
||||||
|
.toInt()
|
||||||
|
.coerceAtLeast(0)
|
||||||
|
|
||||||
|
snapToLine(lineIndex = index, scrollOffset = offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class LazyListScrollbarAdapter(
|
||||||
|
private val scrollState: LazyListState,
|
||||||
|
) : LazyLineContentAdapter() {
|
||||||
|
|
||||||
|
override val viewportSize: Double
|
||||||
|
get() = with(scrollState.layoutInfo) {
|
||||||
|
if (orientation == Orientation.Vertical) {
|
||||||
|
viewportSize.height
|
||||||
|
} else {
|
||||||
|
viewportSize.width
|
||||||
|
}
|
||||||
|
}.toDouble()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A heuristic that tries to ignore the "currently stickied" header because it breaks the other
|
||||||
|
* computations in this adapter:
|
||||||
|
* - The currently stickied header always appears in the list of visible items, with its
|
||||||
|
* regular index. This makes [firstVisibleLine] always return its index, even if the list has
|
||||||
|
* been scrolled far beyond it.
|
||||||
|
* - [averageVisibleLineSize] calculates the average size in O(1) by assuming that items don't
|
||||||
|
* overlap, and the stickied item breaks this assumption.
|
||||||
|
*
|
||||||
|
* Attempts to return the index into `visibleItemsInfo` of the first non-currently-stickied (it
|
||||||
|
* could be sticky, but not stickied to the top of the list right now) item, if there is one.
|
||||||
|
*
|
||||||
|
* Note that this heuristic breaks down if the sticky header covers the entire list, so that
|
||||||
|
* it's the only visible item for some portion of the scroll range. But there's currently no
|
||||||
|
* known better way to solve it, and it's a relatively unusual case.
|
||||||
|
*/
|
||||||
|
private fun firstFloatingVisibleItemIndex() = with(scrollState.layoutInfo.visibleItemsInfo) {
|
||||||
|
when (size) {
|
||||||
|
0 -> null
|
||||||
|
1 -> 0
|
||||||
|
else -> {
|
||||||
|
val first = this[0]
|
||||||
|
val second = this[1]
|
||||||
|
// If either the indices or the offsets aren't continuous, then the first item is
|
||||||
|
// sticky, so we return 1
|
||||||
|
if ((first.index < second.index - 1) ||
|
||||||
|
(first.offset + first.size + lineSpacing > second.offset)
|
||||||
|
) {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun firstVisibleLine(): VisibleLine? {
|
||||||
|
val firstFloatingVisibleIndex = firstFloatingVisibleItemIndex() ?: return null
|
||||||
|
val firstFloatingItem = scrollState.layoutInfo.visibleItemsInfo[firstFloatingVisibleIndex]
|
||||||
|
return VisibleLine(
|
||||||
|
index = firstFloatingItem.index,
|
||||||
|
offset = firstFloatingItem.offset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun totalLineCount() = scrollState.layoutInfo.totalItemsCount
|
||||||
|
|
||||||
|
override fun contentPadding() = with(scrollState.layoutInfo) {
|
||||||
|
beforeContentPadding + afterContentPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun snapToLine(lineIndex: Int, scrollOffset: Int) {
|
||||||
|
scrollState.scrollToItem(lineIndex, scrollOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun scrollBy(value: Float) {
|
||||||
|
scrollState.scrollBy(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun averageVisibleLineSize() = with(scrollState.layoutInfo.visibleItemsInfo) {
|
||||||
|
val firstFloatingIndex = firstFloatingVisibleItemIndex() ?: return@with 0.0
|
||||||
|
val first = this[firstFloatingIndex]
|
||||||
|
val last = last()
|
||||||
|
val count = size - firstFloatingIndex
|
||||||
|
(last.offset + last.size - first.offset - (count - 1) * lineSpacing).toDouble() / count
|
||||||
|
}
|
||||||
|
|
||||||
|
override val lineSpacing get() = scrollState.layoutInfo.mainAxisItemSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class LazyGridScrollbarAdapter(
|
||||||
|
private val scrollState: LazyGridState,
|
||||||
|
) : LazyLineContentAdapter() {
|
||||||
|
|
||||||
|
override val viewportSize: Double
|
||||||
|
get() = with(scrollState.layoutInfo) {
|
||||||
|
if (isVertical) {
|
||||||
|
viewportSize.height
|
||||||
|
} else {
|
||||||
|
viewportSize.width
|
||||||
|
}
|
||||||
|
}.toDouble()
|
||||||
|
|
||||||
|
private val isVertical by lazy {
|
||||||
|
scrollState.layoutInfo.orientation == Orientation.Vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
private val unknownLine = with(LazyGridItemInfo) {
|
||||||
|
if (isVertical) UnknownRow else UnknownColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyGridItemInfo.line() = if (isVertical) row else column
|
||||||
|
|
||||||
|
private fun LazyGridItemInfo.mainAxisSize() = with(size) {
|
||||||
|
if (isVertical) height else width
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyGridItemInfo.mainAxisOffset() = with(offset) {
|
||||||
|
if (isVertical) y else x
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun slotsPerLine(): Int = scrollState.layoutInfo.visibleItemsInfo
|
||||||
|
.groupingBy { it.line() }.eachCount().maxOfOrNull { it.value } ?: 1
|
||||||
|
|
||||||
|
private fun lineOfIndex(index: Int) = index / slotsPerLine()
|
||||||
|
|
||||||
|
private fun indexOfFirstInLine(line: Int) = line * slotsPerLine()
|
||||||
|
|
||||||
|
override fun firstVisibleLine(): VisibleLine? = scrollState.layoutInfo.visibleItemsInfo
|
||||||
|
.firstOrNull { it.line() != unknownLine } // Skip exiting items
|
||||||
|
?.let { firstVisibleItem ->
|
||||||
|
VisibleLine(
|
||||||
|
index = firstVisibleItem.line(),
|
||||||
|
offset = firstVisibleItem.mainAxisOffset(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun totalLineCount(): Int {
|
||||||
|
val itemCount = scrollState.layoutInfo.totalItemsCount
|
||||||
|
return if (itemCount == 0) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
lineOfIndex(itemCount - 1) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contentPadding() = with(scrollState.layoutInfo) {
|
||||||
|
beforeContentPadding + afterContentPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun snapToLine(lineIndex: Int, scrollOffset: Int) {
|
||||||
|
scrollState.scrollToItem(
|
||||||
|
index = indexOfFirstInLine(lineIndex),
|
||||||
|
scrollOffset = scrollOffset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun scrollBy(value: Float) {
|
||||||
|
scrollState.scrollBy(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun averageVisibleLineSize(): Double {
|
||||||
|
val visibleItemsInfo = scrollState.layoutInfo.visibleItemsInfo
|
||||||
|
val indexOfFirstKnownLineItem = visibleItemsInfo.indexOfFirst { it.line() != unknownLine }
|
||||||
|
if (indexOfFirstKnownLineItem == -1) {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
val reallyVisibleItemsInfo = // Non-exiting visible items
|
||||||
|
visibleItemsInfo.subList(indexOfFirstKnownLineItem, visibleItemsInfo.size)
|
||||||
|
|
||||||
|
// Compute the size of the last line
|
||||||
|
val lastLine = reallyVisibleItemsInfo.last().line()
|
||||||
|
val lastLineSize = reallyVisibleItemsInfo
|
||||||
|
.asReversed()
|
||||||
|
.asSequence()
|
||||||
|
.takeWhile { it.line() == lastLine }
|
||||||
|
.maxOf { it.mainAxisSize() }
|
||||||
|
|
||||||
|
val first = reallyVisibleItemsInfo.first()
|
||||||
|
val last = reallyVisibleItemsInfo.last()
|
||||||
|
val lineCount = last.line() - first.line() + 1
|
||||||
|
val lineSpacingSum = (lineCount - 1) * lineSpacing
|
||||||
|
return (
|
||||||
|
last.mainAxisOffset() + lastLineSize - first.mainAxisOffset() - lineSpacingSum
|
||||||
|
).toDouble() / lineCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override val lineSpacing get() = scrollState.layoutInfo.mainAxisItemSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SliderAdapter(
|
||||||
|
private val adapter: ScrollbarAdapter,
|
||||||
|
private val trackSize: Int,
|
||||||
|
private val minHeight: Float,
|
||||||
|
private val reverseLayout: Boolean,
|
||||||
|
private val isVertical: Boolean,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val contentSize get() = adapter.contentSize
|
||||||
|
private val visiblePart: Double
|
||||||
|
get() {
|
||||||
|
val contentSize = contentSize
|
||||||
|
return if (contentSize == 0.0) {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
(adapter.viewportSize / contentSize).coerceAtMost(1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbSize
|
||||||
|
get() = (trackSize * visiblePart).coerceAtLeast(minHeight.toDouble())
|
||||||
|
|
||||||
|
private val scrollScale: Double
|
||||||
|
get() {
|
||||||
|
val extraScrollbarSpace = trackSize - thumbSize
|
||||||
|
val extraContentSpace = adapter.maxScrollOffset // == contentSize - viewportSize
|
||||||
|
return if (extraContentSpace == 0.0) 1.0 else extraScrollbarSpace / extraContentSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
private val rawPosition: Double
|
||||||
|
get() = scrollScale * adapter.scrollOffset
|
||||||
|
|
||||||
|
val position: Double
|
||||||
|
get() = if (reverseLayout) trackSize - thumbSize - rawPosition else rawPosition
|
||||||
|
|
||||||
|
// How much of the current drag was ignored because we've reached the end of the scrollbar area
|
||||||
|
private var unscrolledDragDistance = 0.0
|
||||||
|
|
||||||
|
/** Called when the thumb dragging starts */
|
||||||
|
fun onDragStarted() {
|
||||||
|
unscrolledDragDistance = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setPosition(value: Double) {
|
||||||
|
val rawPosition = if (reverseLayout) {
|
||||||
|
trackSize - thumbSize - value
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
adapter.scrollTo(rawPosition / scrollScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dragMutex = Mutex()
|
||||||
|
|
||||||
|
/** Called on every movement while dragging the thumb */
|
||||||
|
fun onDragDelta(offset: Offset) {
|
||||||
|
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||||
|
// Mutex is used to ensure that all earlier drag deltas were applied
|
||||||
|
// before calculating a new raw position
|
||||||
|
dragMutex.withLock {
|
||||||
|
val dragDelta = if (isVertical) offset.y else offset.x
|
||||||
|
val maxScrollPosition = adapter.maxScrollOffset * scrollScale
|
||||||
|
val currentPosition = position
|
||||||
|
val targetPosition =
|
||||||
|
(currentPosition + dragDelta + unscrolledDragDistance).coerceIn(
|
||||||
|
0.0,
|
||||||
|
maxScrollPosition,
|
||||||
|
)
|
||||||
|
val sliderDelta = targetPosition - currentPosition
|
||||||
|
|
||||||
|
// Have to add to position for smooth content scroll if the items are of different size
|
||||||
|
val newPos = position + sliderDelta
|
||||||
|
setPosition(newPos)
|
||||||
|
unscrolledDragDistance += dragDelta - sliderDelta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ fun SearchBarInputField(
|
|||||||
trailingIcon = trailingIcon?.let { trailing -> { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } } },
|
trailingIcon = trailingIcon?.let { trailing -> { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } } },
|
||||||
shape = SearchBarDefaults.inputFieldShape,
|
shape = SearchBarDefaults.inputFieldShape,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
|
contentPadding = TextFieldDefaults.contentPaddingWithLabel(),
|
||||||
container = {},
|
container = {},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
@file:Suppress("ktlint:standard:property-naming")
|
||||||
|
|
||||||
package androidx.compose.material3
|
package androidx.compose.material3
|
||||||
|
|
||||||
import androidx.compose.animation.core.AnimationSpec
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
import androidx.compose.animation.core.TweenSpec
|
import androidx.compose.animation.core.TweenSpec
|
||||||
import androidx.compose.animation.core.animate
|
import androidx.compose.animation.core.animate
|
||||||
import androidx.compose.animation.core.exponentialDecay
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
|
||||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||||
import androidx.compose.foundation.gestures.anchoredHorizontalDraggable
|
import androidx.compose.foundation.gestures.anchoredHorizontalDraggable
|
||||||
@@ -53,8 +53,6 @@ import kotlinx.coroutines.CancellationException
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
|
private val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
|
||||||
private const val DrawerPositionalThreshold = 0.5f
|
|
||||||
private val DrawerVelocityThreshold = 400.dp
|
|
||||||
|
|
||||||
@Suppress("NotCloseable")
|
@Suppress("NotCloseable")
|
||||||
@Stable
|
@Stable
|
||||||
@@ -65,11 +63,7 @@ class DrawerState2(
|
|||||||
|
|
||||||
val anchoredDraggableState = AnchoredDraggableState(
|
val anchoredDraggableState = AnchoredDraggableState(
|
||||||
initialValue = initialValue,
|
initialValue = initialValue,
|
||||||
snapAnimationSpec = AnimationSpec,
|
|
||||||
decayAnimationSpec = exponentialDecay(7.5f),
|
|
||||||
confirmValueChange = confirmStateChange,
|
confirmValueChange = confirmStateChange,
|
||||||
positionalThreshold = { distance -> distance * DrawerPositionalThreshold },
|
|
||||||
velocityThreshold = { with(requireDensity()) { DrawerVelocityThreshold.toPx() } },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,10 +224,8 @@ class DrawerState2(
|
|||||||
fun rememberDrawerState2(
|
fun rememberDrawerState2(
|
||||||
initialValue: DrawerValue,
|
initialValue: DrawerValue,
|
||||||
confirmStateChange: (DrawerValue) -> Boolean = { true },
|
confirmStateChange: (DrawerValue) -> Boolean = { true },
|
||||||
): DrawerState2 {
|
): DrawerState2 = rememberSaveable(saver = DrawerState2.Saver(confirmStateChange)) {
|
||||||
return rememberSaveable(saver = DrawerState2.Saver(confirmStateChange)) {
|
DrawerState2(initialValue, confirmStateChange)
|
||||||
DrawerState2(initialValue, confirmStateChange)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -266,6 +258,10 @@ fun ModalSideDrawer(
|
|||||||
enableDragFromStartToEnd = drawerState.isOpen,
|
enableDragFromStartToEnd = drawerState.isOpen,
|
||||||
enableDragFromEndToStart = drawerState.isClosed,
|
enableDragFromEndToStart = drawerState.isClosed,
|
||||||
enabled = gesturesEnabled,
|
enabled = gesturesEnabled,
|
||||||
|
flingBehavior = AnchoredDraggableDefaults.flingBehavior(
|
||||||
|
state = drawerState.anchoredDraggableState,
|
||||||
|
animationSpec = AnimationSpec,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val radius by remember {
|
val radius by remember {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ package androidx.compose.material3.fork
|
|||||||
|
|
||||||
import androidx.annotation.FloatRange
|
import androidx.annotation.FloatRange
|
||||||
import androidx.compose.animation.core.SpringSpec
|
import androidx.compose.animation.core.SpringSpec
|
||||||
import androidx.compose.animation.core.exponentialDecay
|
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
|
||||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
@@ -38,21 +38,16 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
||||||
private val SnapAnimationSpec = SpringSpec<Float>()
|
private val SnapAnimationSpec = SpringSpec<Float>()
|
||||||
|
|
||||||
// TODO: Update to M3 spec once it's available
|
|
||||||
private val DecayAnimationSpec = exponentialDecay<Float>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State of the [SwipeToDismissBox] composable.
|
* State of the [SwipeToDismissBox] composable.
|
||||||
*
|
*
|
||||||
* @param initialValue The initial value of the state.
|
* @param initialValue The initial value of the state.
|
||||||
* @param density The density that this state can use to convert values to and from dp.
|
* @param density The density that this state can use to convert values to and from dp.
|
||||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||||
* @param positionalThreshold The positional threshold to be used when calculating the target state
|
|
||||||
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
|
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
|
||||||
* the start of a transition. It will be, depending on the direction of the interaction, added or
|
* the start of a transition. It will be, depending on the direction of the interaction, added or
|
||||||
* subtracted from/to the origin offset. It should always be a positive value.
|
* subtracted from/to the origin offset. It should always be a positive value.
|
||||||
@@ -61,16 +56,11 @@ class SwipeToDismissBoxState(
|
|||||||
initialValue: SwipeToDismissBoxValue,
|
initialValue: SwipeToDismissBoxValue,
|
||||||
internal val density: Density,
|
internal val density: Density,
|
||||||
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
|
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
|
||||||
positionalThreshold: (totalDistance: Float) -> Float,
|
|
||||||
) {
|
) {
|
||||||
internal val anchoredDraggableState =
|
internal val anchoredDraggableState =
|
||||||
AnchoredDraggableState(
|
AnchoredDraggableState(
|
||||||
initialValue = initialValue,
|
initialValue = initialValue,
|
||||||
snapAnimationSpec = SnapAnimationSpec,
|
|
||||||
decayAnimationSpec = DecayAnimationSpec,
|
|
||||||
confirmValueChange = confirmValueChange,
|
confirmValueChange = confirmValueChange,
|
||||||
positionalThreshold = positionalThreshold,
|
|
||||||
velocityThreshold = { with(density) { DismissVelocityThreshold.toPx() } },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
internal val offset: Float get() = anchoredDraggableState.offset
|
internal val offset: Float get() = anchoredDraggableState.offset
|
||||||
@@ -150,7 +140,6 @@ class SwipeToDismissBoxState(
|
|||||||
*/
|
*/
|
||||||
fun Saver(
|
fun Saver(
|
||||||
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean,
|
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean,
|
||||||
positionalThreshold: (totalDistance: Float) -> Float,
|
|
||||||
density: Density,
|
density: Density,
|
||||||
) = Saver<SwipeToDismissBoxState, SwipeToDismissBoxValue>(
|
) = Saver<SwipeToDismissBoxState, SwipeToDismissBoxValue>(
|
||||||
save = { it.currentValue },
|
save = { it.currentValue },
|
||||||
@@ -159,7 +148,6 @@ class SwipeToDismissBoxState(
|
|||||||
it,
|
it,
|
||||||
density,
|
density,
|
||||||
confirmValueChange,
|
confirmValueChange,
|
||||||
positionalThreshold,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -171,7 +159,6 @@ class SwipeToDismissBoxState(
|
|||||||
*
|
*
|
||||||
* @param initialValue The initial value of the state.
|
* @param initialValue The initial value of the state.
|
||||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||||
* @param positionalThreshold The positional threshold to be used when calculating the target state
|
|
||||||
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
|
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
|
||||||
* the start of a transition. It will be, depending on the direction of the interaction, added or
|
* the start of a transition. It will be, depending on the direction of the interaction, added or
|
||||||
* subtracted from/to the origin offset. It should always be a positive value.
|
* subtracted from/to the origin offset. It should always be a positive value.
|
||||||
@@ -180,18 +167,15 @@ class SwipeToDismissBoxState(
|
|||||||
fun rememberSwipeToDismissBoxState(
|
fun rememberSwipeToDismissBoxState(
|
||||||
initialValue: SwipeToDismissBoxValue = SwipeToDismissBoxValue.Settled,
|
initialValue: SwipeToDismissBoxValue = SwipeToDismissBoxValue.Settled,
|
||||||
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
|
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
|
||||||
positionalThreshold: (totalDistance: Float) -> Float =
|
|
||||||
SwipeToDismissBoxDefaults.positionalThreshold,
|
|
||||||
): SwipeToDismissBoxState {
|
): SwipeToDismissBoxState {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
return rememberSaveable(
|
return rememberSaveable(
|
||||||
saver = SwipeToDismissBoxState.Saver(
|
saver = SwipeToDismissBoxState.Saver(
|
||||||
confirmValueChange = confirmValueChange,
|
confirmValueChange = confirmValueChange,
|
||||||
density = density,
|
density = density,
|
||||||
positionalThreshold = positionalThreshold,
|
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
SwipeToDismissBoxState(initialValue, density, confirmValueChange, positionalThreshold)
|
SwipeToDismissBoxState(initialValue, density, confirmValueChange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +208,11 @@ fun SwipeToDismissBox(
|
|||||||
enableDragFromStartToEnd = enableDismissFromStartToEnd,
|
enableDragFromStartToEnd = enableDismissFromStartToEnd,
|
||||||
enableDragFromEndToStart = enableDismissFromEndToStart,
|
enableDragFromEndToStart = enableDismissFromEndToStart,
|
||||||
enabled = gesturesEnabled && state.currentValue == SwipeToDismissBoxValue.Settled,
|
enabled = gesturesEnabled && state.currentValue == SwipeToDismissBoxValue.Settled,
|
||||||
|
flingBehavior = AnchoredDraggableDefaults.flingBehavior(
|
||||||
|
state = state.anchoredDraggableState,
|
||||||
|
positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold,
|
||||||
|
animationSpec = SnapAnimationSpec,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
propagateMinConstraints = true,
|
propagateMinConstraints = true,
|
||||||
) {
|
) {
|
||||||
@@ -251,5 +240,3 @@ fun SwipeToDismissBox(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DismissVelocityThreshold = 125.dp
|
|
||||||
+14
-36
@@ -912,16 +912,12 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
/**
|
/**
|
||||||
* Convert screen to source x coordinate.
|
* Convert screen to source x coordinate.
|
||||||
*/
|
*/
|
||||||
private fun viewToSourceX(vx: Float): Float {
|
private fun viewToSourceX(vx: Float): Float = if (vTranslate == null) Float.NaN else (vx - vTranslate!!.x) / scale
|
||||||
return if (vTranslate == null) Float.NaN else (vx - vTranslate!!.x) / scale
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert screen to source y coordinate.
|
* Convert screen to source y coordinate.
|
||||||
*/
|
*/
|
||||||
private fun viewToSourceY(vy: Float): Float {
|
private fun viewToSourceY(vy: Float): Float = if (vTranslate == null) Float.NaN else (vy - vTranslate!!.y) / scale
|
||||||
return if (vTranslate == null) Float.NaN else (vy - vTranslate!!.y) / scale
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert screen coordinate to source coordinate.
|
* Convert screen coordinate to source coordinate.
|
||||||
@@ -929,9 +925,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
* @param vxy view X/Y coordinate.
|
* @param vxy view X/Y coordinate.
|
||||||
* @return a coordinate representing the corresponding source coordinate.
|
* @return a coordinate representing the corresponding source coordinate.
|
||||||
*/
|
*/
|
||||||
fun viewToSourceCoord(vxy: PointF): PointF? {
|
fun viewToSourceCoord(vxy: PointF): PointF? = viewToSourceCoord(vxy.x, vxy.y, PointF())
|
||||||
return viewToSourceCoord(vxy.x, vxy.y, PointF())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert screen coordinate to source coordinate.
|
* Convert screen coordinate to source coordinate.
|
||||||
@@ -952,16 +946,12 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
/**
|
/**
|
||||||
* Convert source to view x coordinate.
|
* Convert source to view x coordinate.
|
||||||
*/
|
*/
|
||||||
private fun sourceToViewX(sx: Float): Float {
|
private fun sourceToViewX(sx: Float): Float = if (vTranslate == null) Float.NaN else sx * scale + vTranslate!!.x
|
||||||
return if (vTranslate == null) Float.NaN else sx * scale + vTranslate!!.x
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert source to view y coordinate.
|
* Convert source to view y coordinate.
|
||||||
*/
|
*/
|
||||||
private fun sourceToViewY(sy: Float): Float {
|
private fun sourceToViewY(sy: Float): Float = if (vTranslate == null) Float.NaN else sy * scale + vTranslate!!.y
|
||||||
return if (vTranslate == null) Float.NaN else sy * scale + vTranslate!!.y
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert source coordinate to view coordinate.
|
* Convert source coordinate to view coordinate.
|
||||||
@@ -969,9 +959,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
* @param sxy source coordinates to convert.
|
* @param sxy source coordinates to convert.
|
||||||
* @return view coordinates.
|
* @return view coordinates.
|
||||||
*/
|
*/
|
||||||
fun sourceToViewCoord(sxy: PointF?): PointF? {
|
fun sourceToViewCoord(sxy: PointF?): PointF? = sourceToViewCoord(sxy!!.x, sxy.y, PointF())
|
||||||
return sourceToViewCoord(sxy!!.x, sxy.y, PointF())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert source coordinate to view coordinate.
|
* Convert source coordinate to view coordinate.
|
||||||
@@ -1053,9 +1041,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
/**
|
/**
|
||||||
* Adjust a requested scale to be within the allowed limits.
|
* Adjust a requested scale to be within the allowed limits.
|
||||||
*/
|
*/
|
||||||
private fun limitedScale(targetScale: Float): Float {
|
private fun limitedScale(targetScale: Float): Float = targetScale.coerceIn(minScale(), maxScale)
|
||||||
return targetScale.coerceIn(minScale(), maxScale)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a selected type of easing.
|
* Apply a selected type of easing.
|
||||||
@@ -1067,12 +1053,10 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
* @param duration Anm duration
|
* @param duration Anm duration
|
||||||
* @return Current value
|
* @return Current value
|
||||||
*/
|
*/
|
||||||
private fun ease(type: Int, time: Long, from: Float, change: Float, duration: Long): Float {
|
private fun ease(type: Int, time: Long, from: Float, change: Float, duration: Long): Float = when (type) {
|
||||||
return when (type) {
|
EASE_IN_OUT_QUAD -> easeInOutQuad(time, from, change, duration)
|
||||||
EASE_IN_OUT_QUAD -> easeInOutQuad(time, from, change, duration)
|
EASE_OUT_QUAD -> easeOutQuad(time, from, change, duration)
|
||||||
EASE_OUT_QUAD -> easeOutQuad(time, from, change, duration)
|
else -> throw IllegalStateException("Unexpected easing type: $type")
|
||||||
else -> throw IllegalStateException("Unexpected easing type: $type")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1199,9 +1183,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
*
|
*
|
||||||
* @return the minimum scale as a source/view pixels ratio.
|
* @return the minimum scale as a source/view pixels ratio.
|
||||||
*/
|
*/
|
||||||
fun getMinScale(): Float {
|
fun getMinScale(): Float = minScale()
|
||||||
return minScale()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the minimum scale allowed. A value of 1 means 1:1 pixels at minimum scale. You may wish to set this according
|
* Set the minimum scale allowed. A value of 1 means 1:1 pixels at minimum scale. You may wish to set this according
|
||||||
@@ -1325,9 +1307,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
* @param sCenter Target center point
|
* @param sCenter Target center point
|
||||||
* @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
|
* @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
|
||||||
*/
|
*/
|
||||||
fun animateCenter(sCenter: PointF): AnimationBuilder? {
|
fun animateCenter(sCenter: PointF): AnimationBuilder? = if (isReady) AnimationBuilder(sCenter) else null
|
||||||
return if (isReady) AnimationBuilder(sCenter) else null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image
|
* Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image
|
||||||
@@ -1337,9 +1317,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
* @param sCenter Target source center.
|
* @param sCenter Target source center.
|
||||||
* @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
|
* @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
|
||||||
*/
|
*/
|
||||||
fun animateScaleAndCenter(scale: Float, sCenter: PointF?): AnimationBuilder? {
|
fun animateScaleAndCenter(scale: Float, sCenter: PointF?): AnimationBuilder? = if (isReady) AnimationBuilder(scale, sCenter) else null
|
||||||
return if (isReady) AnimationBuilder(scale, sCenter) else null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event listener, allowing subclasses and activities to be notified of significant events.
|
* An event listener, allowing subclasses and activities to be notified of significant events.
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
|
|||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
import coil3.EventListener
|
import coil3.EventListener
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import coil3.asCoilImage
|
import coil3.asImage
|
||||||
import coil3.gif.AnimatedImageDecoder
|
import coil3.gif.AnimatedImageDecoder
|
||||||
import coil3.gif.GifDecoder
|
import coil3.gif.GifDecoder
|
||||||
import coil3.network.ktor.KtorNetworkFetcherFactory
|
import coil3.network.ktor.KtorNetworkFetcherFactory
|
||||||
@@ -43,13 +43,14 @@ import com.hippo.ehviewer.coil.CropBorderInterceptor
|
|||||||
import com.hippo.ehviewer.coil.DownloadThumbInterceptor
|
import com.hippo.ehviewer.coil.DownloadThumbInterceptor
|
||||||
import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
|
import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
|
||||||
import com.hippo.ehviewer.coil.MergeInterceptor
|
import com.hippo.ehviewer.coil.MergeInterceptor
|
||||||
|
import com.hippo.ehviewer.cronet.cronetHttpClient
|
||||||
import com.hippo.ehviewer.dailycheck.checkDawn
|
import com.hippo.ehviewer.dailycheck.checkDawn
|
||||||
import com.hippo.ehviewer.dao.SearchDatabase
|
import com.hippo.ehviewer.dao.SearchDatabase
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
import com.hippo.ehviewer.download.DownloadsFilterMode
|
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||||
import com.hippo.ehviewer.ktbuilder.diskCache
|
import com.hippo.ehviewer.ktbuilder.diskCache
|
||||||
import com.hippo.ehviewer.ktbuilder.imageLoader
|
import com.hippo.ehviewer.ktbuilder.imageLoader
|
||||||
import com.hippo.ehviewer.ktor.CronetEngine
|
import com.hippo.ehviewer.ktor.Cronet
|
||||||
import com.hippo.ehviewer.legacy.cleanObsoleteCache
|
import com.hippo.ehviewer.legacy.cleanObsoleteCache
|
||||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||||
import com.hippo.ehviewer.ui.lockObserver
|
import com.hippo.ehviewer.ui.lockObserver
|
||||||
@@ -58,6 +59,7 @@ import com.hippo.ehviewer.util.AppConfig
|
|||||||
import com.hippo.ehviewer.util.Crash
|
import com.hippo.ehviewer.util.Crash
|
||||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastO
|
||||||
import com.hippo.ehviewer.util.isAtLeastP
|
import com.hippo.ehviewer.util.isAtLeastP
|
||||||
import com.hippo.ehviewer.util.isAtLeastS
|
import com.hippo.ehviewer.util.isAtLeastS
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
@@ -77,7 +79,9 @@ import splitties.init.appCtx
|
|||||||
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
|
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
|
||||||
private val lifecycleScope = lifecycle.coroutineScope
|
private val lifecycleScope = lifecycle.coroutineScope
|
||||||
|
|
||||||
class EhApplication : Application(), SingletonImageLoader.Factory {
|
class EhApplication :
|
||||||
|
Application(),
|
||||||
|
SingletonImageLoader.Factory {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
// Initialize Settings on first access
|
// Initialize Settings on first access
|
||||||
lifecycleScope.launchIO {
|
lifecycleScope.launchIO {
|
||||||
@@ -168,8 +172,10 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
add(KtorNetworkFetcherFactory { ktorClient })
|
add(KtorNetworkFetcherFactory { ktorClient })
|
||||||
add(MergeInterceptor)
|
add(MergeInterceptor)
|
||||||
add(DownloadThumbInterceptor)
|
add(DownloadThumbInterceptor)
|
||||||
|
if (isAtLeastO) {
|
||||||
|
add(HardwareBitmapInterceptor)
|
||||||
|
}
|
||||||
add(CropBorderInterceptor)
|
add(CropBorderInterceptor)
|
||||||
add(HardwareBitmapInterceptor)
|
|
||||||
if (isAtLeastP) {
|
if (isAtLeastP) {
|
||||||
add(AnimatedImageDecoder.Factory(false))
|
add(AnimatedImageDecoder.Factory(false))
|
||||||
} else {
|
} else {
|
||||||
@@ -179,7 +185,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
diskCache { imageCache }
|
diskCache { imageCache }
|
||||||
crossfade(300)
|
crossfade(300)
|
||||||
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
||||||
if (drawable != null) error(drawable.asCoilImage(true))
|
if (drawable != null) error(drawable.asImage(true))
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
logger(DebugLogger())
|
logger(DebugLogger())
|
||||||
} else {
|
} else {
|
||||||
@@ -195,7 +201,10 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val ktorClient by lazy {
|
val ktorClient by lazy {
|
||||||
HttpClient(CronetEngine) {
|
HttpClient(Cronet) {
|
||||||
|
engine {
|
||||||
|
client = cronetHttpClient
|
||||||
|
}
|
||||||
install(HttpCookies) {
|
install(HttpCookies) {
|
||||||
storage = EhCookieStore
|
storage = EhCookieStore
|
||||||
}
|
}
|
||||||
@@ -203,7 +212,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val noRedirectKtorClient by lazy {
|
val noRedirectKtorClient by lazy {
|
||||||
HttpClient(CronetEngine) {
|
HttpClient(ktorClient.engine) {
|
||||||
followRedirects = false
|
followRedirects = false
|
||||||
install(HttpCookies) {
|
install(HttpCookies) {
|
||||||
storage = EhCookieStore
|
storage = EhCookieStore
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ import com.hippo.ehviewer.dao.QuickSearch
|
|||||||
import com.hippo.ehviewer.dao.Schema17to18
|
import com.hippo.ehviewer.dao.Schema17to18
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
import com.hippo.ehviewer.util.sendTo
|
import com.hippo.ehviewer.util.sendTo
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import com.hippo.unifile.asUniFile
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import okio.Path
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
import splitties.arch.room.roomDb
|
import splitties.arch.room.roomDb
|
||||||
|
|
||||||
object EhDB {
|
object EhDB {
|
||||||
@@ -273,10 +273,10 @@ object EhDB {
|
|||||||
db.filterDao().update(filter)
|
db.filterDao().update(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportDB(context: Context, file: UniFile) {
|
fun exportDB(context: Context, file: Path) {
|
||||||
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
|
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
|
||||||
val dbFile = context.getDatabasePath(DB_NAME)
|
val dbFile = context.getDatabasePath(DB_NAME)
|
||||||
dbFile.asUniFile() sendTo file
|
dbFile.toOkioPath() sendTo file
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun importDB(context: Context, uri: Uri) {
|
suspend fun importDB(context: Context, uri: Uri) {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ fun <T> PrefDelegate<T>.asMutableState(): MutableState<T> {
|
|||||||
object Settings : DataStorePreferences(null) {
|
object Settings : DataStorePreferences(null) {
|
||||||
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
|
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
|
||||||
|
|
||||||
|
@Suppress("ktlint:standard:backing-property-naming")
|
||||||
private val _favFlow = MutableSharedFlow<Unit>()
|
private val _favFlow = MutableSharedFlow<Unit>()
|
||||||
val favChangesFlow = _favFlow.debounce(1000)
|
val favChangesFlow = _favFlow.debounce(1000)
|
||||||
var favCat by stringArrayPref("fav_cat", 10, "Favorites").emitTo(_favFlow)
|
var favCat by stringArrayPref("fav_cat", 10, "Favorites").emitTo(_favFlow)
|
||||||
@@ -140,6 +141,7 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var readCacheSize by intPref("read_cache_size_2", 640)
|
var readCacheSize by intPref("read_cache_size_2", 640)
|
||||||
var launchPage by intPref("launch_page_2", 0)
|
var launchPage by intPref("launch_page_2", 0)
|
||||||
var commentThreshold by intPref("comment_threshold", -101)
|
var commentThreshold by intPref("comment_threshold", -101)
|
||||||
|
var hardwareBitmapThreshold by intPref("hardware_bitmap_threshold", 16384)
|
||||||
var forceEhThumb by boolPref("force_eh_thumb", false)
|
var forceEhThumb by boolPref("force_eh_thumb", false)
|
||||||
var showComments by boolPref("show_gallery_comments", true)
|
var showComments by boolPref("show_gallery_comments", true)
|
||||||
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
|
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
|
||||||
@@ -225,7 +227,7 @@ object Settings : DataStorePreferences(null) {
|
|||||||
private fun intArrayPref(key: String, count: Int) = object : Delegate<IntArray> {
|
private fun intArrayPref(key: String, count: Int) = object : Delegate<IntArray> {
|
||||||
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
|
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
|
||||||
|
|
||||||
@Suppress("ktlint:standard:property-naming")
|
@Suppress("ktlint:standard:backing-property-naming")
|
||||||
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
|
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
|
||||||
override fun getValue(thisRef: Any?, prop: KProperty<*>?): IntArray = _value.map { it.value }.toIntArray()
|
override fun getValue(thisRef: Any?, prop: KProperty<*>?): IntArray = _value.map { it.value }.toIntArray()
|
||||||
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: IntArray) {
|
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: IntArray) {
|
||||||
@@ -237,7 +239,7 @@ object Settings : DataStorePreferences(null) {
|
|||||||
private fun stringArrayPref(key: String, count: Int, defMetaValue: String) = object : Delegate<Array<String>> {
|
private fun stringArrayPref(key: String, count: Int, defMetaValue: String) = object : Delegate<Array<String>> {
|
||||||
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
|
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
|
||||||
|
|
||||||
@Suppress("ktlint:standard:property-naming")
|
@Suppress("ktlint:standard:backing-property-naming")
|
||||||
private var _value = (0 until count).map { stringPref("${key}_$it", "$defMetaValue $it") }.toTypedArray()
|
private var _value = (0 until count).map { stringPref("${key}_$it", "$defMetaValue $it") }.toTypedArray()
|
||||||
override fun getValue(thisRef: Any?, prop: KProperty<*>?): Array<String> = _value.map { it.value }.toTypedArray()
|
override fun getValue(thisRef: Any?, prop: KProperty<*>?): Array<String> = _value.map { it.value }.toTypedArray()
|
||||||
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Array<String>) {
|
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Array<String>) {
|
||||||
|
|||||||
@@ -29,13 +29,9 @@ private const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
|
|||||||
private const val URL_PREFIX_THUMB_EX = "https://s.exhentai.org/"
|
private const val URL_PREFIX_THUMB_EX = "https://s.exhentai.org/"
|
||||||
private const val URL_PREFIX_LARGE_THUMB_EX = URL_PREFIX_THUMB_EX + "t/"
|
private const val URL_PREFIX_LARGE_THUMB_EX = URL_PREFIX_THUMB_EX + "t/"
|
||||||
|
|
||||||
fun getImageKey(gid: Long, index: Int): String {
|
fun getImageKey(gid: Long, index: Int): String = "image:$gid:$index"
|
||||||
return "image:$gid:$index"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getThumbKey(url: String): String {
|
fun getThumbKey(url: String): String = url.removePrefix(URL_PREFIX_THUMB_E).removePrefix(URL_PREFIX_LARGE_THUMB_EX).removePrefix(URL_PREFIX_THUMB_EX)
|
||||||
return url.removePrefix(URL_PREFIX_THUMB_E).removePrefix(URL_PREFIX_LARGE_THUMB_EX).removePrefix(URL_PREFIX_THUMB_EX)
|
|
||||||
}
|
|
||||||
|
|
||||||
val GalleryPreview.url
|
val GalleryPreview.url
|
||||||
get() = if (exThumb) {
|
get() = if (exThumb) {
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
package com.hippo.ehviewer.client
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.compose.ui.util.fastAny
|
|
||||||
import androidx.compose.ui.util.fastJoinToString
|
|
||||||
import io.ktor.client.plugins.cookies.CookiesStorage
|
import io.ktor.client.plugins.cookies.CookiesStorage
|
||||||
import io.ktor.http.Cookie
|
import io.ktor.http.Cookie
|
||||||
import io.ktor.http.Url
|
import io.ktor.http.Url
|
||||||
import io.ktor.http.parseClientCookiesHeader
|
import io.ktor.http.parseClientCookiesHeader
|
||||||
import io.ktor.http.renderCookieHeader
|
|
||||||
import io.ktor.http.renderSetCookieHeader
|
import io.ktor.http.renderSetCookieHeader
|
||||||
|
|
||||||
object EhCookieStore : CookiesStorage {
|
object EhCookieStore : CookiesStorage {
|
||||||
private val manager = CookieManager.getInstance()
|
private val manager = CookieManager.getInstance()
|
||||||
fun removeAllCookies() = manager.removeAllCookies(null)
|
fun removeAllCookies() = manager.removeAllCookies(null)
|
||||||
fun contains(url: String, name: String) = load(Url(url)).fastAny { it.name == name }
|
|
||||||
|
|
||||||
fun hasSignedIn(): Boolean {
|
fun hasSignedIn(): Boolean = getCookies(EhUrl.HOST_E)?.run {
|
||||||
val url = EhUrl.HOST_E
|
containsKey(KEY_IPB_MEMBER_ID) && containsKey(KEY_IPB_PASS_HASH)
|
||||||
return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH)
|
} ?: false
|
||||||
}
|
|
||||||
|
|
||||||
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
||||||
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
||||||
@@ -53,11 +48,7 @@ object EhCookieStore : CookiesStorage {
|
|||||||
|
|
||||||
fun flush() = manager.flush()
|
fun flush() = manager.flush()
|
||||||
|
|
||||||
fun getCookieHeader(url: String): String {
|
fun getCookieHeader(url: String): String? = manager.getCookie(url)
|
||||||
return load(Url(url)).fastJoinToString("; ") {
|
|
||||||
renderCookieHeader(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
||||||
override suspend fun addCookie(requestUrl: Url, cookie: Cookie) {
|
override suspend fun addCookie(requestUrl: Url, cookie: Cookie) {
|
||||||
|
|||||||
@@ -297,6 +297,10 @@ object EhEngine {
|
|||||||
}.executeSafely { }
|
}.executeSafely { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getFavoriteNote(gid: Long, token: String) =
|
||||||
|
ehRequest(EhUrl.getAddFavorites(gid, token), EhUrl.getGalleryDetailUrl(gid, token))
|
||||||
|
.fetchUsingAsText(FavoritesParser::parseNote)
|
||||||
|
|
||||||
suspend fun downloadArchive(gid: Long, token: String, or: String, res: String, isHAtH: Boolean): String? {
|
suspend fun downloadArchive(gid: Long, token: String, or: String, res: String, isHAtH: Boolean): String? {
|
||||||
val url = EhUrl.getDownloadArchive(gid, token, or)
|
val url = EhUrl.getDownloadArchive(gid, token, or)
|
||||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ import com.hippo.ehviewer.util.AppConfig
|
|||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.StatusCodeException
|
import com.hippo.ehviewer.util.StatusCodeException
|
||||||
import com.hippo.ehviewer.util.copyTo
|
import com.hippo.ehviewer.util.copyTo
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.ehviewer.util.sha1
|
||||||
import com.hippo.unifile.sha1
|
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.prepareGet
|
import io.ktor.client.request.prepareGet
|
||||||
@@ -45,6 +44,7 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
@@ -63,9 +63,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
|
|
||||||
var initialized by mutableStateOf(false)
|
var initialized by mutableStateOf(false)
|
||||||
|
|
||||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
|
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? = tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
|
||||||
return tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun internalSuggestFlow(
|
private fun internalSuggestFlow(
|
||||||
tags: Map<String, String>,
|
tags: Map<String, String>,
|
||||||
@@ -131,24 +129,16 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
||||||
removeSpace().contains(other.removeSpace(), ignoreCase)
|
removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||||
|
|
||||||
private fun getMetadata(context: Context): Array<String>? {
|
private fun getMetadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||||
return context.resources.getStringArray(R.array.tag_translation_metadata)
|
.takeIf { it.size == 4 }
|
||||||
.takeIf { it.size == 4 }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTranslatable(context: Context): Boolean {
|
fun isTranslatable(context: Context): Boolean = context.resources.getBoolean(R.bool.tag_translatable)
|
||||||
return context.resources.getBoolean(R.bool.tag_translatable)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFileContent(file: File): String? {
|
private fun getFileContent(file: File): String? = runCatching {
|
||||||
return runCatching {
|
file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }
|
||||||
file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }
|
}.getOrNull()
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkData(sha1: String?, data: File): Boolean {
|
private fun checkData(sha1: String?, data: File): Boolean = sha1 != null && sha1 == data.toOkioPath().sha1()
|
||||||
return sha1 != null && sha1 == data.asUniFile().sha1()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun save(client: HttpClient, url: String, file: File) {
|
private suspend fun save(client: HttpClient, url: String, file: File) {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|||||||
@@ -88,21 +88,18 @@ object EhUtils {
|
|||||||
|
|
||||||
fun getCategory(type: String?): Int {
|
fun getCategory(type: String?): Int {
|
||||||
for (entry in CATEGORY_STRINGS) {
|
for (entry in CATEGORY_STRINGS) {
|
||||||
for (str in entry.first)
|
for (str in entry.first) {
|
||||||
if (str.equals(type, ignoreCase = true)) {
|
if (str.equals(type, ignoreCase = true)) {
|
||||||
return entry.second
|
return entry.second
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return UNKNOWN
|
return UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCategory(type: Int): String {
|
fun getCategory(type: Int): String = CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
||||||
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun differenceDegrees(a: Float, b: Float): Float {
|
private fun differenceDegrees(a: Float, b: Float): Float = 180.0f - abs(abs(a - b) - 180.0f)
|
||||||
return 180.0f - abs(abs(a - b) - 180.0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sanitizeDegreesDouble(degrees: Float): Float {
|
private fun sanitizeDegreesDouble(degrees: Float): Float {
|
||||||
val deg = degrees % 360.0f
|
val deg = degrees % 360.0f
|
||||||
@@ -159,13 +156,11 @@ object EhUtils {
|
|||||||
Settings.needSignIn = true
|
Settings.needSignIn = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSuitableTitle(gi: GalleryInfo): String {
|
fun getSuitableTitle(gi: GalleryInfo): String = if (Settings.showJpnTitle) {
|
||||||
return if (Settings.showJpnTitle) {
|
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
|
||||||
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
|
} else {
|
||||||
} else {
|
if (gi.title.isNullOrEmpty()) gi.titleJpn else gi.title
|
||||||
if (gi.title.isNullOrEmpty()) gi.titleJpn else gi.title
|
}.orEmpty()
|
||||||
}.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extractTitle(fullTitle: String?): String? {
|
fun extractTitle(fullTitle: String?): String? {
|
||||||
var title: String = fullTitle ?: return null
|
var title: String = fullTitle ?: return null
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ data class BaseGalleryInfo(
|
|||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
override var favoriteNote: String? = null,
|
override var favoriteNote: String? = null,
|
||||||
) : GalleryInfo, Parcelable {
|
) : GalleryInfo,
|
||||||
|
Parcelable {
|
||||||
constructor() : this(0, "")
|
constructor() : this(0, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ data class GalleryDetail(
|
|||||||
var comments: GalleryCommentList,
|
var comments: GalleryCommentList,
|
||||||
val previewPages: Int,
|
val previewPages: Int,
|
||||||
val previewList: List<GalleryPreview>,
|
val previewList: List<GalleryPreview>,
|
||||||
) : GalleryInfo by galleryInfo, Parcelable {
|
) : GalleryInfo by galleryInfo,
|
||||||
|
Parcelable {
|
||||||
fun fillInfo() {
|
fun fillInfo() {
|
||||||
val index = LANGUAGES.indexOf(language)
|
val index = LANGUAGES.indexOf(language)
|
||||||
if (index != -1) simpleLanguage = S_LANGS[index]
|
if (index != -1) simpleLanguage = S_LANGS[index]
|
||||||
|
|||||||
@@ -134,14 +134,10 @@ interface GalleryInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun GalleryInfo.findBaseInfo(): BaseGalleryInfo {
|
fun GalleryInfo.findBaseInfo(): BaseGalleryInfo = when (this) {
|
||||||
return when (this) {
|
is BaseGalleryInfo -> this
|
||||||
is BaseGalleryInfo -> this
|
is GalleryDetail -> galleryInfo
|
||||||
is GalleryDetail -> galleryInfo
|
else -> unreachable()
|
||||||
else -> unreachable()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun GalleryInfo.asGalleryDetail(): GalleryDetail? {
|
fun GalleryInfo.asGalleryDetail(): GalleryDetail? = this as? GalleryDetail
|
||||||
return this as? GalleryDetail
|
|
||||||
}
|
|
||||||
@@ -22,4 +22,5 @@ import kotlinx.parcelize.Parcelize
|
|||||||
data class GalleryTagGroup(
|
data class GalleryTagGroup(
|
||||||
val groupName: String,
|
val groupName: String,
|
||||||
private val tagList: MutableList<String> = mutableListOf(),
|
private val tagList: MutableList<String> = mutableListOf(),
|
||||||
) : Parcelable, MutableList<String> by tagList
|
) : Parcelable,
|
||||||
|
MutableList<String> by tagList
|
||||||
@@ -42,6 +42,7 @@ data class ListUrlBuilder(
|
|||||||
var category: Int = EhUtils.NONE,
|
var category: Int = EhUtils.NONE,
|
||||||
private var mKeyword: String? = null,
|
private var mKeyword: String? = null,
|
||||||
var hash: String? = null,
|
var hash: String? = null,
|
||||||
|
var language: Int = -1,
|
||||||
var advanceSearch: Int = -1,
|
var advanceSearch: Int = -1,
|
||||||
var minRating: Int = -1,
|
var minRating: Int = -1,
|
||||||
var pageFrom: Int = -1,
|
var pageFrom: Int = -1,
|
||||||
@@ -83,18 +84,16 @@ data class ListUrlBuilder(
|
|||||||
mNext = q.name.substringAfterLast('@', "").ifEmpty { null }
|
mNext = q.name.substringAfterLast('@', "").ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toQuickSearch(name: String): QuickSearch {
|
fun toQuickSearch(name: String): QuickSearch = QuickSearch(
|
||||||
return QuickSearch(
|
name = name,
|
||||||
name = name,
|
mode = mode,
|
||||||
mode = mode,
|
category = category,
|
||||||
category = category,
|
keyword = mKeyword,
|
||||||
keyword = mKeyword,
|
advanceSearch = advanceSearch,
|
||||||
advanceSearch = advanceSearch,
|
minRating = minRating,
|
||||||
minRating = minRating,
|
pageFrom = pageFrom,
|
||||||
pageFrom = pageFrom,
|
pageTo = pageTo,
|
||||||
pageTo = pageTo,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun equalsQuickSearch(q: QuickSearch?): Boolean {
|
fun equalsQuickSearch(q: QuickSearch?): Boolean {
|
||||||
if (null == q) {
|
if (null == q) {
|
||||||
@@ -259,85 +258,89 @@ data class ListUrlBuilder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(): String {
|
fun build(): String = when (mode) {
|
||||||
return when (mode) {
|
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl(EhUrl.WATCHED_PATH.takeIf { mode == MODE_SUBSCRIPTION }) {
|
||||||
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl(EhUrl.WATCHED_PATH.takeIf { mode == MODE_SUBSCRIPTION }) {
|
val category = if (!Settings.hasSignedIn.value && category <= 0) EhUtils.NON_H else category
|
||||||
val category = if (!Settings.hasSignedIn.value && category <= 0) EhUtils.NON_H else category
|
if (category > 0) {
|
||||||
if (category > 0) {
|
addQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
|
||||||
addQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
|
}
|
||||||
|
val query = mKeyword?.let { keyword ->
|
||||||
|
if (language == -1 || "gid:" in keyword || "l:" in keyword || "language:" in keyword) {
|
||||||
|
keyword
|
||||||
|
} else {
|
||||||
|
val tag = GalleryInfo.S_LANG_TAGS[language]
|
||||||
|
if (keyword.isNotEmpty()) {
|
||||||
|
"$tag $keyword"
|
||||||
|
} else {
|
||||||
|
tag
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val query = mKeyword?.let {
|
}
|
||||||
val index = Settings.languageFilter.value
|
addQueryParameterIfNotBlank("f_search", query)
|
||||||
GalleryInfo.S_LANG_TAGS.getOrNull(index)?.let { lang ->
|
addQueryParameterIfNotBlank("prev", mPrev)
|
||||||
"$it $lang"
|
addQueryParameterIfNotBlank("next", mNext)
|
||||||
} ?: it
|
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||||
|
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||||
|
// Advance search
|
||||||
|
if (advanceSearch > 0 || minRating > 0 || pageFrom > 0 || pageTo > 0) {
|
||||||
|
addQueryParameter("advsearch", "1")
|
||||||
|
if (advanceSearch and AdvanceTable.SH != 0) {
|
||||||
|
addQueryParameter("f_sh", "on")
|
||||||
}
|
}
|
||||||
addQueryParameterIfNotBlank("f_search", query)
|
if (advanceSearch and AdvanceTable.STO != 0) {
|
||||||
|
addQueryParameter("f_sto", "on")
|
||||||
|
}
|
||||||
|
if (advanceSearch and AdvanceTable.SFL != 0) {
|
||||||
|
addQueryParameter("f_sfl", "on")
|
||||||
|
}
|
||||||
|
if (advanceSearch and AdvanceTable.SFU != 0) {
|
||||||
|
addQueryParameter("f_sfu", "on")
|
||||||
|
}
|
||||||
|
if (advanceSearch and AdvanceTable.SFT != 0) {
|
||||||
|
addQueryParameter("f_sft", "on")
|
||||||
|
}
|
||||||
|
// Set min star
|
||||||
|
if (minRating > 0) {
|
||||||
|
addQueryParameter("f_sr", "on")
|
||||||
|
addQueryParameter("f_srdd", "$minRating")
|
||||||
|
}
|
||||||
|
// Pages
|
||||||
|
if (pageFrom > 0 || pageTo > 0) {
|
||||||
|
addQueryParameter("f_sp", "on")
|
||||||
|
addQueryParameterIfNotBlank(
|
||||||
|
"f_spf",
|
||||||
|
pageFrom.takeIf { it > 0 }?.toString(),
|
||||||
|
)
|
||||||
|
addQueryParameterIfNotBlank(
|
||||||
|
"f_spt",
|
||||||
|
pageTo.takeIf { it > 0 }?.toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.buildString()
|
||||||
|
|
||||||
|
MODE_UPLOADER, MODE_TAG -> {
|
||||||
|
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
||||||
|
ehUrl(listOf(path, requireNotNull(mKeyword))) {
|
||||||
addQueryParameterIfNotBlank("prev", mPrev)
|
addQueryParameterIfNotBlank("prev", mPrev)
|
||||||
addQueryParameterIfNotBlank("next", mNext)
|
addQueryParameterIfNotBlank("next", mNext)
|
||||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||||
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||||
// Advance search
|
|
||||||
if (advanceSearch > 0 || minRating > 0 || pageFrom > 0 || pageTo > 0) {
|
|
||||||
addQueryParameter("advsearch", "1")
|
|
||||||
if (advanceSearch and AdvanceTable.SH != 0) {
|
|
||||||
addQueryParameter("f_sh", "on")
|
|
||||||
}
|
|
||||||
if (advanceSearch and AdvanceTable.STO != 0) {
|
|
||||||
addQueryParameter("f_sto", "on")
|
|
||||||
}
|
|
||||||
if (advanceSearch and AdvanceTable.SFL != 0) {
|
|
||||||
addQueryParameter("f_sfl", "on")
|
|
||||||
}
|
|
||||||
if (advanceSearch and AdvanceTable.SFU != 0) {
|
|
||||||
addQueryParameter("f_sfu", "on")
|
|
||||||
}
|
|
||||||
if (advanceSearch and AdvanceTable.SFT != 0) {
|
|
||||||
addQueryParameter("f_sft", "on")
|
|
||||||
}
|
|
||||||
// Set min star
|
|
||||||
if (minRating > 0) {
|
|
||||||
addQueryParameter("f_sr", "on")
|
|
||||||
addQueryParameter("f_srdd", "$minRating")
|
|
||||||
}
|
|
||||||
// Pages
|
|
||||||
if (pageFrom > 0 || pageTo > 0) {
|
|
||||||
addQueryParameter("f_sp", "on")
|
|
||||||
addQueryParameterIfNotBlank(
|
|
||||||
"f_spf",
|
|
||||||
pageFrom.takeIf { it > 0 }?.toString(),
|
|
||||||
)
|
|
||||||
addQueryParameterIfNotBlank(
|
|
||||||
"f_spt",
|
|
||||||
pageTo.takeIf { it > 0 }?.toString(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.buildString()
|
}.buildString()
|
||||||
|
|
||||||
MODE_UPLOADER, MODE_TAG -> {
|
|
||||||
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
|
||||||
ehUrl(listOf(path, requireNotNull(mKeyword))) {
|
|
||||||
addQueryParameterIfNotBlank("prev", mPrev)
|
|
||||||
addQueryParameterIfNotBlank("next", mNext)
|
|
||||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
|
||||||
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
|
||||||
}.buildString()
|
|
||||||
}
|
|
||||||
|
|
||||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
|
||||||
MODE_IMAGE_SEARCH -> ehUrl {
|
|
||||||
addQueryParameter("f_shash", requireNotNull(hash))
|
|
||||||
}.buildString()
|
|
||||||
MODE_TOPLIST -> {
|
|
||||||
ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
|
||||||
addQueryParameter("tl", requireNotNull(mKeyword))
|
|
||||||
addQueryParameterIfNotBlank("p", mJumpTo)
|
|
||||||
}.buildString()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||||
|
MODE_IMAGE_SEARCH -> ehUrl {
|
||||||
|
addQueryParameter("f_shash", requireNotNull(hash))
|
||||||
|
}.buildString()
|
||||||
|
MODE_TOPLIST -> {
|
||||||
|
ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
||||||
|
addQueryParameter("tl", requireNotNull(mKeyword))
|
||||||
|
addQueryParameterIfNotBlank("p", mJumpTo)
|
||||||
|
}.buildString()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@IntDef(
|
@IntDef(
|
||||||
|
|||||||
@@ -20,7 +20,5 @@ package com.hippo.ehviewer.client.parser
|
|||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
object EventPaneParser {
|
object EventPaneParser {
|
||||||
fun parse(body: String): String? {
|
fun parse(body: String): String? = Jsoup.parse(body).getElementById("eventpane")?.html()
|
||||||
return Jsoup.parse(body).getElementById("eventpane")?.html()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
|
import com.hippo.ehviewer.util.unescapeXml
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
@@ -17,13 +18,16 @@ data class FavParserResult(
|
|||||||
}
|
}
|
||||||
|
|
||||||
object FavoritesParser {
|
object FavoritesParser {
|
||||||
fun parse(body: ByteBuffer): FavParserResult {
|
private val NoteRegex = Regex("<textarea[^>]*>([^<]*)</textarea>")
|
||||||
return runSuspendCatching {
|
|
||||||
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
|
fun parse(body: ByteBuffer): FavParserResult = runSuspendCatching {
|
||||||
}.getOrElse {
|
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
|
||||||
throw ParseException("Parse favorites error", it)
|
}.getOrElse {
|
||||||
}
|
throw ParseException("Parse favorites error", it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parseNote(body: String) = NoteRegex.find(body)?.run { groupValues[1].unescapeXml() }
|
||||||
|
?: throw ParseException("Can't parse favorite note")
|
||||||
}
|
}
|
||||||
|
|
||||||
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit()): Int
|
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit()): Int
|
||||||
@@ -21,15 +21,13 @@ import com.hippo.ehviewer.util.ExceptionUtils
|
|||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
object ForumsParser {
|
object ForumsParser {
|
||||||
fun parse(body: String): String {
|
fun parse(body: String): String = runCatching {
|
||||||
return runCatching {
|
val d = Jsoup.parse(body, EhUrl.URL_FORUMS)
|
||||||
val d = Jsoup.parse(body, EhUrl.URL_FORUMS)
|
val userlinks = d.getElementById("userlinks")
|
||||||
val userlinks = d.getElementById("userlinks")
|
val child = userlinks!!.child(0).child(0).child(0)
|
||||||
val child = userlinks!!.child(0).child(0).child(0)
|
child.attr("href")
|
||||||
child.attr("href")
|
}.getOrElse {
|
||||||
}.getOrElse {
|
ExceptionUtils.throwIfFatal(it)
|
||||||
ExceptionUtils.throwIfFatal(it)
|
throw NotLoggedInException()
|
||||||
throw NotLoggedInException()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,56 +283,50 @@ object GalleryDetailParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseTagGroup(element: Element): GalleryTagGroup? {
|
private fun parseTagGroup(element: Element): GalleryTagGroup? = try {
|
||||||
return try {
|
var nameSpace = element.child(0).text()
|
||||||
var nameSpace = element.child(0).text()
|
// Remove last ':'
|
||||||
// Remove last ':'
|
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
|
||||||
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
|
val group = GalleryTagGroup(nameSpace)
|
||||||
val group = GalleryTagGroup(nameSpace)
|
val tags = element.child(1).children()
|
||||||
val tags = element.child(1).children()
|
tags.forEach {
|
||||||
tags.forEach {
|
var tag = it.text()
|
||||||
var tag = it.text()
|
// Sometimes parody tag is followed with '|' and english translate, just remove them
|
||||||
// Sometimes parody tag is followed with '|' and english translate, just remove them
|
val index = tag.indexOf('|')
|
||||||
val index = tag.indexOf('|')
|
if (index >= 0) {
|
||||||
if (index >= 0) {
|
tag = tag.substring(0, index).trim()
|
||||||
tag = tag.substring(0, index).trim()
|
|
||||||
}
|
|
||||||
if (it.className() == "gtw") {
|
|
||||||
tag = "_$tag" // weak tag
|
|
||||||
}
|
|
||||||
group.add(tag)
|
|
||||||
}
|
}
|
||||||
if (group.size > 0) group else null
|
if (it.className() == "gtw") {
|
||||||
} catch (e: Throwable) {
|
tag = "_$tag" // weak tag
|
||||||
ExceptionUtils.throwIfFatal(e)
|
}
|
||||||
logcat(e)
|
group.add(tag)
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
if (group.size > 0) group else null
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
ExceptionUtils.throwIfFatal(e)
|
||||||
|
logcat(e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse tag groups with html parser
|
* Parse tag groups with html parser
|
||||||
*/
|
*/
|
||||||
private fun parseTagGroups(document: Document): List<GalleryTagGroup> {
|
private fun parseTagGroups(document: Document): List<GalleryTagGroup> = try {
|
||||||
return try {
|
val taglist = document.getElementById("taglist")!!
|
||||||
val taglist = document.getElementById("taglist")!!
|
val tagGroups = taglist.child(0).child(0).children()
|
||||||
val tagGroups = taglist.child(0).child(0).children()
|
parseTagGroups(tagGroups)
|
||||||
parseTagGroups(tagGroups)
|
} catch (e: Throwable) {
|
||||||
} catch (e: Throwable) {
|
ExceptionUtils.throwIfFatal(e)
|
||||||
ExceptionUtils.throwIfFatal(e)
|
logcat(e)
|
||||||
logcat(e)
|
emptyList()
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseTagGroups(trs: Elements): List<GalleryTagGroup> {
|
private fun parseTagGroups(trs: Elements): List<GalleryTagGroup> = try {
|
||||||
return try {
|
trs.mapNotNull { parseTagGroup(it) }
|
||||||
trs.mapNotNull { parseTagGroup(it) }
|
} catch (e: Throwable) {
|
||||||
} catch (e: Throwable) {
|
ExceptionUtils.throwIfFatal(e)
|
||||||
ExceptionUtils.throwIfFatal(e)
|
logcat(e)
|
||||||
logcat(e)
|
emptyList()
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun parseComment(element: Element): GalleryComment? {
|
private suspend fun parseComment(element: Element): GalleryComment? {
|
||||||
@@ -449,22 +443,16 @@ object GalleryDetailParser {
|
|||||||
/**
|
/**
|
||||||
* Parse preview pages with regular expressions
|
* Parse preview pages with regular expressions
|
||||||
*/
|
*/
|
||||||
fun parsePreviewPages(body: String): Int {
|
fun parsePreviewPages(body: String): Int = PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||||
return PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
?: throw ParseException("Parse preview page count error")
|
||||||
?: throw ParseException("Parse preview page count error")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse pages with regular expressions
|
* Parse pages with regular expressions
|
||||||
*/
|
*/
|
||||||
fun parsePages(body: String): Int {
|
fun parsePages(body: String): Int = PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||||
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
?: throw ParseException("Parse pages error")
|
||||||
?: throw ParseException("Parse pages error")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> {
|
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> = runCatching { parseNormalPreview(body) }.getOrElse { parseLargePreview(body) }
|
||||||
return runCatching { parseNormalPreview(body) }.getOrElse { parseLargePreview(body) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseLargePreview(body: String): Pair<List<GalleryPreview>, List<String>> {
|
private fun parseLargePreview(body: String): Pair<List<GalleryPreview>, List<String>> {
|
||||||
check(PATTERN_LARGE_PREVIEW.containsMatchIn(body))
|
check(PATTERN_LARGE_PREVIEW.containsMatchIn(body))
|
||||||
|
|||||||
@@ -22,14 +22,12 @@ import eu.kanade.tachiyomi.util.system.logcat
|
|||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
object GalleryNotAvailableParser {
|
object GalleryNotAvailableParser {
|
||||||
fun parse(body: String): String? {
|
fun parse(body: String): String? = runCatching {
|
||||||
return runCatching {
|
val e = Jsoup.parse(body).getElementsByClass("d").first()
|
||||||
val e = Jsoup.parse(body).getElementsByClass("d").first()
|
e!!.child(0).html().replace("<br>", "\n")
|
||||||
e!!.child(0).html().replace("<br>", "\n")
|
}.getOrElse {
|
||||||
}.getOrElse {
|
ExceptionUtils.throwIfFatal(it)
|
||||||
ExceptionUtils.throwIfFatal(it)
|
logcat(it)
|
||||||
logcat(it)
|
null
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,13 +32,11 @@ object GalleryTokenApiParser {
|
|||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
fun parse(body: String): String {
|
fun parse(body: String): String = runCatching {
|
||||||
return runCatching {
|
body.parseAs<Result>().tokenList[0].token
|
||||||
body.parseAs<Result>().tokenList[0].token
|
}.getOrElse {
|
||||||
}.getOrElse {
|
ExceptionUtils.throwIfFatal(it)
|
||||||
ExceptionUtils.throwIfFatal(it)
|
throw EhException(body.parseAs<Error>().error)
|
||||||
throw EhException(body.parseAs<Error>().error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -37,23 +37,13 @@ object ParserUtils {
|
|||||||
minute()
|
minute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatDate(time: Long): String {
|
fun formatDate(time: Long): String = formatter.format(time.toLocalDateTime())
|
||||||
return formatter.format(time.toLocalDateTime())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseDate(str: String): Long {
|
fun parseDate(str: String): Long = formatter.parse(str).toEpochMillis()
|
||||||
return formatter.parse(str).toEpochMillis()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun trim(str: String?): String {
|
fun trim(str: String?): String = str?.unescapeXml()?.trim() ?: ""
|
||||||
return str?.unescapeXml()?.trim() ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseInt(str: String?, defValue: Int): Int {
|
fun parseInt(str: String?, defValue: Int): Int = trim(str).replace(",", "").toIntOrDefault(defValue)
|
||||||
return trim(str).replace(",", "").toIntOrDefault(defValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseLong(str: String?, defValue: Long): Long {
|
fun parseLong(str: String?, defValue: Long): Long = trim(str).replace(",", "").toLongOrDefault(defValue)
|
||||||
return trim(str).replace(",", "").toLongOrDefault(defValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -22,31 +22,29 @@ import eu.kanade.tachiyomi.util.system.logcat
|
|||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
object ProfileParser {
|
object ProfileParser {
|
||||||
fun parse(body: String): Result {
|
fun parse(body: String): Result = runCatching {
|
||||||
return runCatching {
|
val d = Jsoup.parse(body)
|
||||||
val d = Jsoup.parse(body)
|
val profilename = d.getElementById("profilename")
|
||||||
val profilename = d.getElementById("profilename")
|
val displayName = profilename!!.child(0).text()
|
||||||
val displayName = profilename!!.child(0).text()
|
val avatar = runCatching {
|
||||||
val avatar = runCatching {
|
val avatar =
|
||||||
val avatar =
|
profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr("src")
|
||||||
profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr("src")
|
if (avatar.isEmpty()) {
|
||||||
if (avatar.isEmpty()) {
|
|
||||||
null
|
|
||||||
} else if (!avatar.startsWith("http")) {
|
|
||||||
EhUrl.URL_FORUMS + avatar
|
|
||||||
} else {
|
|
||||||
avatar
|
|
||||||
}
|
|
||||||
}.getOrElse {
|
|
||||||
ExceptionUtils.throwIfFatal(it)
|
|
||||||
logcat { "No avatar" }
|
|
||||||
null
|
null
|
||||||
|
} else if (!avatar.startsWith("http")) {
|
||||||
|
EhUrl.URL_FORUMS + avatar
|
||||||
|
} else {
|
||||||
|
avatar
|
||||||
}
|
}
|
||||||
Result(displayName, avatar)
|
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
ExceptionUtils.throwIfFatal(it)
|
ExceptionUtils.throwIfFatal(it)
|
||||||
throw ParseException("Parse forums error")
|
logcat { "No avatar" }
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
Result(displayName, avatar)
|
||||||
|
}.getOrElse {
|
||||||
|
ExceptionUtils.throwIfFatal(it)
|
||||||
|
throw ParseException("Parse forums error")
|
||||||
}
|
}
|
||||||
|
|
||||||
class Result(val displayName: String?, val avatar: String?)
|
class Result(val displayName: String?, val avatar: String?)
|
||||||
|
|||||||
@@ -25,11 +25,9 @@ object SignInParser {
|
|||||||
"|<span class=\"postcolor\">(.+?)</span>",
|
"|<span class=\"postcolor\">(.+?)</span>",
|
||||||
)
|
)
|
||||||
|
|
||||||
fun parse(body: String): String {
|
fun parse(body: String): String = NAME_PATTERN.find(body)?.let {
|
||||||
return NAME_PATTERN.find(body)?.let {
|
it.groupValues[1]
|
||||||
it.groupValues[1]
|
} ?: ERROR_PATTERN.find(body)?.let {
|
||||||
} ?: ERROR_PATTERN.find(body)?.let {
|
throw EhException(it.groupValues[1].ifEmpty { it.groupValues[2] })
|
||||||
throw EhException(it.groupValues[1].ifEmpty { it.groupValues[2] })
|
} ?: throw ParseException("Can't parse sign in")
|
||||||
} ?: throw ParseException("Can't parse sign in")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,5 @@ import com.hippo.ehviewer.client.parseAs
|
|||||||
|
|
||||||
object VoteTagParser {
|
object VoteTagParser {
|
||||||
// {"error":"The tag \"neko\" is not allowed. Use character:neko or artist:neko"}
|
// {"error":"The tag \"neko\" is not allowed. Use character:neko or artist:neko"}
|
||||||
fun parse(body: String): String? {
|
fun parse(body: String): String? = runCatching { body.parseAs<Error>().error }.getOrNull()
|
||||||
return runCatching { body.parseAs<Error>().error }.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
package com.hippo.ehviewer.coil
|
package com.hippo.ehviewer.coil
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import androidx.compose.ui.unit.IntRect
|
import androidx.compose.ui.unit.IntRect
|
||||||
import coil3.BitmapImage
|
import coil3.BitmapImage
|
||||||
import coil3.Extras
|
import coil3.Extras
|
||||||
import coil3.Image
|
import coil3.Image
|
||||||
import coil3.asCoilImage
|
|
||||||
import coil3.getExtra
|
import coil3.getExtra
|
||||||
import coil3.intercept.Interceptor
|
import coil3.intercept.Interceptor
|
||||||
import coil3.intercept.Interceptor.Chain
|
import coil3.intercept.Interceptor.Chain
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.ImageResult
|
import coil3.request.ImageResult
|
||||||
import coil3.request.SuccessResult
|
import coil3.request.SuccessResult
|
||||||
import coil3.request.allowHardware
|
|
||||||
import com.hippo.ehviewer.image.detectBorder
|
import com.hippo.ehviewer.image.detectBorder
|
||||||
import com.hippo.ehviewer.util.isAtLeastO
|
|
||||||
|
|
||||||
private const val CROP_THRESHOLD = 0.75f
|
private const val CROP_THRESHOLD = 0.75f
|
||||||
private const val RATIO_THRESHOLD = 2
|
private const val RATIO_THRESHOLD = 2
|
||||||
@@ -30,59 +26,33 @@ val ImageRequest.maybeCropBorder: Boolean
|
|||||||
|
|
||||||
class BitmapImageWithRect(
|
class BitmapImageWithRect(
|
||||||
val rect: IntRect,
|
val rect: IntRect,
|
||||||
val image: Image,
|
val image: BitmapImage,
|
||||||
) : Image by image
|
) : Image by image
|
||||||
|
|
||||||
object CropBorderInterceptor : Interceptor {
|
object CropBorderInterceptor : Interceptor {
|
||||||
override suspend fun intercept(chain: Chain): ImageResult {
|
override suspend fun intercept(chain: Chain): ImageResult {
|
||||||
val req = chain.request
|
val result = chain.proceed()
|
||||||
if (req.maybeCropBorder) {
|
if (chain.request.maybeCropBorder && result is SuccessResult) {
|
||||||
val hw = req.allowHardware && isAtLeastO
|
val image = result.image
|
||||||
val result = if (hw) {
|
if (image is BitmapImage) {
|
||||||
val nonHw = req.newBuilder().allowHardware(false).build()
|
val bitmap = image.bitmap
|
||||||
chain.withRequest(nonHw).proceed()
|
val ratio = if (image.height > image.width) {
|
||||||
} else {
|
image.height / image.width
|
||||||
chain.proceed()
|
} else {
|
||||||
}
|
image.width / image.height
|
||||||
if (result is SuccessResult) {
|
}
|
||||||
val image = result.image
|
if (ratio < RATIO_THRESHOLD) {
|
||||||
if (image is BitmapImage) {
|
val array = detectBorder(bitmap)
|
||||||
val bitmap = image.bitmap
|
val minWidth = image.width * CROP_THRESHOLD
|
||||||
val ratio = if (image.height > image.width) {
|
val minHeight = image.height * CROP_THRESHOLD
|
||||||
image.height / image.width
|
val rect = IntRect(array[0], array[1], array[2], array[3])
|
||||||
} else {
|
if (rect.width > minWidth && rect.height > minHeight) {
|
||||||
image.width / image.height
|
val new = BitmapImageWithRect(rect, image)
|
||||||
|
return result.copy(image = new, request = result.request, dataSource = result.dataSource)
|
||||||
}
|
}
|
||||||
val rect = if (ratio < RATIO_THRESHOLD) {
|
|
||||||
val array = detectBorder(bitmap)
|
|
||||||
val minWidth = image.width * CROP_THRESHOLD
|
|
||||||
val minHeight = image.height * CROP_THRESHOLD
|
|
||||||
IntRect(array[0], array[1], array[2], array[3]).takeIf {
|
|
||||||
it.width > minWidth && it.height > minHeight
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val maybeHwImage = if (hw) {
|
|
||||||
// Upload to Graphical Buffer to accelerate render
|
|
||||||
// May fail if the image is too long
|
|
||||||
bitmap.copy(Bitmap.Config.HARDWARE, false)?.run {
|
|
||||||
bitmap.recycle()
|
|
||||||
asCoilImage()
|
|
||||||
} ?: image
|
|
||||||
} else {
|
|
||||||
image
|
|
||||||
}
|
|
||||||
val new = if (rect != null) {
|
|
||||||
BitmapImageWithRect(rect, maybeHwImage)
|
|
||||||
} else {
|
|
||||||
maybeHwImage
|
|
||||||
}
|
|
||||||
return result.copy(image = new, request = result.request, dataSource = result.dataSource)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
return chain.proceed()
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,28 +7,20 @@ import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
|||||||
import com.hippo.ehviewer.download.downloadLocation
|
import com.hippo.ehviewer.download.downloadLocation
|
||||||
import com.hippo.ehviewer.spider.DownloadInfoMagics.decodeMagicRequestOrUrl
|
import com.hippo.ehviewer.spider.DownloadInfoMagics.decodeMagicRequestOrUrl
|
||||||
import com.hippo.ehviewer.util.sendTo
|
import com.hippo.ehviewer.util.sendTo
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.files.isFile
|
||||||
|
import com.hippo.files.toUri
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
|
|
||||||
object DownloadThumbInterceptor : Interceptor {
|
object DownloadThumbInterceptor : Interceptor {
|
||||||
// TODO: Remove this after a few releases
|
|
||||||
private const val LEGACY_THUMB_FILE = ".thumb"
|
|
||||||
private const val THUMB_FILE = "thumb.jpg"
|
private const val THUMB_FILE = "thumb.jpg"
|
||||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
val magicOrUrl = chain.request.data as? String
|
val magicOrUrl = chain.request.data as? String
|
||||||
if (magicOrUrl != null) {
|
if (magicOrUrl != null) {
|
||||||
val (url, location) = decodeMagicRequestOrUrl(magicOrUrl)
|
val (url, location) = decodeMagicRequestOrUrl(magicOrUrl)
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
val thumb = withIOContext {
|
val thumb = withIOContext { downloadLocation / location / THUMB_FILE }
|
||||||
val legacyThumb = downloadLocation / location / LEGACY_THUMB_FILE
|
|
||||||
if (legacyThumb.isFile) {
|
|
||||||
legacyThumb.apply { renameTo(THUMB_FILE) }
|
|
||||||
} else {
|
|
||||||
downloadLocation / location / THUMB_FILE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (withIOContext { thumb.isFile }) {
|
if (withIOContext { thumb.isFile }) {
|
||||||
val new = chain.request.newBuilder().data(thumb.uri.toString()).build()
|
val new = chain.request.newBuilder().data(thumb.toUri()).build()
|
||||||
val result = chain.withRequest(new).proceed()
|
val result = chain.withRequest(new).proceed()
|
||||||
if (result is SuccessResult) return result
|
if (result is SuccessResult) return result
|
||||||
}
|
}
|
||||||
@@ -38,11 +30,9 @@ object DownloadThumbInterceptor : Interceptor {
|
|||||||
withIOContext {
|
withIOContext {
|
||||||
// Accessing the recreated file immediately after deleting it throws
|
// Accessing the recreated file immediately after deleting it throws
|
||||||
// FileNotFoundException, so we just overwrite the existing file.
|
// FileNotFoundException, so we just overwrite the existing file.
|
||||||
if (thumb.ensureFile()) {
|
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
imageCache.read(key) {
|
||||||
imageCache.read(key) {
|
data sendTo thumb
|
||||||
data.asUniFile() sendTo thumb
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.hippo.ehviewer.coil
|
|||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import coil3.asCoilImage
|
import coil3.asImage
|
||||||
import coil3.decode.DecodeResult
|
import coil3.decode.DecodeResult
|
||||||
import coil3.decode.Decoder
|
import coil3.decode.Decoder
|
||||||
import coil3.network.NetworkHeaders
|
import coil3.network.NetworkHeaders
|
||||||
@@ -42,7 +42,7 @@ fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
|
|||||||
httpHeaders(header)
|
httpHeaders(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val stubImage = ColorDrawable(Color.BLACK).asCoilImage(true)
|
private val stubImage = ColorDrawable(Color.BLACK).asImage(true)
|
||||||
private val stubResult = DecodeResult(stubImage, false)
|
private val stubResult = DecodeResult(stubImage, false)
|
||||||
private val stubFactory = Decoder { stubResult }
|
private val stubFactory = Decoder { stubResult }
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,57 @@
|
|||||||
package com.hippo.ehviewer.coil
|
package com.hippo.ehviewer.coil
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.ui.unit.IntRect
|
||||||
|
import coil3.BitmapImage
|
||||||
|
import coil3.Extras
|
||||||
|
import coil3.asImage
|
||||||
|
import coil3.getExtra
|
||||||
import coil3.intercept.Interceptor
|
import coil3.intercept.Interceptor
|
||||||
import coil3.intercept.Interceptor.Chain
|
import coil3.intercept.Interceptor.Chain
|
||||||
import coil3.request.ErrorResult
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.ImageResult
|
import coil3.request.ImageResult
|
||||||
|
import coil3.request.SuccessResult
|
||||||
import coil3.request.allowHardware
|
import coil3.request.allowHardware
|
||||||
|
|
||||||
/**
|
private val hardwareThresholdKey = Extras.Key(default = 16384)
|
||||||
* Allocating hardware bitmaps may fail on extremely long images.
|
|
||||||
*
|
fun ImageRequest.Builder.hardwareThreshold(size: Int) = apply {
|
||||||
* Retry with [allowHardware] disabled.
|
extras[hardwareThresholdKey] = size
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
val ImageRequest.hardwareThreshold: Int
|
||||||
|
get() = getExtra(hardwareThresholdKey)
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
object HardwareBitmapInterceptor : Interceptor {
|
object HardwareBitmapInterceptor : Interceptor {
|
||||||
override suspend fun intercept(chain: Chain): ImageResult {
|
override suspend fun intercept(chain: Chain): ImageResult {
|
||||||
val result = chain.proceed()
|
val result = chain.proceed()
|
||||||
return if (chain.request.allowHardware && result is ErrorResult && result.throwable is OutOfMemoryError) {
|
val request = result.request
|
||||||
val req = chain.request.newBuilder().allowHardware(false).build()
|
if (!request.allowHardware && result is SuccessResult) {
|
||||||
chain.withRequest(req).proceed()
|
var rect: IntRect? = null
|
||||||
} else {
|
val image = result.image
|
||||||
result
|
val bitmap = when (image) {
|
||||||
|
is BitmapImageWithRect -> image.image.also { rect = image.rect }
|
||||||
|
is BitmapImage -> image
|
||||||
|
else -> return result
|
||||||
|
}.bitmap
|
||||||
|
// Large hardware bitmaps have rendering issues (e.g. crash, empty) on some devices.
|
||||||
|
// This is not ideal but I haven't figured out how to probe the threshold.
|
||||||
|
// All we know is that it's less than the maximum texture size.
|
||||||
|
if (maxOf(bitmap.width, bitmap.height) <= request.hardwareThreshold) {
|
||||||
|
bitmap.copy(Bitmap.Config.HARDWARE, false)?.let { hwBitmap ->
|
||||||
|
bitmap.recycle()
|
||||||
|
val newImage = hwBitmap.asImage()
|
||||||
|
return result.copy(
|
||||||
|
image = rect?.let { BitmapImageWithRect(it, newImage as BitmapImage) } ?: newImage,
|
||||||
|
request = request,
|
||||||
|
dataSource = result.dataSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ object MergeInterceptor : Interceptor {
|
|||||||
|
|
||||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
val req = chain.request
|
val req = chain.request
|
||||||
val key = req.memoryCacheKey?.takeIf { it.startsWith("m/") || Settings.preloadThumbAggressively } ?: return withContext(req.interceptorDispatcher) { chain.proceed() }
|
val key = req.memoryCacheKey?.takeIf { it.startsWith("m/") || Settings.preloadThumbAggressively } ?: return withContext(req.interceptorCoroutineContext) { chain.proceed() }
|
||||||
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
synchronized(pendingContinuationMapLock) {
|
synchronized(pendingContinuationMapLock) {
|
||||||
@@ -62,7 +62,7 @@ object MergeInterceptor : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return withContext(req.interceptorDispatcher) { chain.proceed() }.apply {
|
return withContext(req.interceptorCoroutineContext) { chain.proceed() }.apply {
|
||||||
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
|
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
|
||||||
notifyScope.launch {
|
notifyScope.launch {
|
||||||
synchronized(pendingContinuationMapLock) {
|
synchronized(pendingContinuationMapLock) {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.hippo.ehviewer.cronet
|
|
||||||
|
|
||||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.asExecutor
|
|
||||||
|
|
||||||
val pool = DirectByteBufferPool(32)
|
|
||||||
|
|
||||||
// Limit thread to 1 since we are async & non-blocking
|
|
||||||
val cronetDispatcher = Dispatchers.Default.limitedParallelism(1)
|
|
||||||
val cronetHttpClientExecutor = cronetDispatcher.asExecutor()
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.hippo.ehviewer.cronet
|
package com.hippo.ehviewer.cronet
|
||||||
|
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import okio.Path.Companion.toOkioPath
|
import java.io.File
|
||||||
import org.chromium.net.CronetEngine
|
import org.chromium.net.CronetEngine
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
@@ -10,8 +10,8 @@ val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
|
|||||||
setUserAgent(Settings.userAgent)
|
setUserAgent(Settings.userAgent)
|
||||||
|
|
||||||
// Cache Quic hint only since the real cache mechanism should on Ktor layer
|
// Cache Quic hint only since the real cache mechanism should on Ktor layer
|
||||||
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
|
val cache = File(appCtx.cacheDir, "http_cache").apply { mkdirs() }
|
||||||
setStoragePath(cache.absolutePath)
|
setStoragePath(cache.path)
|
||||||
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 4096)
|
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 4096)
|
||||||
|
|
||||||
addQuicHint("e-hentai.org", 443, 443)
|
addQuicHint("e-hentai.org", 443, 443)
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ import splitties.init.appCtx
|
|||||||
|
|
||||||
private const val CHANNEL_ID = "DailyCheckNotification"
|
private const val CHANNEL_ID = "DailyCheckNotification"
|
||||||
|
|
||||||
class DailyCheckWork(context: Context, workerParams: WorkerParameters) :
|
class DailyCheckWork(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
|
||||||
CoroutineWorker(context, workerParams) {
|
|
||||||
override suspend fun doWork(): Result = withIOContext {
|
override suspend fun doWork(): Result = withIOContext {
|
||||||
checkDawn().onFailure {
|
checkDawn().onFailure {
|
||||||
return@withIOContext Result.retry()
|
return@withIOContext Result.retry()
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ data class DownloadInfo(
|
|||||||
|
|
||||||
@Embedded
|
@Embedded
|
||||||
val downloadInfo: DownloadEntity = DownloadEntity(galleryInfo.gid),
|
val downloadInfo: DownloadEntity = DownloadEntity(galleryInfo.gid),
|
||||||
) : GalleryInfo by galleryInfo, AbstractDownloadInfo by downloadInfo {
|
) : GalleryInfo by galleryInfo,
|
||||||
|
AbstractDownloadInfo by downloadInfo {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val STATE_INVALID = -1
|
const val STATE_INVALID = -1
|
||||||
|
|||||||
@@ -38,17 +38,18 @@ import com.hippo.ehviewer.spider.COMIC_INFO_FILE
|
|||||||
import com.hippo.ehviewer.spider.SpiderQueen
|
import com.hippo.ehviewer.spider.SpiderQueen
|
||||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.SPIDER_INFO_FILENAME
|
import com.hippo.ehviewer.spider.SpiderQueen.Companion.SPIDER_INFO_FILENAME
|
||||||
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||||
import com.hippo.ehviewer.spider.putToDownloadDir
|
import com.hippo.ehviewer.spider.downloadDirname
|
||||||
import com.hippo.ehviewer.spider.readComicInfo
|
import com.hippo.ehviewer.spider.readComicInfo
|
||||||
import com.hippo.ehviewer.spider.readCompatFromUniFile
|
import com.hippo.ehviewer.spider.readCompatFromPath
|
||||||
import com.hippo.ehviewer.spider.toSimpleTags
|
import com.hippo.ehviewer.spider.toSimpleTags
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.insertWith
|
import com.hippo.ehviewer.util.insertWith
|
||||||
import com.hippo.ehviewer.util.mapNotNull
|
import com.hippo.ehviewer.util.mapNotNull
|
||||||
import com.hippo.ehviewer.util.runAssertingNotMainThread
|
import com.hippo.ehviewer.util.runAssertingNotMainThread
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.files.delete
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.files.find
|
||||||
import com.hippo.unifile.asUniFileOrNull
|
import com.hippo.files.toOkioPath
|
||||||
|
import com.hippo.files.toUri
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -61,6 +62,9 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import okio.Path
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
|
import okio.Path.Companion.toPath
|
||||||
import splitties.preferences.edit
|
import splitties.preferences.edit
|
||||||
|
|
||||||
object DownloadManager : OnSpiderListener, CoroutineScope {
|
object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||||
@@ -172,7 +176,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// It is new download info
|
// It is new download info
|
||||||
info = DownloadInfo(galleryInfo, galleryInfo.putToDownloadDir())
|
info = DownloadInfo(galleryInfo, galleryInfo.downloadDirname())
|
||||||
info.label = label
|
info.label = label
|
||||||
info.state = DownloadInfo.STATE_WAIT
|
info.state = DownloadInfo.STATE_WAIT
|
||||||
// Add to all download list and map
|
// Add to all download list and map
|
||||||
@@ -541,7 +545,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
}
|
}
|
||||||
}.parMapNotNull(concurrency = 5) { (info, updateGallery, updateArtist) ->
|
}.parMapNotNull(concurrency = 5) { (info, updateGallery, updateArtist) ->
|
||||||
info.downloadDir?.run {
|
info.downloadDir?.run {
|
||||||
val comicInfo = findFile(COMIC_INFO_FILE)?.let { readComicInfo(it) }
|
val comicInfo = find(COMIC_INFO_FILE)?.let { readComicInfo(it) }
|
||||||
if (comicInfo != null) {
|
if (comicInfo != null) {
|
||||||
val galleryInfo = if (updateGallery) {
|
val galleryInfo = if (updateGallery) {
|
||||||
info.pages = comicInfo.pageCount
|
info.pages = comicInfo.pageCount
|
||||||
@@ -564,8 +568,8 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
} else if (info.pages == 0) {
|
} else if (info.pages == 0) {
|
||||||
val galleryInfo = findFile(SPIDER_INFO_FILENAME)?.let {
|
val galleryInfo = find(SPIDER_INFO_FILENAME)?.let {
|
||||||
readCompatFromUniFile(it)?.run {
|
readCompatFromPath(it)?.run {
|
||||||
info.pages = pages
|
info.pages = pages
|
||||||
info.galleryInfo
|
info.galleryInfo
|
||||||
}
|
}
|
||||||
@@ -605,6 +609,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
override fun onGet509(index: Int) {
|
override fun onGet509(index: Int) {
|
||||||
launch {
|
launch {
|
||||||
mDownloadListener?.onGet509()
|
mDownloadListener?.onGet509()
|
||||||
|
stopAllDownload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +631,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
info.finished = finished
|
info.finished = finished
|
||||||
info.downloaded = downloaded
|
info.downloaded = downloaded
|
||||||
info.total = total
|
info.total = total
|
||||||
mDownloadListener?.onGetPage(info)
|
mDownloadListener?.onDownload(info)
|
||||||
mutableNotifyFlow.emit(info)
|
mutableNotifyFlow.emit(info)
|
||||||
} ?: logcat(TAG, LogPriority.ERROR) { "Current task is null, but it should not be" }
|
} ?: logcat(TAG, LogPriority.ERROR) { "Current task is null, but it should not be" }
|
||||||
}
|
}
|
||||||
@@ -702,11 +707,6 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
*/
|
*/
|
||||||
fun onDownload(info: DownloadInfo)
|
fun onDownload(info: DownloadInfo)
|
||||||
|
|
||||||
/**
|
|
||||||
* Update page downloaded
|
|
||||||
*/
|
|
||||||
fun onGetPage(info: DownloadInfo)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download done
|
* Download done
|
||||||
*/
|
*/
|
||||||
@@ -813,18 +813,21 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
private fun comparator() = SortMode.from(Settings.downloadSortMode.value).comparator()
|
private fun comparator() = SortMode.from(Settings.downloadSortMode.value).comparator()
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadLocation: UniFile
|
var downloadLocation: Path
|
||||||
get() = with(Settings) {
|
get() = with(Settings) {
|
||||||
val uri = Uri.Builder().apply {
|
if (downloadScheme != null) {
|
||||||
scheme(downloadScheme)
|
Uri.Builder().apply {
|
||||||
encodedAuthority(downloadAuthority)
|
scheme(downloadScheme)
|
||||||
encodedPath(downloadPath)
|
encodedAuthority(downloadAuthority)
|
||||||
encodedQuery(downloadQuery)
|
encodedPath(downloadPath)
|
||||||
encodedFragment(downloadFragment)
|
encodedQuery(downloadQuery)
|
||||||
}.build()
|
encodedFragment(downloadFragment)
|
||||||
uri.asUniFileOrNull() ?: AppConfig.defaultDownloadDir?.asUniFile() ?: UniFile.Stub
|
}.build().toOkioPath()
|
||||||
|
} else {
|
||||||
|
AppConfig.defaultDownloadDir?.toOkioPath() ?: "".toPath()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
set(value) = with(value.uri) {
|
set(value) = with(value.toUri()) {
|
||||||
Settings.edit {
|
Settings.edit {
|
||||||
downloadScheme = scheme
|
downloadScheme = scheme
|
||||||
downloadAuthority = encodedAuthority
|
downloadAuthority = encodedAuthority
|
||||||
@@ -835,5 +838,5 @@ var downloadLocation: UniFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation / it }
|
val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation / it }
|
||||||
val DownloadInfo.archiveFile get() = downloadDir?.run { findFile("$gid.cbz") ?: findFile("$gid.zip") }
|
val DownloadInfo.archiveFile get() = downloadDir?.run { find("$gid.cbz") ?: find("$gid.zip") }
|
||||||
val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid")
|
val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid")?.toOkioPath()
|
||||||
@@ -21,8 +21,6 @@ import android.app.Service
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.SystemClock
|
|
||||||
import androidx.annotation.IntDef
|
|
||||||
import androidx.collection.LongSparseArray
|
import androidx.collection.LongSparseArray
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationChannelCompat
|
import androidx.core.app.NotificationChannelCompat
|
||||||
@@ -36,55 +34,57 @@ import com.hippo.ehviewer.dao.DownloadInfo
|
|||||||
import com.hippo.ehviewer.ui.MainActivity
|
import com.hippo.ehviewer.ui.MainActivity
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.ReadableTime
|
import com.hippo.ehviewer.util.ReadableTime
|
||||||
import com.hippo.ehviewer.util.SimpleHandler
|
|
||||||
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
||||||
|
import com.hippo.ehviewer.util.unsafeLazy
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineScope {
|
class DownloadService :
|
||||||
|
Service(),
|
||||||
|
DownloadManager.DownloadListener,
|
||||||
|
CoroutineScope {
|
||||||
override val coroutineContext = Dispatchers.IO + SupervisorJob()
|
override val coroutineContext = Dispatchers.IO + SupervisorJob()
|
||||||
private val deferredMgr = async { DownloadManager.apply { setDownloadListener(this@DownloadService) } }
|
private val deferredMgr = async { DownloadManager }
|
||||||
private var mNotifyManager: NotificationManagerCompat? = null
|
private val notifyManager by unsafeLazy { NotificationManagerCompat.from(this) }
|
||||||
private var mDownloadingBuilder: NotificationCompat.Builder? = null
|
private val downloadingNotification by unsafeLazy { initDownloadingNotification() }
|
||||||
private var mDownloadedBuilder: NotificationCompat.Builder? = null
|
private val downloadedNotification by lazy { initDownloadedNotification() }
|
||||||
private var m509dBuilder: NotificationCompat.Builder? = null
|
private val error509Notification by lazy { init509Notification() }
|
||||||
private var mDownloadingDelay: NotificationDelay? = null
|
private val channelId by unsafeLazy { "$packageName.download" }
|
||||||
private var mDownloadedDelay: NotificationDelay? = null
|
|
||||||
private var m509Delay: NotificationDelay? = null
|
|
||||||
private var mChannelID: String? = null
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
notifyManager.createNotificationChannel(
|
||||||
mChannelID = "$packageName.download"
|
NotificationChannelCompat.Builder(channelId, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||||
mNotifyManager = NotificationManagerCompat.from(this)
|
|
||||||
mNotifyManager!!.createNotificationChannel(
|
|
||||||
NotificationChannelCompat.Builder(mChannelID!!, NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.download_service)).build(),
|
.setName(getString(R.string.download_service)).build(),
|
||||||
)
|
)
|
||||||
ensureDownloadingBuilder()
|
downloadingNotification.builder.run {
|
||||||
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
|
setContentTitle(getString(R.string.download_service))
|
||||||
.setContentText(null)
|
.setContentText(null)
|
||||||
.setSubText(null)
|
.setSubText(null)
|
||||||
.setProgress(0, 0, true)
|
.setProgress(0, 0, true)
|
||||||
startForeground(ID_DOWNLOADING, mDownloadingBuilder!!.build())
|
startForeground(ID_DOWNLOADING, build())
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
deferredMgr.await().setDownloadListener(this@DownloadService)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
|
||||||
val scope = this
|
val scope = this
|
||||||
launch {
|
launch {
|
||||||
deferredMgr.await().setDownloadListener(null)
|
deferredMgr.await().setDownloadListener(null)
|
||||||
|
// Wait for the last notification to be posted
|
||||||
|
delay(DELAY)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
mNotifyManager = null
|
|
||||||
mDownloadingBuilder = null
|
|
||||||
mDownloadedBuilder = null
|
|
||||||
m509dBuilder = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
@@ -152,17 +152,14 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent) = error("No bindService")
|
override fun onBind(intent: Intent) = null
|
||||||
|
|
||||||
private fun ensureDownloadingBuilder() {
|
private fun initDownloadingNotification(): NotificationHandler {
|
||||||
if (mDownloadingBuilder != null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val stopAllIntent = Intent(this, DownloadService::class.java)
|
val stopAllIntent = Intent(this, DownloadService::class.java)
|
||||||
stopAllIntent.action = ACTION_STOP_ALL
|
stopAllIntent.action = ACTION_STOP_ALL
|
||||||
val piStopAll =
|
val piStopAll =
|
||||||
PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE)
|
PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
mDownloadingBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
|
val builder = NotificationCompat.Builder(applicationContext, channelId)
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setAutoCancel(false)
|
.setAutoCancel(false)
|
||||||
@@ -173,15 +170,11 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
piStopAll,
|
piStopAll,
|
||||||
)
|
)
|
||||||
.setShowWhen(false)
|
.setShowWhen(false)
|
||||||
.setChannelId(mChannelID!!)
|
return NotificationHandler(this, notifyManager, builder, ID_DOWNLOADING)
|
||||||
mDownloadingDelay =
|
.apply { launch { run() } }
|
||||||
NotificationDelay(this, mNotifyManager!!, mDownloadingBuilder!!, ID_DOWNLOADING)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureDownloadedBuilder() {
|
private fun initDownloadedNotification(): NotificationHandler {
|
||||||
if (mDownloadedBuilder != null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val clearIntent = Intent(this, DownloadService::class.java)
|
val clearIntent = Intent(this, DownloadService::class.java)
|
||||||
clearIntent.action = ACTION_CLEAR
|
clearIntent.action = ACTION_CLEAR
|
||||||
val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
|
val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
@@ -191,29 +184,26 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
||||||
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
||||||
val piActivity = PendingIntent.getActivity(
|
val piActivity = PendingIntent.getActivity(
|
||||||
this@DownloadService,
|
this,
|
||||||
0,
|
0,
|
||||||
activityIntent,
|
activityIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
mDownloadedBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
|
val builder = NotificationCompat.Builder(applicationContext, channelId)
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
.setContentTitle(getString(R.string.stat_download_done_title))
|
.setContentTitle(getString(R.string.stat_download_done_title))
|
||||||
.setDeleteIntent(piClear)
|
.setDeleteIntent(piClear)
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setContentIntent(piActivity)
|
.setContentIntent(piActivity)
|
||||||
mDownloadedDelay =
|
return NotificationHandler(this, notifyManager, builder, ID_DOWNLOADED)
|
||||||
NotificationDelay(this, mNotifyManager!!, mDownloadedBuilder!!, ID_DOWNLOADED)
|
.apply { launch { run() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensure509Builder() {
|
private fun init509Notification(): NotificationHandler {
|
||||||
if (m509dBuilder != null) {
|
val builder = NotificationCompat.Builder(applicationContext, channelId)
|
||||||
return
|
|
||||||
}
|
|
||||||
m509dBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
|
|
||||||
.setSmallIcon(R.drawable.ic_baseline_warning_24)
|
.setSmallIcon(R.drawable.ic_baseline_warning_24)
|
||||||
.setContentText(getString(R.string.stat_509_alert_title))
|
.setContentTitle(getString(R.string.stat_509_alert_title))
|
||||||
.setContentText(getString(R.string.stat_509_alert_text))
|
.setContentText(getString(R.string.stat_509_alert_text))
|
||||||
.setStyle(
|
.setStyle(
|
||||||
NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)),
|
NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)),
|
||||||
@@ -221,89 +211,61 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||||
m509Delay = NotificationDelay(this, mNotifyManager!!, m509dBuilder!!, ID_509)
|
return NotificationHandler(this, notifyManager, builder, ID_509)
|
||||||
|
.apply { launch { run() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGet509() {
|
override fun onGet509() {
|
||||||
launch {
|
error509Notification.run {
|
||||||
deferredMgr.await().stopAllDownload()
|
builder.setWhen(System.currentTimeMillis())
|
||||||
if (mNotifyManager == null) {
|
show()
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
ensure509Builder()
|
|
||||||
m509dBuilder!!.setWhen(System.currentTimeMillis())
|
|
||||||
m509Delay!!.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart(info: DownloadInfo) {
|
override fun onStart(info: DownloadInfo) {
|
||||||
if (mNotifyManager == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ensureDownloadingBuilder()
|
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putLong(KEY_GID, info.gid)
|
bundle.putLong(KEY_GID, info.gid)
|
||||||
val activityIntent = Intent(this, MainActivity::class.java)
|
val activityIntent = Intent(this, MainActivity::class.java)
|
||||||
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
||||||
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
||||||
val piActivity = PendingIntent.getActivity(
|
val piActivity = PendingIntent.getActivity(
|
||||||
this@DownloadService,
|
this,
|
||||||
0,
|
0,
|
||||||
activityIntent,
|
activityIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
)
|
)
|
||||||
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
|
downloadingNotification.run {
|
||||||
.setContentText(null)
|
builder.setContentTitle(EhUtils.getSuitableTitle(info))
|
||||||
.setSubText(null)
|
.setContentText(null)
|
||||||
.setProgress(0, 0, true)
|
.setSubText(null)
|
||||||
.setContentIntent(piActivity)
|
.setProgress(0, 0, true)
|
||||||
mDownloadingDelay!!.startForeground()
|
.setContentIntent(piActivity)
|
||||||
}
|
startForeground()
|
||||||
|
|
||||||
private fun onUpdate(info: DownloadInfo) {
|
|
||||||
if (mNotifyManager == null) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ensureDownloadingBuilder()
|
|
||||||
var speed = info.speed
|
|
||||||
if (speed < 0) {
|
|
||||||
speed = 0
|
|
||||||
}
|
|
||||||
var text = FileUtils.humanReadableByteCount(speed, false) + "/s"
|
|
||||||
val remaining = info.remaining
|
|
||||||
text = if (remaining >= 0) {
|
|
||||||
getString(
|
|
||||||
R.string.download_speed_text_2,
|
|
||||||
text,
|
|
||||||
ReadableTime.getShortTimeInterval(remaining),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
getString(R.string.download_speed_text, text)
|
|
||||||
}
|
|
||||||
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
|
|
||||||
.setContentText(text)
|
|
||||||
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
|
||||||
.setSubText(if (info.total == -1 || info.finished == -1) null else info.finished.toString() + "/" + info.total)
|
|
||||||
.setProgress(info.total, info.finished, false)
|
|
||||||
mDownloadingDelay!!.startForeground()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDownload(info: DownloadInfo) {
|
override fun onDownload(info: DownloadInfo) {
|
||||||
onUpdate(info)
|
val speed = info.speed.coerceAtLeast(0)
|
||||||
}
|
val speedText = FileUtils.humanReadableByteCount(speed, false) + "/s"
|
||||||
|
val remaining = info.remaining
|
||||||
override fun onGetPage(info: DownloadInfo) {
|
val text = if (remaining >= 0) {
|
||||||
onUpdate(info)
|
val interval = ReadableTime.getShortTimeInterval(remaining)
|
||||||
|
getString(R.string.download_speed_text_2, speedText, interval)
|
||||||
|
} else {
|
||||||
|
getString(R.string.download_speed_text, speedText)
|
||||||
|
}
|
||||||
|
downloadingNotification.run {
|
||||||
|
builder.setContentTitle(EhUtils.getSuitableTitle(info))
|
||||||
|
.setContentText(text)
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
||||||
|
.setSubText(if (info.total == -1 || info.finished == -1) null else info.finished.toString() + "/" + info.total)
|
||||||
|
.setProgress(info.total, info.finished, false)
|
||||||
|
startForeground()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinish(info: DownloadInfo) {
|
override fun onFinish(info: DownloadInfo) {
|
||||||
if (mNotifyManager == null) {
|
downloadingNotification.cancel()
|
||||||
return
|
|
||||||
}
|
|
||||||
if (null != mDownloadingDelay) {
|
|
||||||
mDownloadingDelay!!.cancel()
|
|
||||||
}
|
|
||||||
ensureDownloadedBuilder()
|
|
||||||
val finish = info.state == DownloadInfo.STATE_FINISH
|
val finish = info.state == DownloadInfo.STATE_FINISH
|
||||||
val gid = info.gid
|
val gid = info.gid
|
||||||
val index = sItemStateArray.indexOfKey(gid)
|
val index = sItemStateArray.indexOfKey(gid)
|
||||||
@@ -392,21 +354,18 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
} else {
|
} else {
|
||||||
style = null
|
style = null
|
||||||
}
|
}
|
||||||
mDownloadedBuilder!!.setContentText(text)
|
downloadedNotification.run {
|
||||||
.setStyle(style)
|
builder.setContentText(text)
|
||||||
.setWhen(System.currentTimeMillis())
|
.setStyle(style)
|
||||||
.setNumber(sDownloadedCount)
|
.setWhen(System.currentTimeMillis())
|
||||||
mDownloadedDelay!!.show()
|
.setNumber(sDownloadedCount)
|
||||||
|
show()
|
||||||
|
}
|
||||||
checkStopSelf()
|
checkStopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancel(info: DownloadInfo) {
|
override fun onCancel(info: DownloadInfo) {
|
||||||
if (mNotifyManager == null) {
|
downloadingNotification.cancel()
|
||||||
return
|
|
||||||
}
|
|
||||||
if (null != mDownloadingDelay) {
|
|
||||||
mDownloadingDelay!!.cancel()
|
|
||||||
}
|
|
||||||
checkStopSelf()
|
checkStopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,105 +378,49 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NotificationDelay(
|
private class NotificationHandler(
|
||||||
private var mService: Service,
|
private val service: Service,
|
||||||
private val mNotifyManager: NotificationManagerCompat,
|
private val notifyManager: NotificationManagerCompat,
|
||||||
private val mBuilder: NotificationCompat.Builder,
|
val builder: NotificationCompat.Builder,
|
||||||
private val mId: Int,
|
private val id: Int,
|
||||||
) : Runnable {
|
) {
|
||||||
private var mLastTime: Long = 0
|
private val channel = Channel<Ops>(Channel.CONFLATED)
|
||||||
private var mPosted = false
|
|
||||||
|
|
||||||
// false for show, true for cancel
|
|
||||||
@Ops
|
|
||||||
private var mOps = 0
|
|
||||||
|
|
||||||
fun show() {
|
fun show() {
|
||||||
if (mPosted) {
|
channel.trySend(Ops.Notify)
|
||||||
mOps = OPS_NOTIFY
|
|
||||||
} else {
|
|
||||||
val now = SystemClock.currentThreadTimeMillis()
|
|
||||||
if (now - mLastTime > DELAY) {
|
|
||||||
// Wait long enough, do it now
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
|
||||||
mService,
|
|
||||||
Manifest.permission.POST_NOTIFICATIONS,
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mNotifyManager.notify(mId, mBuilder.build())
|
|
||||||
} else {
|
|
||||||
// Too quick, post delay
|
|
||||||
mOps = OPS_NOTIFY
|
|
||||||
mPosted = true
|
|
||||||
SimpleHandler.postDelayed(this, DELAY)
|
|
||||||
}
|
|
||||||
mLastTime = now
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
if (mPosted) {
|
channel.trySend(Ops.Cancel)
|
||||||
mOps = OPS_CANCEL
|
|
||||||
} else {
|
|
||||||
val now = SystemClock.currentThreadTimeMillis()
|
|
||||||
if (now - mLastTime > DELAY) {
|
|
||||||
// Wait long enough, do it now
|
|
||||||
mNotifyManager.cancel(mId)
|
|
||||||
} else {
|
|
||||||
// Too quick, post delay
|
|
||||||
mOps = OPS_CANCEL
|
|
||||||
mPosted = true
|
|
||||||
SimpleHandler.postDelayed(this, DELAY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startForeground() {
|
fun startForeground() {
|
||||||
if (mPosted) {
|
channel.trySend(Ops.StartForeground)
|
||||||
mOps = OPS_START_FOREGROUND
|
|
||||||
} else {
|
|
||||||
val now = SystemClock.currentThreadTimeMillis()
|
|
||||||
if (now - mLastTime > DELAY) {
|
|
||||||
// Wait long enough, do it now
|
|
||||||
mService.startForeground(mId, mBuilder.build())
|
|
||||||
} else {
|
|
||||||
// Too quick, post delay
|
|
||||||
mOps = OPS_START_FOREGROUND
|
|
||||||
mPosted = true
|
|
||||||
SimpleHandler.postDelayed(this, DELAY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun run() {
|
suspend fun run() {
|
||||||
mPosted = false
|
channel.receiveAsFlow().sample(DELAY).collect {
|
||||||
when (mOps) {
|
when (it) {
|
||||||
OPS_NOTIFY -> {
|
Ops.Notify -> {
|
||||||
if (ActivityCompat.checkSelfPermission(
|
if (ActivityCompat.checkSelfPermission(
|
||||||
mService,
|
service,
|
||||||
Manifest.permission.POST_NOTIFICATIONS,
|
Manifest.permission.POST_NOTIFICATIONS,
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
return
|
notifyManager.notify(id, builder.build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mNotifyManager.notify(mId, mBuilder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
OPS_CANCEL -> mNotifyManager.cancel(mId)
|
Ops.Cancel -> notifyManager.cancel(id)
|
||||||
OPS_START_FOREGROUND -> mService.startForeground(mId, mBuilder.build())
|
Ops.StartForeground -> service.startForeground(id, builder.build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IntDef(OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND)
|
private enum class Ops {
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
Notify,
|
||||||
private annotation class Ops
|
Cancel,
|
||||||
companion object {
|
StartForeground,
|
||||||
private const val OPS_NOTIFY = 0
|
|
||||||
private const val OPS_CANCEL = 1
|
|
||||||
private const val OPS_START_FOREGROUND = 2
|
|
||||||
private const val DELAY: Long = 1000 // 1s
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +447,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
private const val ID_DOWNLOADING = 1
|
private const val ID_DOWNLOADING = 1
|
||||||
private const val ID_DOWNLOADED = 2
|
private const val ID_DOWNLOADED = 2
|
||||||
private const val ID_509 = 3
|
private const val ID_509 = 3
|
||||||
|
private const val DELAY = 1000L // 1s
|
||||||
private val sItemStateArray = LongSparseArray<Boolean>()
|
private val sItemStateArray = LongSparseArray<Boolean>()
|
||||||
private val sItemTitleArray = LongSparseArray<String>()
|
private val sItemTitleArray = LongSparseArray<String>()
|
||||||
private var sFailedCount = 0
|
private var sFailedCount = 0
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ import com.hippo.ehviewer.jni.openArchive
|
|||||||
import com.hippo.ehviewer.jni.providePassword
|
import com.hippo.ehviewer.jni.providePassword
|
||||||
import com.hippo.ehviewer.jni.releaseByteBuffer
|
import com.hippo.ehviewer.jni.releaseByteBuffer
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.ehviewer.util.displayPath
|
||||||
import com.hippo.unifile.displayPath
|
import com.hippo.files.openFileDescriptor
|
||||||
|
import com.hippo.files.toUri
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
@@ -41,21 +42,22 @@ import kotlinx.coroutines.sync.Semaphore
|
|||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import moe.tarsin.coroutines.NamedMutex
|
import moe.tarsin.coroutines.NamedMutex
|
||||||
import moe.tarsin.coroutines.withLock
|
import moe.tarsin.coroutines.withLock
|
||||||
|
import okio.Path
|
||||||
|
|
||||||
typealias PasswdInvalidator = (String) -> Boolean
|
typealias PasswdInvalidator = (String) -> Boolean
|
||||||
typealias PasswdProvider = suspend (PasswdInvalidator) -> String
|
typealias PasswdProvider = suspend (PasswdInvalidator) -> String
|
||||||
|
|
||||||
class ArchivePageLoader(
|
class ArchivePageLoader(
|
||||||
private val file: UniFile,
|
private val file: Path,
|
||||||
gid: Long = 0,
|
gid: Long = 0,
|
||||||
startPage: Int = 0,
|
startPage: Int = 0,
|
||||||
passwdProvider: PasswdProvider? = null,
|
passwdProvider: PasswdProvider? = null,
|
||||||
) : PageLoader2(gid, startPage) {
|
) : PageLoader2(gid, startPage) {
|
||||||
private lateinit var pfd: ParcelFileDescriptor
|
private lateinit var pfd: ParcelFileDescriptor
|
||||||
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
||||||
logcat(DEBUG_TAG) { "Open archive ${file.uri.displayPath}" }
|
logcat(DEBUG_TAG) { "Open archive ${file.toUri().displayPath}" }
|
||||||
pfd = file.openFileDescriptor("r")
|
pfd = file.openFileDescriptor("r")
|
||||||
size = openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name!!.endsWith(".zip"))
|
size = openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name.endsWith(".zip"))
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -81,7 +83,7 @@ class ArchivePageLoader(
|
|||||||
super.stop()
|
super.stop()
|
||||||
closeArchive()
|
closeArchive()
|
||||||
pfd.close()
|
pfd.close()
|
||||||
logcat(DEBUG_TAG) { "Close archive ${file.uri.displayPath} successfully!" }
|
logcat(DEBUG_TAG) { "Close archive ${file.toUri().displayPath} successfully!" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mJobMap = hashMapOf<Int, Job>()
|
private val mJobMap = hashMapOf<Int, Job>()
|
||||||
@@ -149,19 +151,15 @@ class ArchivePageLoader(
|
|||||||
FileUtils.getNameFromFilename(file.name)!!
|
FileUtils.getNameFromFilename(file.name)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getImageExtension(index: Int): String {
|
override fun getImageExtension(index: Int): String = getExtension(index)
|
||||||
return getExtension(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun save(index: Int, file: UniFile): Boolean {
|
override fun save(index: Int, file: Path): Boolean = runCatching {
|
||||||
return runCatching {
|
file.openFileDescriptor("w").use {
|
||||||
file.openFileDescriptor("w").use {
|
extractToFd(index, it.fd)
|
||||||
extractToFd(index, it.fd)
|
|
||||||
}
|
|
||||||
}.getOrElse {
|
|
||||||
logcat(it)
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
}.getOrElse {
|
||||||
|
logcat(it)
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
|
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ import com.hippo.ehviewer.spider.SpiderQueen
|
|||||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
|
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
|
||||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
|
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
|
||||||
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||||
import com.hippo.unifile.UniFile
|
import okio.Path
|
||||||
|
|
||||||
class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
||||||
PageLoader2(mGalleryInfo.gid, startPage), OnSpiderListener {
|
PageLoader2(mGalleryInfo.gid, startPage),
|
||||||
|
OnSpiderListener {
|
||||||
private lateinit var mSpiderQueen: SpiderQueen
|
private lateinit var mSpiderQueen: SpiderQueen
|
||||||
override fun start() {
|
override fun start() {
|
||||||
super.start()
|
super.start()
|
||||||
@@ -45,13 +46,9 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
|||||||
EhUtils.getSuitableTitle(mGalleryInfo)
|
EhUtils.getSuitableTitle(mGalleryInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getImageExtension(index: Int): String? {
|
override fun getImageExtension(index: Int): String? = mSpiderQueen.getExtension(index)
|
||||||
return mSpiderQueen.getExtension(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun save(index: Int, file: UniFile): Boolean {
|
override fun save(index: Int, file: Path): Boolean = mSpiderQueen.save(index, file)
|
||||||
return mSpiderQueen.save(index, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val size: Int
|
override val size: Int
|
||||||
get() = mSpiderQueen.size
|
get() = mSpiderQueen.size
|
||||||
@@ -64,9 +61,7 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
|||||||
mSpiderQueen.forceRequest(index, orgImg)
|
mSpiderQueen.forceRequest(index, orgImg)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun awaitReady(): Boolean {
|
override suspend fun awaitReady(): Boolean = super.awaitReady() && mSpiderQueen.awaitReady()
|
||||||
return super.awaitReady() && mSpiderQueen.awaitReady()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isReady: Boolean
|
override val isReady: Boolean
|
||||||
get() = ::mSpiderQueen.isInitialized && mSpiderQueen.isReady
|
get() = ::mSpiderQueen.isInitialized && mSpiderQueen.isReady
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package com.hippo.ehviewer.gallery
|
|||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.CoroutineStart
|
import kotlinx.coroutines.CoroutineStart
|
||||||
@@ -26,8 +25,11 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import okio.Path
|
||||||
|
|
||||||
abstract class PageLoader2(private val gid: Long, var startPage: Int) : PageLoader(), CoroutineScope {
|
abstract class PageLoader2(private val gid: Long, var startPage: Int) :
|
||||||
|
PageLoader(),
|
||||||
|
CoroutineScope {
|
||||||
override val coroutineContext = Dispatchers.IO + Job()
|
override val coroutineContext = Dispatchers.IO + Job()
|
||||||
private val progressJob = launch(start = CoroutineStart.LAZY) {
|
private val progressJob = launch(start = CoroutineStart.LAZY) {
|
||||||
if (startPage == -1) {
|
if (startPage == -1) {
|
||||||
@@ -66,9 +68,5 @@ abstract class PageLoader2(private val gid: Long, var startPage: Int) : PageLoad
|
|||||||
FileUtils.sanitizeFilename("$title - ${index + 1}.${it.lowercase()}")
|
FileUtils.sanitizeFilename("$title - ${index + 1}.${it.lowercase()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun save(index: Int, file: UniFile): Boolean
|
abstract fun save(index: Int, file: Path): Boolean
|
||||||
|
|
||||||
fun saveToDir(index: Int, dir: UniFile): UniFile? = getImageFilename(index)?.let { filename ->
|
|
||||||
(dir / filename).takeIf { save(index, it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -30,10 +30,12 @@ import coil3.imageLoader
|
|||||||
import coil3.request.CachePolicy
|
import coil3.request.CachePolicy
|
||||||
import coil3.request.ErrorResult
|
import coil3.request.ErrorResult
|
||||||
import coil3.request.SuccessResult
|
import coil3.request.SuccessResult
|
||||||
|
import coil3.request.allowHardware
|
||||||
import coil3.size.Dimension
|
import coil3.size.Dimension
|
||||||
import coil3.size.Precision
|
import coil3.size.Precision
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.coil.BitmapImageWithRect
|
import com.hippo.ehviewer.coil.BitmapImageWithRect
|
||||||
|
import com.hippo.ehviewer.coil.hardwareThreshold
|
||||||
import com.hippo.ehviewer.coil.maybeCropBorder
|
import com.hippo.ehviewer.coil.maybeCropBorder
|
||||||
import com.hippo.ehviewer.jni.isGif
|
import com.hippo.ehviewer.jni.isGif
|
||||||
import com.hippo.ehviewer.jni.mmap
|
import com.hippo.ehviewer.jni.mmap
|
||||||
@@ -42,9 +44,11 @@ import com.hippo.ehviewer.jni.rewriteGifSource
|
|||||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||||
import com.hippo.ehviewer.util.isAtLeastP
|
import com.hippo.ehviewer.util.isAtLeastP
|
||||||
import com.hippo.ehviewer.util.isAtLeastU
|
import com.hippo.ehviewer.util.isAtLeastU
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.files.openFileDescriptor
|
||||||
|
import com.hippo.files.toUri
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import okio.Path
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
class Image private constructor(image: CoilImage, private val src: AutoCloseable) {
|
class Image private constructor(image: CoilImage, private val src: AutoCloseable) {
|
||||||
@@ -75,12 +79,14 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
|||||||
companion object {
|
companion object {
|
||||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
|
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
|
||||||
|
|
||||||
private suspend fun Either<ByteBufferSource, UniFileSource>.decodeCoil(): CoilImage {
|
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(): CoilImage {
|
||||||
val req = appCtx.imageRequest {
|
val req = appCtx.imageRequest {
|
||||||
onLeft { data(it.source) }
|
onLeft { data(it.source) }
|
||||||
onRight { data(it.source.uri) }
|
onRight { data(it.source.toUri()) }
|
||||||
size(Dimension(targetWidth), Dimension.Undefined)
|
size(Dimension(targetWidth), Dimension.Undefined)
|
||||||
precision(Precision.INEXACT)
|
precision(Precision.INEXACT)
|
||||||
|
allowHardware(false)
|
||||||
|
hardwareThreshold(Settings.hardwareBitmapThreshold)
|
||||||
maybeCropBorder(Settings.cropBorder.value)
|
maybeCropBorder(Settings.cropBorder.value)
|
||||||
memoryCachePolicy(CachePolicy.DISABLED)
|
memoryCachePolicy(CachePolicy.DISABLED)
|
||||||
}
|
}
|
||||||
@@ -93,7 +99,7 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
|||||||
suspend fun decode(src: ImageSource): Image? {
|
suspend fun decode(src: ImageSource): Image? {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val image = when (src) {
|
val image = when (src) {
|
||||||
is UniFileSource -> {
|
is PathSource -> {
|
||||||
if (isAtLeastP && !isAtLeastU) {
|
if (isAtLeastP && !isAtLeastU) {
|
||||||
src.source.openFileDescriptor("rw").use {
|
src.source.openFileDescriptor("rw").use {
|
||||||
val fd = it.fd
|
val fd = it.fd
|
||||||
@@ -133,8 +139,8 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
|||||||
|
|
||||||
sealed interface ImageSource : AutoCloseable
|
sealed interface ImageSource : AutoCloseable
|
||||||
|
|
||||||
interface UniFileSource : ImageSource {
|
interface PathSource : ImageSource {
|
||||||
val source: UniFile
|
val source: Path
|
||||||
val type: String
|
val type: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.hippo.ehviewer.ktor
|
||||||
|
|
||||||
|
import io.ktor.client.engine.HttpClientEngine
|
||||||
|
import io.ktor.client.engine.HttpClientEngineFactory
|
||||||
|
|
||||||
|
object Cronet : HttpClientEngineFactory<CronetConfig> {
|
||||||
|
override fun create(block: CronetConfig.() -> Unit): HttpClientEngine =
|
||||||
|
CronetEngine(CronetConfig().apply(block))
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.hippo.ehviewer.ktor
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.ktor.client.engine.HttpClientEngineConfig
|
||||||
|
import org.chromium.net.CronetEngine
|
||||||
|
|
||||||
|
class CronetConfig : HttpClientEngineConfig() {
|
||||||
|
var client: CronetEngine? = null
|
||||||
|
|
||||||
|
fun config(context: Context, block: CronetEngine.Builder.() -> Unit) {
|
||||||
|
client = CronetEngine.Builder(context).apply(block).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
package com.hippo.ehviewer.ktor
|
package com.hippo.ehviewer.ktor
|
||||||
|
|
||||||
import com.hippo.ehviewer.cronet.cronetDispatcher
|
|
||||||
import com.hippo.ehviewer.cronet.cronetHttpClient
|
|
||||||
import com.hippo.ehviewer.cronet.cronetHttpClientExecutor
|
|
||||||
import com.hippo.ehviewer.cronet.pool
|
|
||||||
import io.ktor.client.engine.HttpClientEngineBase
|
import io.ktor.client.engine.HttpClientEngineBase
|
||||||
import io.ktor.client.engine.HttpClientEngineConfig
|
|
||||||
import io.ktor.client.engine.callContext
|
import io.ktor.client.engine.callContext
|
||||||
import io.ktor.client.request.HttpRequestData
|
import io.ktor.client.request.HttpRequestData
|
||||||
import io.ktor.client.request.HttpResponseData
|
import io.ktor.client.request.HttpResponseData
|
||||||
@@ -18,12 +13,15 @@ import io.ktor.util.date.GMTDate
|
|||||||
import io.ktor.util.flattenForEach
|
import io.ktor.util.flattenForEach
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import io.ktor.utils.io.InternalAPI
|
import io.ktor.utils.io.InternalAPI
|
||||||
|
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||||
import io.ktor.utils.io.pool.useInstance
|
import io.ktor.utils.io.pool.useInstance
|
||||||
import io.ktor.utils.io.writer
|
import io.ktor.utils.io.writer
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.asExecutor
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.consumeEach
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import kotlinx.coroutines.job
|
import kotlinx.coroutines.job
|
||||||
@@ -33,9 +31,12 @@ import org.chromium.net.UrlRequest
|
|||||||
import org.chromium.net.UrlResponseInfo
|
import org.chromium.net.UrlResponseInfo
|
||||||
import org.chromium.net.apihelpers.UploadDataProviders
|
import org.chromium.net.apihelpers.UploadDataProviders
|
||||||
|
|
||||||
object CronetEngine : HttpClientEngineBase("Cronet") {
|
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
|
||||||
override val config = HttpClientEngineConfig()
|
// Limit thread to 1 since we are async & non-blocking
|
||||||
override val dispatcher = cronetDispatcher
|
override val dispatcher = Dispatchers.Default.limitedParallelism(1)
|
||||||
|
private val executor = dispatcher.asExecutor()
|
||||||
|
private val pool = DirectByteBufferPool(32)
|
||||||
|
private val client = config.client ?: error("Cronet client is not configured")
|
||||||
|
|
||||||
@InternalAPI
|
@InternalAPI
|
||||||
override suspend fun execute(data: HttpRequestData) = executeHttpRequest(callContext(), data)
|
override suspend fun execute(data: HttpRequestData) = executeHttpRequest(callContext(), data)
|
||||||
@@ -94,12 +95,12 @@ object CronetEngine : HttpClientEngineBase("Cronet") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cronetHttpClient.newUrlRequestBuilder(data.url.toString(), callback, cronetHttpClientExecutor).apply {
|
client.newUrlRequestBuilder(data.url.toString(), callback, executor).apply {
|
||||||
setHttpMethod(data.method.value)
|
setHttpMethod(data.method.value)
|
||||||
data.headers.flattenForEach { key, value -> addHeader(key, value) }
|
data.headers.flattenForEach { key, value -> addHeader(key, value) }
|
||||||
data.body.contentType?.let { addHeader(HttpHeaders.ContentType, it.toString()) }
|
data.body.contentType?.let { addHeader(HttpHeaders.ContentType, it.toString()) }
|
||||||
data.body.contentLength?.let { addHeader(HttpHeaders.ContentLength, it.toString()) }
|
data.body.contentLength?.let { addHeader(HttpHeaders.ContentLength, it.toString()) }
|
||||||
data.body.toUploadDataProvider()?.let { setUploadDataProvider(it, cronetHttpClientExecutor) }
|
data.body.toUploadDataProvider()?.let { setUploadDataProvider(it, executor) }
|
||||||
}.build().apply {
|
}.build().apply {
|
||||||
start()
|
start()
|
||||||
callContext.job.invokeOnCompletion { cancel() }
|
callContext.job.invokeOnCompletion { cancel() }
|
||||||
|
|||||||
@@ -7,17 +7,11 @@ import okio.source
|
|||||||
|
|
||||||
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
||||||
val source = inputStream.source().buffer()
|
val source = inputStream.source().buffer()
|
||||||
fun read(): String {
|
fun read(): String = source.readUtf8LineStrict()
|
||||||
return source.readUtf8LineStrict()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readInt(): Int {
|
fun readInt(): Int = read().toInt()
|
||||||
return read().toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readLong(): Long {
|
fun readLong(): Long = read().toLong()
|
||||||
return read().toLong()
|
|
||||||
}
|
|
||||||
repeat(2) { read() } // We assert that only info v2
|
repeat(2) { read() } // We assert that only info v2
|
||||||
val gid = readLong()
|
val gid = readLong()
|
||||||
val token = read()
|
val token = read()
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ import com.hippo.ehviewer.client.data.GalleryDetail
|
|||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
import com.hippo.ehviewer.client.data.SimpleTagsConverter
|
import com.hippo.ehviewer.client.data.SimpleTagsConverter
|
||||||
import com.hippo.ehviewer.client.data.TagNamespace
|
import com.hippo.ehviewer.client.data.TagNamespace
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.files.openInputStream
|
||||||
import com.hippo.unifile.openInputStream
|
import com.hippo.files.openOutputStream
|
||||||
import com.hippo.unifile.openOutputStream
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.io.OutputStreamWriter
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -18,15 +15,19 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
import nl.adaptivity.xmlutil.XmlDeclMode
|
import nl.adaptivity.xmlutil.XmlDeclMode
|
||||||
|
import nl.adaptivity.xmlutil.XmlUtilInternal
|
||||||
import nl.adaptivity.xmlutil.core.XmlVersion
|
import nl.adaptivity.xmlutil.core.XmlVersion
|
||||||
import nl.adaptivity.xmlutil.newGenericWriter
|
import nl.adaptivity.xmlutil.newWriter
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
import nl.adaptivity.xmlutil.serialization.XmlElement
|
import nl.adaptivity.xmlutil.serialization.XmlElement
|
||||||
import nl.adaptivity.xmlutil.xmlStreaming
|
import nl.adaptivity.xmlutil.xmlStreaming
|
||||||
|
import okio.Path
|
||||||
|
|
||||||
const val COMIC_INFO_FILE = "ComicInfo.xml"
|
const val COMIC_INFO_FILE = "ComicInfo.xml"
|
||||||
private const val TAG_ORIGINAL = "original"
|
private const val TAG_ORIGINAL = "original"
|
||||||
|
|
||||||
|
// Workaround for https://youtrack.jetbrains.com/issue/KT-69182
|
||||||
|
@OptIn(XmlUtilInternal::class)
|
||||||
private val xml = XML {
|
private val xml = XML {
|
||||||
recommended {
|
recommended {
|
||||||
ignoreUnknownChildren()
|
ignoreUnknownChildren()
|
||||||
@@ -97,22 +98,18 @@ fun ComicInfo.toSimpleTags() = listOfNotNull(
|
|||||||
teams,
|
teams,
|
||||||
).flatten().ifEmpty { null }
|
).flatten().ifEmpty { null }
|
||||||
|
|
||||||
fun ComicInfo.write(file: UniFile) {
|
fun ComicInfo.write(file: Path) {
|
||||||
file.openOutputStream().use {
|
file.openOutputStream().bufferedWriter().use {
|
||||||
OutputStreamWriter(it, Charsets.UTF_8).use { writer ->
|
xmlStreaming.newWriter(it).use { writer ->
|
||||||
xmlStreaming.newGenericWriter(writer).use { xmlWriter ->
|
xml.encodeToWriter(writer, ComicInfo.serializer(), this)
|
||||||
xml.encodeToWriter(xmlWriter, ComicInfo.serializer(), this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readComicInfo(file: UniFile): ComicInfo? = runCatching {
|
fun readComicInfo(file: Path): ComicInfo? = runCatching {
|
||||||
file.openInputStream().use {
|
file.openInputStream().bufferedReader().use {
|
||||||
InputStreamReader(it, Charsets.UTF_8).use { reader ->
|
xmlStreaming.newReader(it).use { reader ->
|
||||||
xmlStreaming.newGenericReader(reader).use { xmlReader ->
|
xml.decodeFromReader(ComicInfo.serializer(), reader)
|
||||||
xml.decodeFromReader(ComicInfo.serializer(), xmlReader)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|||||||
@@ -18,12 +18,10 @@ object DownloadInfoMagics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decodeMagicRequestOrUrl(encoded: String): Pair<String, String?> {
|
fun decodeMagicRequestOrUrl(encoded: String): Pair<String, String?> = if (encoded.startsWith(DOWNLOAD_INFO_DIRNAME_URL_MAGIC)) {
|
||||||
return if (encoded.startsWith(DOWNLOAD_INFO_DIRNAME_URL_MAGIC)) {
|
val (a, b) = encoded.removePrefix(DOWNLOAD_INFO_DIRNAME_URL_MAGIC).split(DOWNLOAD_INFO_DIRNAME_URL_SEPARATOR)
|
||||||
val (a, b) = encoded.removePrefix(DOWNLOAD_INFO_DIRNAME_URL_MAGIC).split(DOWNLOAD_INFO_DIRNAME_URL_SEPARATOR)
|
a to b
|
||||||
a to b
|
} else {
|
||||||
} else {
|
encoded to null
|
||||||
encoded to null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,13 +36,18 @@ import com.hippo.ehviewer.dao.DownloadArtist
|
|||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
import com.hippo.ehviewer.download.downloadLocation
|
import com.hippo.ehviewer.download.downloadLocation
|
||||||
import com.hippo.ehviewer.download.tempDownloadDir
|
import com.hippo.ehviewer.download.tempDownloadDir
|
||||||
import com.hippo.ehviewer.image.UniFileSource
|
import com.hippo.ehviewer.image.PathSource
|
||||||
import com.hippo.ehviewer.jni.archiveFdBatch
|
import com.hippo.ehviewer.jni.archiveFdBatch
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.sendTo
|
import com.hippo.ehviewer.util.sendTo
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.ehviewer.util.sha1
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.files.delete
|
||||||
import com.hippo.unifile.openOutputStream
|
import com.hippo.files.exists
|
||||||
|
import com.hippo.files.find
|
||||||
|
import com.hippo.files.isDirectory
|
||||||
|
import com.hippo.files.list
|
||||||
|
import com.hippo.files.openFileDescriptor
|
||||||
|
import com.hippo.files.openOutputStream
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import io.ktor.client.plugins.onDownload
|
import io.ktor.client.plugins.onDownload
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
@@ -52,16 +57,23 @@ import io.ktor.http.isSuccess
|
|||||||
import io.ktor.utils.io.jvm.nio.copyTo
|
import io.ktor.utils.io.jvm.nio.copyTo
|
||||||
import kotlin.io.path.readText
|
import kotlin.io.path.readText
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import okio.Path
|
||||||
|
|
||||||
class SpiderDen(val info: GalleryInfo) {
|
class SpiderDen(val info: GalleryInfo) {
|
||||||
private val gid = info.gid
|
private val gid = info.gid
|
||||||
var downloadDir: UniFile? = null
|
var downloadDir: Path? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var tempDownloadDir: UniFile? = null
|
private var tempDownloadDir: Path? = null
|
||||||
private val saveAsCbz = Settings.saveAsCbz
|
private val saveAsCbz = Settings.saveAsCbz
|
||||||
private val archiveName = "$gid.cbz"
|
private val archiveName = "$gid.cbz"
|
||||||
|
|
||||||
|
// Search in both directories to maintain compatibility
|
||||||
|
private val fileCache by lazy {
|
||||||
|
(tempDownloadDir?.list().orEmpty() + downloadDir?.list().orEmpty())
|
||||||
|
.associateBy { it.name } as MutableMap
|
||||||
|
}
|
||||||
|
|
||||||
private val imageDir
|
private val imageDir
|
||||||
get() = tempDownloadDir.takeIf { saveAsCbz } ?: downloadDir
|
get() = tempDownloadDir.takeIf { saveAsCbz } ?: downloadDir
|
||||||
|
|
||||||
@@ -78,10 +90,10 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
mode = value
|
mode = value
|
||||||
if (mode == SpiderQueen.MODE_DOWNLOAD) {
|
if (mode == SpiderQueen.MODE_DOWNLOAD) {
|
||||||
if (downloadDir == null) {
|
if (downloadDir == null) {
|
||||||
downloadDir = getGalleryDownloadDir(gid)?.takeIf { it.ensureDir() }
|
downloadDir = getGalleryDownloadDir(info)
|
||||||
}
|
}
|
||||||
if (saveAsCbz && tempDownloadDir == null) {
|
if (saveAsCbz && tempDownloadDir == null) {
|
||||||
tempDownloadDir = info.tempDownloadDir?.takeIf { it.ensureDir() }
|
tempDownloadDir = info.tempDownloadDir!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,14 +103,12 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
return sCache.read(key) { true } ?: false
|
return sCache.read(key) { true } ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search in both directories to maintain compatibility
|
private fun findImageFile(index: Int) = synchronized(fileCache) {
|
||||||
private fun findImageFile(index: Int): UniFile? {
|
val head = perFilename(index)
|
||||||
return tempDownloadDir?.findImageFile(index) ?: downloadDir?.findImageFile(index)
|
fileCache.entries.firstOrNull { (name) -> name.startsWith(head) }?.value
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun containInDownloadDir(index: Int): Boolean {
|
private fun containInDownloadDir(index: Int): Boolean = findImageFile(index) != null
|
||||||
return findImageFile(index) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
|
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
|
||||||
val dir = imageDir ?: return false
|
val dir = imageDir ?: return false
|
||||||
@@ -106,8 +116,8 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
return runCatching {
|
return runCatching {
|
||||||
sCache.read(key) {
|
sCache.read(key) {
|
||||||
val extension = metadata.toFile().readText()
|
val extension = metadata.toFile().readText()
|
||||||
val file = dir.createFile(perFilename(index, extension)) ?: return false
|
val file = dir.findDownloadFileForIndex(index, extension)
|
||||||
data.asUniFile() sendTo file
|
data sendTo file
|
||||||
true
|
true
|
||||||
} ?: false
|
} ?: false
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
@@ -115,19 +125,17 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun contains(index: Int): Boolean {
|
operator fun contains(index: Int): Boolean = when (mode) {
|
||||||
return when (mode) {
|
SpiderQueen.MODE_READ -> {
|
||||||
SpiderQueen.MODE_READ -> {
|
containInCache(index) || containInDownloadDir(index)
|
||||||
containInCache(index) || containInDownloadDir(index)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
SpiderQueen.MODE_DOWNLOAD -> {
|
SpiderQueen.MODE_DOWNLOAD -> {
|
||||||
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
|
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
false
|
false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,16 +144,20 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
return sCache.remove(key)
|
return sCache.remove(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeFromDownloadDir(index: Int): Boolean {
|
private fun removeFromDownloadDir(index: Int) = findImageFile(index)?.run {
|
||||||
return findImageFile(index)?.delete() ?: false
|
delete()
|
||||||
|
synchronized(fileCache) {
|
||||||
|
fileCache.remove(name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(index: Int): Boolean {
|
fun remove(index: Int) = removeFromCache(index).also { removeFromDownloadDir(index) }
|
||||||
return removeFromCache(index) or removeFromDownloadDir(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findDownloadFileForIndex(index: Int, extension: String): UniFile? {
|
private fun Path.findDownloadFileForIndex(index: Int, extension: String): Path {
|
||||||
return imageDir?.createFile(perFilename(index, extension))
|
val name = perFilename(index, extension)
|
||||||
|
return synchronized(fileCache) {
|
||||||
|
fileCache.getOrPut(name) { resolve(name) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun makeHttpCallAndSaveImage(
|
suspend fun makeHttpCallAndSaveImage(
|
||||||
@@ -170,10 +182,10 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
private suspend inline fun saveResponseMeta(
|
private suspend inline fun saveResponseMeta(
|
||||||
index: Int,
|
index: Int,
|
||||||
ext: String,
|
ext: String,
|
||||||
crossinline fops: suspend (UniFile) -> Unit,
|
crossinline fops: suspend (Path) -> Unit,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
findDownloadFileForIndex(index, ext)?.run {
|
imageDir?.run {
|
||||||
fops(this)
|
fops(findDownloadFileForIndex(index, ext))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +194,7 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
val key = getImageKey(gid, index)
|
val key = getImageKey(gid, index)
|
||||||
return sCache.suspendEdit(key) {
|
return sCache.suspendEdit(key) {
|
||||||
metadata.toFile().writeText(ext)
|
metadata.toFile().writeText(ext)
|
||||||
fops(data.asUniFile())
|
fops(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -195,16 +207,19 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
outFile.openOutputStream().use {
|
outFile.openOutputStream().use {
|
||||||
response.bodyAsChannel().copyTo(it.channel)
|
response.bodyAsChannel().copyTo(it.channel)
|
||||||
}
|
}
|
||||||
|
val expected = FileHashRegex.findAll(url).last().groupValues[1]
|
||||||
|
val actual = outFile.sha1()
|
||||||
|
check(expected == actual) { "File hash mismatch: expected $expected, but got $actual\nURL: $url" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveToUniFile(index: Int, file: UniFile): Boolean {
|
fun saveToPath(index: Int, file: Path): Boolean {
|
||||||
val key = getImageKey(gid, index)
|
val key = getImageKey(gid, index)
|
||||||
|
|
||||||
// Read from diskCache first
|
// Read from diskCache first
|
||||||
sCache.read(key) {
|
sCache.read(key) {
|
||||||
runCatching {
|
runCatching {
|
||||||
data.asUniFile() sendTo file
|
data sendTo file
|
||||||
return true
|
return true
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
logcat(it)
|
logcat(it)
|
||||||
@@ -230,13 +245,13 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
?: findImageFile(index)?.name.let { FileUtils.getExtensionFromFilename(it) }
|
?: findImageFile(index)?.name.let { FileUtils.getExtensionFromFilename(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImageSource(index: Int): UniFileSource? {
|
fun getImageSource(index: Int): PathSource? {
|
||||||
if (mode == SpiderQueen.MODE_READ) {
|
if (mode == SpiderQueen.MODE_READ) {
|
||||||
val key = getImageKey(gid, index)
|
val key = getImageKey(gid, index)
|
||||||
val snapshot = sCache.openSnapshot(key)
|
val snapshot = sCache.openSnapshot(key)
|
||||||
if (snapshot != null) {
|
if (snapshot != null) {
|
||||||
return object : UniFileSource, AutoCloseable by snapshot {
|
return object : PathSource, AutoCloseable by snapshot {
|
||||||
override val source = snapshot.data.asUniFile()
|
override val source = snapshot.data
|
||||||
override val type by lazy {
|
override val type by lazy {
|
||||||
snapshot.metadata.toNioPath().readText()
|
snapshot.metadata.toNioPath().readText()
|
||||||
}
|
}
|
||||||
@@ -244,7 +259,7 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val source = findImageFile(index) ?: return null
|
val source = findImageFile(index) ?: return null
|
||||||
return object : UniFileSource {
|
return object : PathSource {
|
||||||
override val source = source
|
override val source = source
|
||||||
override val type by lazy {
|
override val type by lazy {
|
||||||
FileUtils.getExtensionFromFilename(source.name)!!
|
FileUtils.getExtensionFromFilename(source.name)!!
|
||||||
@@ -257,7 +272,7 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
suspend fun archive() {
|
suspend fun archive() {
|
||||||
if (saveAsCbz) {
|
if (saveAsCbz) {
|
||||||
downloadDir?.run {
|
downloadDir?.run {
|
||||||
findFile(archiveName) ?: createFile(archiveName)?.let { file ->
|
resolve(archiveName).let { file ->
|
||||||
runCatching {
|
runCatching {
|
||||||
archiveTo(file)
|
archiveTo(file)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
@@ -272,23 +287,23 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
|
|
||||||
suspend fun postArchive(): Boolean {
|
suspend fun postArchive(): Boolean {
|
||||||
val dir = downloadDir
|
val dir = downloadDir
|
||||||
val archived = saveAsCbz && dir?.findFile(archiveName) != null
|
val archived = saveAsCbz && dir?.find(archiveName) != null
|
||||||
if (archived) {
|
if (archived) {
|
||||||
dir.listFiles().parMap(concurrency = 10) {
|
dir.list().parMap(concurrency = 10) {
|
||||||
if (it.name?.matches(filenamePattern) == true) {
|
if (it.name.matches(FileNameRegex)) {
|
||||||
it.delete()
|
it.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dir.findFile(SpiderQueen.SPIDER_INFO_FILENAME)?.delete()
|
(dir / SpiderQueen.SPIDER_INFO_FILENAME).delete()
|
||||||
tempDownloadDir?.delete()
|
tempDownloadDir?.delete()
|
||||||
}
|
}
|
||||||
return archived
|
return archived
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun exportAsCbz(file: UniFile) =
|
suspend fun exportAsCbz(file: Path) =
|
||||||
downloadDir!!.findFile(archiveName)?.sendTo(file) ?: archiveTo(file)
|
downloadDir!!.find(archiveName)?.sendTo(file) ?: archiveTo(file)
|
||||||
|
|
||||||
private suspend fun archiveTo(file: UniFile) = resourceScope {
|
private suspend fun archiveTo(file: Path) = resourceScope {
|
||||||
val comicInfo = closeable {
|
val comicInfo = closeable {
|
||||||
val f = downloadDir!! / COMIC_INFO_FILE
|
val f = downloadDir!! / COMIC_INFO_FILE
|
||||||
if (!f.exists()) {
|
if (!f.exists()) {
|
||||||
@@ -308,18 +323,17 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun initDownloadDirIfExist() {
|
suspend fun initDownloadDirIfExist() {
|
||||||
downloadDir = getGalleryDownloadDir(gid)?.takeIf { it.isDirectory }
|
downloadDir = getGalleryDownloadDir(info).takeIf { it.isDirectory }
|
||||||
tempDownloadDir = info.tempDownloadDir?.takeIf { it.isDirectory }
|
tempDownloadDir = info.tempDownloadDir?.takeIf { it.isDirectory }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun initDownloadDir() {
|
suspend fun initDownloadDir() {
|
||||||
downloadDir = getGalleryDownloadDir(gid) ?: (downloadLocation / info.putToDownloadDir())
|
downloadDir = getGalleryDownloadDir(info)
|
||||||
check(downloadDir!!.ensureDir())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun writeComicInfo(fetchMetadata: Boolean = true) {
|
suspend fun writeComicInfo(fetchMetadata: Boolean = true) {
|
||||||
downloadDir?.run {
|
downloadDir?.run {
|
||||||
createFile(COMIC_INFO_FILE)?.also {
|
resolve(COMIC_INFO_FILE).also {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (info !is GalleryDetail && fetchMetadata) {
|
if (info !is GalleryDetail && fetchMetadata) {
|
||||||
EhEngine.fillGalleryListByApi(listOf(info))
|
EhEngine.fillGalleryListByApi(listOf(info))
|
||||||
@@ -339,25 +353,22 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val filenamePattern = Regex("^\\d{8}\\.\\w{3,4}")
|
private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
|
||||||
|
private val FileHashRegex = Regex("/([0-9a-f]{40})(?:-\\d+){3}-\\w+")
|
||||||
|
|
||||||
fun perFilename(index: Int, extension: String = ""): String {
|
fun perFilename(index: Int, extension: String = ""): String = "%08d.%s".format(index + 1, extension)
|
||||||
return "%08d.%s".format(index + 1, extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun UniFile.findImageFile(index: Int): UniFile? {
|
suspend fun GalleryInfo.downloadDirname(): String {
|
||||||
val head = perFilename(index)
|
var dirname = EhDB.getDownloadDirname(gid)
|
||||||
return findFirst { name -> name.startsWith(head) }
|
if (dirname == null) {
|
||||||
}
|
val title = getSuitableTitle(this)
|
||||||
|
dirname = FileUtils.sanitizeFilename("$gid-$title")
|
||||||
suspend fun GalleryInfo.putToDownloadDir(): String {
|
EhDB.putDownloadDirname(gid, dirname)
|
||||||
val title = getSuitableTitle(this)
|
}
|
||||||
val dirname = FileUtils.sanitizeFilename("$gid-$title")
|
|
||||||
EhDB.putDownloadDirname(gid, dirname)
|
|
||||||
return dirname
|
return dirname
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getGalleryDownloadDir(gid: Long): UniFile? {
|
suspend fun getGalleryDownloadDir(info: GalleryInfo): Path {
|
||||||
val dirname = EhDB.getDownloadDirname(gid) ?: return null
|
val dirname = info.downloadDirname()
|
||||||
return downloadLocation / dirname
|
return downloadLocation / dirname
|
||||||
}
|
}
|
||||||
@@ -4,15 +4,15 @@ import com.hippo.ehviewer.coil.edit
|
|||||||
import com.hippo.ehviewer.coil.read
|
import com.hippo.ehviewer.coil.read
|
||||||
import com.hippo.ehviewer.ktbuilder.diskCache
|
import com.hippo.ehviewer.ktbuilder.diskCache
|
||||||
import com.hippo.ehviewer.legacy.readLegacySpiderInfo
|
import com.hippo.ehviewer.legacy.readLegacySpiderInfo
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.files.openInputStream
|
||||||
import com.hippo.unifile.openInputStream
|
import com.hippo.files.openOutputStream
|
||||||
import com.hippo.unifile.openOutputStream
|
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.cbor.Cbor
|
import kotlinx.serialization.cbor.Cbor
|
||||||
import kotlinx.serialization.decodeFromByteArray
|
import kotlinx.serialization.decodeFromByteArray
|
||||||
import kotlinx.serialization.encodeToByteArray
|
import kotlinx.serialization.encodeToByteArray
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
import okio.Path
|
||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ private val cbor = Cbor {
|
|||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SpiderInfo.write(file: UniFile) {
|
fun SpiderInfo.write(file: Path) {
|
||||||
file.openOutputStream().use {
|
file.openOutputStream().use {
|
||||||
it.write(cbor.encodeToByteArray(this))
|
it.write(cbor.encodeToByteArray(this))
|
||||||
}
|
}
|
||||||
@@ -58,24 +58,20 @@ private val spiderInfoCache by lazy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readFromCache(gid: Long): SpiderInfo? {
|
fun readFromCache(gid: Long): SpiderInfo? = spiderInfoCache.read(gid.toString()) {
|
||||||
return spiderInfoCache.read(gid.toString()) {
|
runCatching {
|
||||||
runCatching {
|
cbor.decodeFromByteArray<SpiderInfo>(data.toFile().readBytes())
|
||||||
cbor.decodeFromByteArray<SpiderInfo>(data.toFile().readBytes())
|
}.onFailure {
|
||||||
}.onFailure {
|
logcat(it)
|
||||||
logcat(it)
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
|
|
||||||
return runCatching {
|
|
||||||
file.openInputStream().use {
|
|
||||||
cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
|
|
||||||
}
|
|
||||||
}.getOrNull() ?: runCatching {
|
|
||||||
file.openInputStream().use { readLegacySpiderInfo(it) }
|
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun readCompatFromPath(file: Path): SpiderInfo? = runCatching {
|
||||||
|
file.openInputStream().use {
|
||||||
|
cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
|
||||||
|
}
|
||||||
|
}.getOrNull() ?: runCatching {
|
||||||
|
file.openInputStream().use { readLegacySpiderInfo(it) }
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
const val TOKEN_FAILED = "failed"
|
const val TOKEN_FAILED = "failed"
|
||||||
@@ -38,7 +38,7 @@ import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
|
|||||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
|
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
|
||||||
import com.hippo.ehviewer.image.Image
|
import com.hippo.ehviewer.image.Image
|
||||||
import com.hippo.ehviewer.util.displayString
|
import com.hippo.ehviewer.util.displayString
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.files.find
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@@ -61,6 +61,7 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
import okio.Path
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {
|
class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {
|
||||||
@@ -302,12 +303,12 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
mWorkerScope.launch(index, force, orgImg)
|
mWorkerScope.launch(index, force, orgImg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun save(index: Int, file: UniFile): Boolean {
|
fun save(index: Int, file: Path): Boolean {
|
||||||
val state = getPageState(index)
|
val state = getPageState(index)
|
||||||
return if (STATE_FINISHED != state) {
|
return if (STATE_FINISHED != state) {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
mSpiderDen.saveToUniFile(index, file)
|
mSpiderDen.saveToPath(index, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,16 +321,14 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readSpiderInfoFromLocal(): SpiderInfo? {
|
private fun readSpiderInfoFromLocal(): SpiderInfo? = mSpiderDen.downloadDir?.run {
|
||||||
return mSpiderDen.downloadDir?.run {
|
find(SPIDER_INFO_FILENAME)?.let { file ->
|
||||||
findFile(SPIDER_INFO_FILENAME)?.let { file ->
|
readCompatFromPath(file)?.takeIf {
|
||||||
readCompatFromUniFile(file)?.takeIf {
|
it.gid == galleryInfo.gid && it.token == galleryInfo.token
|
||||||
it.gid == galleryInfo.gid && it.token == galleryInfo.token
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
|
|
||||||
}
|
}
|
||||||
|
?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
|
||||||
|
|
||||||
private fun readPreviews(body: String, index: Int, spiderInfo: SpiderInfo) {
|
private fun readPreviews(body: String, index: Int, spiderInfo: SpiderInfo) {
|
||||||
spiderInfo.previewPages = parsePreviewPages(body)
|
spiderInfo.previewPages = parsePreviewPages(body)
|
||||||
@@ -349,21 +348,19 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun readSpiderInfoFromInternet(): SpiderInfo? {
|
private suspend fun readSpiderInfoFromInternet(): SpiderInfo? = runSuspendCatching {
|
||||||
return runSuspendCatching {
|
ehRequest(
|
||||||
ehRequest(
|
getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, 0, false),
|
||||||
getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, 0, false),
|
referer,
|
||||||
referer,
|
).fetchUsingAsText {
|
||||||
).fetchUsingAsText {
|
val pages = parsePages(this)
|
||||||
val pages = parsePages(this)
|
val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token, pages)
|
||||||
val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token, pages)
|
readPreviews(this, 0, spiderInfo)
|
||||||
readPreviews(this, 0, spiderInfo)
|
spiderInfo
|
||||||
spiderInfo
|
}
|
||||||
}
|
}.onFailure {
|
||||||
}.onFailure {
|
logcat(it)
|
||||||
logcat(it)
|
}.getOrNull()
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getPTokenFromMultiPageViewer(index: Int): String? {
|
suspend fun getPTokenFromMultiPageViewer(index: Int): String? {
|
||||||
val spiderInfo = mSpiderInfo
|
val spiderInfo = mSpiderInfo
|
||||||
@@ -417,13 +414,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
private fun writeSpiderInfoToLocal() {
|
private fun writeSpiderInfoToLocal() {
|
||||||
if (!isReady) return
|
if (!isReady) return
|
||||||
mSpiderDen.downloadDir?.run { createFile(SPIDER_INFO_FILENAME)?.also { mSpiderInfo.write(it) } }
|
mSpiderDen.downloadDir?.run { mSpiderInfo.write(resolve(SPIDER_INFO_FILENAME)) }
|
||||||
mSpiderInfo.saveToCache()
|
mSpiderInfo.saveToCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isStateDone(state: Int): Boolean {
|
private fun isStateDone(state: Int): Boolean = state == STATE_FINISHED || state == STATE_FAILED
|
||||||
return state == STATE_FINISHED || state == STATE_FAILED
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updatePageState(index: Int, @State state: Int, error: String? = null) {
|
fun updatePageState(index: Int, @State state: Int, error: String? = null) {
|
||||||
var oldState: Int
|
var oldState: Int
|
||||||
|
|||||||
@@ -57,13 +57,16 @@ import com.hippo.ehviewer.ui.destinations.ReaderScreenDestination
|
|||||||
import com.hippo.ehviewer.ui.tools.DialogState
|
import com.hippo.ehviewer.ui.tools.DialogState
|
||||||
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
|
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
|
||||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||||
|
import com.hippo.ehviewer.util.bgWork
|
||||||
import com.hippo.ehviewer.util.findActivity
|
import com.hippo.ehviewer.util.findActivity
|
||||||
import com.hippo.ehviewer.util.isAtLeastT
|
import com.hippo.ehviewer.util.isAtLeastT
|
||||||
import com.hippo.ehviewer.util.mapToLongArray
|
import com.hippo.ehviewer.util.mapToLongArray
|
||||||
import com.hippo.ehviewer.util.requestPermission
|
import com.hippo.ehviewer.util.requestPermission
|
||||||
import com.hippo.ehviewer.util.toEpochMillis
|
import com.hippo.ehviewer.util.toEpochMillis
|
||||||
import com.hippo.ehviewer.util.toLocalDateTime
|
import com.hippo.ehviewer.util.toLocalDateTime
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.files.delete
|
||||||
|
import com.hippo.files.isDirectory
|
||||||
|
import com.hippo.files.openOutputStream
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
@@ -77,20 +80,20 @@ import kotlinx.datetime.TimeZone
|
|||||||
import kotlinx.datetime.minus
|
import kotlinx.datetime.minus
|
||||||
import kotlinx.datetime.todayIn
|
import kotlinx.datetime.todayIn
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
import okio.Path
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
private fun removeNoMediaFile(downloadDir: UniFile) {
|
private fun removeNoMediaFile(downloadDir: Path) {
|
||||||
val noMedia = downloadDir / ".nomedia"
|
(downloadDir / ".nomedia").delete()
|
||||||
noMedia.delete()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureNoMediaFile(downloadDir: UniFile) {
|
private fun ensureNoMediaFile(downloadDir: Path) {
|
||||||
downloadDir.createFile(".nomedia") ?: return
|
(downloadDir / ".nomedia").openOutputStream().close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val lck = Mutex()
|
private val lck = Mutex()
|
||||||
|
|
||||||
suspend fun keepNoMediaFileStatus(downloadDir: UniFile = downloadLocation) {
|
suspend fun keepNoMediaFileStatus(downloadDir: Path = downloadLocation) {
|
||||||
if (downloadDir.isDirectory) {
|
if (downloadDir.isDirectory) {
|
||||||
lck.withLock {
|
lck.withLock {
|
||||||
if (Settings.mediaScan) {
|
if (Settings.mediaScan) {
|
||||||
@@ -205,10 +208,14 @@ suspend fun DialogState.modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
|
|||||||
add(localFav)
|
add(localFav)
|
||||||
addAll(cloudFav)
|
addAll(cloudFav)
|
||||||
}
|
}
|
||||||
|
if (galleryInfo.favoriteSlot >= 0 && galleryInfo.favoriteNote == null) {
|
||||||
|
galleryInfo.favoriteNote = bgWork { EhEngine.getFavoriteNote(galleryInfo.gid, galleryInfo.token) }
|
||||||
|
}
|
||||||
val (slot, note) = awaitSelectItemWithIconAndTextField(
|
val (slot, note) = awaitSelectItemWithIconAndTextField(
|
||||||
items,
|
items,
|
||||||
title = R.string.add_favorites_dialog_title,
|
title = R.string.add_favorites_dialog_title,
|
||||||
hint = R.string.favorite_note,
|
hint = R.string.favorite_note,
|
||||||
|
initialNote = galleryInfo.favoriteNote.orEmpty(),
|
||||||
maxChar = MAX_FAVNOTE_CHAR,
|
maxChar = MAX_FAVNOTE_CHAR,
|
||||||
)
|
)
|
||||||
return doModifyFavorites(galleryInfo, if (isFavorited) slot - 2 else slot - 1, localFavorited, note)
|
return doModifyFavorites(galleryInfo, if (isFavorited) slot - 2 else slot - 1, localFavorited, note)
|
||||||
@@ -247,6 +254,7 @@ private suspend fun doModifyFavorites(
|
|||||||
|
|
||||||
in 0..9 -> {
|
in 0..9 -> {
|
||||||
EhEngine.modifyFavorites(galleryInfo.gid, galleryInfo.token, slot, note)
|
EhEngine.modifyFavorites(galleryInfo.gid, galleryInfo.token, slot, note)
|
||||||
|
galleryInfo.favoriteNote = note
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,9 +447,7 @@ suspend fun DialogState.awaitSelectDate(): String? {
|
|||||||
title = R.string.go_to,
|
title = R.string.go_to,
|
||||||
yearRange = initial.year..yesterday.year,
|
yearRange = initial.year..yesterday.year,
|
||||||
selectableDates = object : SelectableDates {
|
selectableDates = object : SelectableDates {
|
||||||
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis in dateRange
|
||||||
return utcTimeMillis in dateRange
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val date = dateMillis?.run { toLocalDateTime().date.toString() }
|
val date = dateMillis?.run { toLocalDateTime().date.toString() }
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ import com.hippo.ehviewer.ui.tools.LabeledCheckbox
|
|||||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||||
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
||||||
import com.hippo.ehviewer.updater.AppUpdater
|
import com.hippo.ehviewer.updater.AppUpdater
|
||||||
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.addTextToClipboard
|
import com.hippo.ehviewer.util.addTextToClipboard
|
||||||
import com.hippo.ehviewer.util.calculateFraction
|
import com.hippo.ehviewer.util.calculateFraction
|
||||||
import com.hippo.ehviewer.util.displayString
|
import com.hippo.ehviewer.util.displayString
|
||||||
@@ -142,8 +143,9 @@ import com.hippo.ehviewer.util.getParcelableExtraCompat
|
|||||||
import com.hippo.ehviewer.util.getUrlFromClipboard
|
import com.hippo.ehviewer.util.getUrlFromClipboard
|
||||||
import com.hippo.ehviewer.util.isAtLeastQ
|
import com.hippo.ehviewer.util.isAtLeastQ
|
||||||
import com.hippo.ehviewer.util.isAtLeastS
|
import com.hippo.ehviewer.util.isAtLeastS
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.ehviewer.util.sha1
|
||||||
import com.hippo.unifile.sha1
|
import com.hippo.files.isDirectory
|
||||||
|
import com.hippo.files.toOkioPath
|
||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||||
import com.ramcosta.composedestinations.utils.currentDestinationAsState
|
import com.ramcosta.composedestinations.utils.currentDestinationAsState
|
||||||
@@ -226,7 +228,7 @@ class MainActivity : EhActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun DialogState.checkDownloadLocation() {
|
suspend fun DialogState.checkDownloadLocation() {
|
||||||
val valid = withIOContext { downloadLocation.ensureDir() }
|
val valid = withIOContext { downloadLocation.isDirectory }
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
awaitPermissionOrCancel(
|
awaitPermissionOrCancel(
|
||||||
confirmText = R.string.open_settings,
|
confirmText = R.string.open_settings,
|
||||||
@@ -242,17 +244,19 @@ class MainActivity : EhActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
if (!AppConfig.isBenchmark) {
|
||||||
runCatching { dialogState.checkDownloadLocation() }
|
LaunchedEffect(Unit) {
|
||||||
runCatching { dialogState.checkAppLinkVerify() }
|
runCatching { dialogState.checkDownloadLocation() }
|
||||||
runSuspendCatching {
|
runCatching { dialogState.checkAppLinkVerify() }
|
||||||
withIOContext {
|
runSuspendCatching {
|
||||||
AppUpdater.checkForUpdate()?.let {
|
withIOContext {
|
||||||
dialogState.showNewVersion(this@MainActivity, it)
|
AppUpdater.checkForUpdate()?.let {
|
||||||
|
dialogState.showNewVersion(this@MainActivity, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}.onFailure {
|
||||||
|
snackbarState.showSnackbar(getString(R.string.update_failed, it.displayString()))
|
||||||
}
|
}
|
||||||
}.onFailure {
|
|
||||||
snackbarState.showSnackbar(getString(R.string.update_failed, it.displayString()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +281,7 @@ class MainActivity : EhActivity() {
|
|||||||
} else if (type != null && type.startsWith("image/")) {
|
} else if (type != null && type.startsWith("image/")) {
|
||||||
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
||||||
if (null != uri) {
|
if (null != uri) {
|
||||||
val hash = withIOContext { uri.asUniFile().sha1() }
|
val hash = withIOContext { uri.toOkioPath().sha1() }
|
||||||
navigator.navigate(
|
navigator.navigate(
|
||||||
ListUrlBuilder(
|
ListUrlBuilder(
|
||||||
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
|
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.EnterTransition
|
import androidx.compose.animation.EnterTransition
|
||||||
import androidx.compose.animation.ExitTransition
|
import androidx.compose.animation.ExitTransition
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -27,7 +25,6 @@ inline fun ComposeView.setMD3Content(crossinline content: @Composable () -> Unit
|
|||||||
dialogState.Intercept()
|
dialogState.Intercept()
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalDialogState provides dialogState,
|
LocalDialogState provides dialogState,
|
||||||
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@@ -41,7 +38,6 @@ inline fun ComponentActivity.setMD3Content(crossinline content: @Composable () -
|
|||||||
dialogState.Intercept()
|
dialogState.Intercept()
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalDialogState provides dialogState,
|
LocalDialogState provides dialogState,
|
||||||
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.graphics.drawable.Animatable
|
|||||||
import android.graphics.drawable.DrawableWrapper
|
import android.graphics.drawable.DrawableWrapper
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import io.ktor.http.Url
|
|
||||||
|
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
@@ -37,16 +36,7 @@ fun WebViewSignInScreen(navigator: DestinationsNavigator) {
|
|||||||
view.destroy()
|
view.destroy()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var getId = false
|
if (EhCookieStore.hasSignedIn()) {
|
||||||
var getHash = false
|
|
||||||
EhCookieStore.load(Url(EhUrl.HOST_E)).forEach {
|
|
||||||
if (EhCookieStore.KEY_IPB_MEMBER_ID == it.name) {
|
|
||||||
getId = true
|
|
||||||
} else if (EhCookieStore.KEY_IPB_PASS_HASH == it.name) {
|
|
||||||
getHash = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (getId && getHash) {
|
|
||||||
present = true
|
present = true
|
||||||
coroutineScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
withNonCancellableContext { postLogin() }
|
withNonCancellableContext { postLogin() }
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import androidx.compose.runtime.Stable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
@@ -34,13 +33,15 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
|
import arrow.resilience.Schedule
|
||||||
|
import arrow.resilience.retry
|
||||||
import com.hippo.ehviewer.ui.tools.PredictiveBackEasing
|
import com.hippo.ehviewer.ui.tools.PredictiveBackEasing
|
||||||
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
||||||
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
||||||
|
import kotlin.time.Duration.Companion.microseconds
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.coroutines.onEachLatest
|
import moe.tarsin.coroutines.onEachLatest
|
||||||
|
|
||||||
@@ -66,35 +67,32 @@ class FabLayoutState(
|
|||||||
val expandProgress = Animatable(if (initialValue == FabLayoutValue.Expand) 1f else 0f)
|
val expandProgress = Animatable(if (initialValue == FabLayoutValue.Expand) 1f else 0f)
|
||||||
.apply { updateBounds(0f, 1f) }
|
.apply { updateBounds(0f, 1f) }
|
||||||
|
|
||||||
private suspend fun downTo(value: FabLayoutValue, priority: MutatePriority) {
|
private suspend fun downTo(value: FabLayoutValue, priority: MutatePriority) = mutatorMutex.mutate(priority) {
|
||||||
mutatorMutex.mutate(priority) {
|
|
||||||
if (expandProgress.value != 0f) expandProgress.animateTo(0f, animationSpec)
|
|
||||||
if (value == FabLayoutValue.Hidden) {
|
|
||||||
appearProgress.animateTo(0f, animationSpec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun upTo(value: FabLayoutValue, priority: MutatePriority) {
|
|
||||||
mutatorMutex.mutate(priority) {
|
|
||||||
if (appearProgress.value != 1f) appearProgress.animateTo(1f, animationSpec)
|
|
||||||
if (value == FabLayoutValue.Expand) {
|
|
||||||
expandProgress.animateTo(1f, animationSpec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun waitCollapse() {
|
|
||||||
if (expandProgress.value != 0f) {
|
if (expandProgress.value != 0f) {
|
||||||
snapshotFlow { expandProgress.value }.first { it == 0f }
|
expandProgress.animateTo(0f, animationSpec)
|
||||||
|
}
|
||||||
|
if (value == FabLayoutValue.Hidden) {
|
||||||
|
appearProgress.animateTo(0f, animationSpec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun upTo(value: FabLayoutValue, priority: MutatePriority) = mutatorMutex.mutate(priority) {
|
||||||
|
if (appearProgress.value != 1f) {
|
||||||
|
appearProgress.animateTo(1f, animationSpec)
|
||||||
|
}
|
||||||
|
if (value == FabLayoutValue.Expand) {
|
||||||
|
expandProgress.animateTo(1f, animationSpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun show(priority: MutatePriority = MutatePriority.Default) = upTo(FabLayoutValue.Primary, priority)
|
suspend fun show(priority: MutatePriority = MutatePriority.Default) = upTo(FabLayoutValue.Primary, priority)
|
||||||
suspend fun hide(priority: MutatePriority = MutatePriority.Default) = downTo(FabLayoutValue.Hidden, priority)
|
suspend fun hide(priority: MutatePriority = MutatePriority.Default) = downTo(FabLayoutValue.Hidden, priority)
|
||||||
suspend fun collapse(priority: MutatePriority = MutatePriority.Default) = downTo(FabLayoutValue.Primary, priority)
|
suspend fun collapse(priority: MutatePriority = MutatePriority.Default) = downTo(FabLayoutValue.Primary, priority)
|
||||||
suspend fun expand(priority: MutatePriority = MutatePriority.Default) = upTo(FabLayoutValue.Expand, priority)
|
suspend fun expand(priority: MutatePriority = MutatePriority.Default) = upTo(FabLayoutValue.Expand, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val fabSyncSchedule = Schedule.linear<Throwable>(100.microseconds)
|
||||||
|
|
||||||
fun interface FabBuilder {
|
fun interface FabBuilder {
|
||||||
fun onClick(icon: ImageVector, autoClose: Boolean, that: suspend () -> Unit)
|
fun onClick(icon: ImageVector, autoClose: Boolean, that: suspend () -> Unit)
|
||||||
fun onClick(icon: ImageVector, that: suspend () -> Unit) = onClick(icon, true, that)
|
fun onClick(icon: ImageVector, that: suspend () -> Unit) = onClick(icon, true, that)
|
||||||
@@ -118,12 +116,14 @@ fun FabLayout(
|
|||||||
}
|
}
|
||||||
val state = rememberFabLayoutState(newState)
|
val state = rememberFabLayoutState(newState)
|
||||||
LaunchedEffect(newState) {
|
LaunchedEffect(newState) {
|
||||||
when (newState) {
|
fabSyncSchedule.retry {
|
||||||
FabLayoutValue.Expand -> state.expand()
|
when (newState) {
|
||||||
FabLayoutValue.Hidden -> state.hide()
|
FabLayoutValue.Expand -> state.expand()
|
||||||
FabLayoutValue.Primary -> {
|
FabLayoutValue.Hidden -> state.hide()
|
||||||
state.collapse()
|
FabLayoutValue.Primary -> {
|
||||||
state.show()
|
state.collapse()
|
||||||
|
state.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,7 @@ fun FabLayout(
|
|||||||
|
|
||||||
val secondaryFab by delegateSnapshotUpdate {
|
val secondaryFab by delegateSnapshotUpdate {
|
||||||
record { buildFab(builder) }
|
record { buildFab(builder) }
|
||||||
transform { onEachLatest { state.waitCollapse() } }
|
transform { onEachLatest { state.collapse(MutatePriority.PreventUserInput) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|||||||
@@ -151,9 +151,9 @@ fun GalleryList(
|
|||||||
columns = StaggeredGridCells.Fixed(thumbColumns),
|
columns = StaggeredGridCells.Fixed(thumbColumns),
|
||||||
modifier = contentModifier,
|
modifier = contentModifier,
|
||||||
state = thumbListState,
|
state = thumbListState,
|
||||||
|
contentPadding = contentPadding + PaddingValues(marginH, marginV),
|
||||||
verticalItemSpacing = gridInterval,
|
verticalItemSpacing = gridInterval,
|
||||||
horizontalArrangement = Arrangement.spacedBy(gridInterval),
|
horizontalArrangement = Arrangement.spacedBy(gridInterval),
|
||||||
contentPadding = contentPadding + PaddingValues(marginH, marginV),
|
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
count = data.itemCount,
|
count = data.itemCount,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -32,9 +31,9 @@ import androidx.compose.ui.res.stringArrayResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import arrow.core.raise.ensure
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.asMutableState
|
|
||||||
import com.hippo.ehviewer.client.EhTagDatabase
|
import com.hippo.ehviewer.client.EhTagDatabase
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANG_TAGS
|
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANG_TAGS
|
||||||
@@ -62,9 +61,11 @@ private val categoryTable = arrayOf(
|
|||||||
fun SearchFilter(
|
fun SearchFilter(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
category: Int,
|
category: Int,
|
||||||
onCategoryChanged: (Int) -> Unit,
|
onCategoryChange: (Int) -> Unit,
|
||||||
|
language: Int,
|
||||||
|
onLanguageChange: (Int) -> Unit,
|
||||||
advancedOption: AdvancedSearchOption,
|
advancedOption: AdvancedSearchOption,
|
||||||
onAdvancedOptionChanged: (AdvancedSearchOption) -> Unit,
|
onAdvancedOptionChange: (AdvancedSearchOption) -> Unit,
|
||||||
) = Column(modifier) {
|
) = Column(modifier) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -82,7 +83,7 @@ fun SearchFilter(
|
|||||||
items(categories, { it.first }) {
|
items(categories, { it.first }) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = isCategoryChecked(it.first),
|
selected = isCategoryChecked(it.first),
|
||||||
onClick = { onCategoryChanged(category xor it.first) },
|
onClick = { onCategoryChange(category xor it.first) },
|
||||||
label = { Text(text = stringResource(id = it.second)) },
|
label = { Text(text = stringResource(id = it.second)) },
|
||||||
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
||||||
)
|
)
|
||||||
@@ -109,14 +110,11 @@ fun SearchFilter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var languageFilter by Settings.languageFilter.asMutableState()
|
|
||||||
DropdownFilterChip(
|
DropdownFilterChip(
|
||||||
label = languageStr,
|
label = languageStr,
|
||||||
menuItems = languages,
|
menuItems = languages,
|
||||||
selectedItemIndex = languageFilter + 1,
|
selectedItemIndex = language + 1,
|
||||||
onSelectedItemIndexChange = {
|
onSelectedItemIndexChange = { onLanguageChange(it - 1) },
|
||||||
languageFilter = it - 1
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
val minRatingItems = stringArrayResource(id = R.array.search_min_rating)
|
val minRatingItems = stringArrayResource(id = R.array.search_min_rating)
|
||||||
val minRatingStr = stringResource(id = R.string.search_sr)
|
val minRatingStr = stringResource(id = R.string.search_sr)
|
||||||
@@ -125,7 +123,7 @@ fun SearchFilter(
|
|||||||
menuItems = minRatingItems.asList(),
|
menuItems = minRatingItems.asList(),
|
||||||
selectedItemIndex = (advancedOption.minRating - 1).coerceAtLeast(0),
|
selectedItemIndex = (advancedOption.minRating - 1).coerceAtLeast(0),
|
||||||
onSelectedItemIndexChange = {
|
onSelectedItemIndexChange = {
|
||||||
onAdvancedOptionChanged(advancedOption.copy(minRating = if (it == 0) 0 else it + 1))
|
onAdvancedOptionChange(advancedOption.copy(minRating = if (it == 0) 0 else it + 1))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val pageErr1 = stringResource(R.string.search_sp_err1)
|
val pageErr1 = stringResource(R.string.search_sp_err1)
|
||||||
@@ -153,13 +151,13 @@ fun SearchFilter(
|
|||||||
val (from, to) = dialogState.awaitResult(
|
val (from, to) = dialogState.awaitResult(
|
||||||
initial = advancedOption.fromPage to advancedOption.toPage,
|
initial = advancedOption.fromPage to advancedOption.toPage,
|
||||||
title = R.string.key_pages,
|
title = R.string.key_pages,
|
||||||
invalidator = { (from, to) ->
|
invalidator = { (min, _max) ->
|
||||||
if (to != 0 && to < 10) {
|
// 0 means max pages not filled, though +inf
|
||||||
pageErr1
|
val max = if (_max == 0) Int.MAX_VALUE else _max
|
||||||
} else if (from != 0 && to != 0 && to - from < 20) {
|
if (min != 0) {
|
||||||
pageErr2
|
ensure(max - min >= 20) { pageErr2 }
|
||||||
} else {
|
} else {
|
||||||
null
|
ensure(max >= 10) { pageErr1 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { error ->
|
) { error ->
|
||||||
@@ -202,13 +200,13 @@ fun SearchFilter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAdvancedOptionChanged(advancedOption.copy(fromPage = from, toPage = to))
|
onAdvancedOptionChange(advancedOption.copy(fromPage = from, toPage = to))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = { Text(text = pagesText) },
|
label = { Text(text = pagesText) },
|
||||||
)
|
)
|
||||||
fun checked(bit: Int) = advancedOption.advanceSearch and bit != 0
|
fun checked(bit: Int) = advancedOption.advanceSearch and bit != 0
|
||||||
fun AdvancedSearchOption.inv(bit: Int) = onAdvancedOptionChanged(copy(advanceSearch = advanceSearch xor bit))
|
fun AdvancedSearchOption.inv(bit: Int) = onAdvancedOptionChange(copy(advanceSearch = advanceSearch xor bit))
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = checked(AdvanceTable.SH),
|
selected = checked(AdvanceTable.SH),
|
||||||
onClick = { advancedOption.inv(AdvanceTable.SH) },
|
onClick = { advancedOption.inv(AdvanceTable.SH) },
|
||||||
|
|||||||
@@ -20,23 +20,25 @@ import com.hippo.ehviewer.gallery.PageLoader2
|
|||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.awaitActivityResult
|
import com.hippo.ehviewer.util.awaitActivityResult
|
||||||
|
import com.hippo.ehviewer.util.displayPath
|
||||||
import com.hippo.ehviewer.util.isAtLeastQ
|
import com.hippo.ehviewer.util.isAtLeastQ
|
||||||
import com.hippo.ehviewer.util.isAtLeastT
|
import com.hippo.ehviewer.util.isAtLeastT
|
||||||
import com.hippo.ehviewer.util.requestPermission
|
import com.hippo.ehviewer.util.requestPermission
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.files.toOkioPath
|
||||||
import com.hippo.unifile.displayPath
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
import splitties.systemservices.clipboardManager
|
import splitties.systemservices.clipboardManager
|
||||||
|
|
||||||
context(PageLoader2)
|
context(PageLoader2)
|
||||||
private fun Context.provideImage(index: Int): Uri? {
|
private fun Context.provideImage(index: Int): Uri? {
|
||||||
val dir = AppConfig.externalTempDir ?: return null
|
val dir = AppConfig.externalTempDir ?: return null
|
||||||
val name = saveToDir(index, dir.asUniFile())?.name ?: return null
|
val name = getImageFilename(index) ?: return null
|
||||||
return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", File(dir, name))
|
val file = File(dir, name).takeIf { save(index, it.toOkioPath()) } ?: return null
|
||||||
|
return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", file)
|
||||||
}
|
}
|
||||||
|
|
||||||
context(SnackbarHostState, Context, PageLoader2)
|
context(SnackbarHostState, Context, PageLoader2)
|
||||||
@@ -95,7 +97,7 @@ suspend fun save(page: ReaderPage) {
|
|||||||
val values = ContentValues()
|
val values = ContentValues()
|
||||||
val realPath: String
|
val realPath: String
|
||||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||||
values.put(MediaStore.Images.Media.DATE_ADDED, Clock.System.now().toEpochMilliseconds())
|
values.put(MediaStore.MediaColumns.DATE_ADDED, Clock.System.now().epochSeconds)
|
||||||
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||||
if (isAtLeastQ) {
|
if (isAtLeastQ) {
|
||||||
realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME
|
realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME
|
||||||
@@ -113,7 +115,7 @@ suspend fun save(page: ReaderPage) {
|
|||||||
}
|
}
|
||||||
val imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
val imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||||
if (imageUri != null) {
|
if (imageUri != null) {
|
||||||
if (!save(page.index, imageUri.asUniFile())) {
|
if (!save(page.index, imageUri.toOkioPath())) {
|
||||||
try {
|
try {
|
||||||
contentResolver.delete(imageUri, null, null)
|
contentResolver.delete(imageUri, null, null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -146,7 +148,7 @@ suspend fun saveTo(page: ReaderPage) {
|
|||||||
page.runSuspendCatching {
|
page.runSuspendCatching {
|
||||||
val uri = awaitActivityResult(ActivityResultContracts.CreateDocument(mimeType), filename)
|
val uri = awaitActivityResult(ActivityResultContracts.CreateDocument(mimeType), filename)
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
save(index, uri.asUniFile())
|
save(index, uri.toOkioPath())
|
||||||
showSnackbar(getString(R.string.image_saved, uri.displayPath))
|
showSnackbar(getString(R.string.image_saved, uri.displayPath))
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
|||||||
@@ -98,10 +98,8 @@ fun rememberSliderPagerDoubleSyncState(
|
|||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
pageLoader: PageLoader2,
|
pageLoader: PageLoader2,
|
||||||
): SliderPagerDoubleSync {
|
): SliderPagerDoubleSync = remember {
|
||||||
return remember {
|
SliderPagerDoubleSync(lazyListState, pagerState, pageLoader)
|
||||||
SliderPagerDoubleSync(lazyListState, pagerState, pageLoader)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun smoothScroll(distance: Int) =
|
private fun smoothScroll(distance: Int) =
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ data class DownloadsFilterState(
|
|||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
fun DownloadsFilterState.take(info: DownloadInfo) =
|
fun DownloadsFilterState.take(info: DownloadInfo) =
|
||||||
mode.take(info, label) && (state == -1 || info.state == state) &&
|
mode.take(info, label) &&
|
||||||
|
(state == -1 || info.state == state) &&
|
||||||
with(info) {
|
with(info) {
|
||||||
title.containsIgnoreCase(keyword) || titleJpn.containsIgnoreCase(keyword) ||
|
title.containsIgnoreCase(keyword) ||
|
||||||
|
titleJpn.containsIgnoreCase(keyword) ||
|
||||||
simpleTags?.any { it.containsIgnoreCase(keyword) } ?: false
|
simpleTags?.any { it.containsIgnoreCase(keyword) } ?: false
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,6 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -76,7 +75,6 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -153,7 +151,6 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(implicit<Context>()) && EhTagDatabase.initialized
|
val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(implicit<Context>()) && EhTagDatabase.initialized
|
||||||
val ehTags = EhTagDatabase.takeIf { canTranslate }
|
val ehTags = EhTagDatabase.takeIf { canTranslate }
|
||||||
fun String.translateArtist() = ehTags?.getTranslation(TagNamespace.Artist.toPrefix(), this) ?: this
|
fun String.translateArtist() = ehTags?.getTranslation(TagNamespace.Artist.toPrefix(), this) ?: this
|
||||||
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
|
|
||||||
val allName = stringResource(R.string.download_all)
|
val allName = stringResource(R.string.download_all)
|
||||||
val defaultName = stringResource(R.string.default_download_label_name)
|
val defaultName = stringResource(R.string.default_download_label_name)
|
||||||
val unknownName = stringResource(R.string.unknown_artists)
|
val unknownName = stringResource(R.string.unknown_artists)
|
||||||
@@ -172,8 +169,6 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
val hint = stringResource(R.string.search_bar_hint, title)
|
val hint = stringResource(R.string.search_bar_hint, title)
|
||||||
val sortModes = stringArrayResource(id = R.array.download_sort_modes)
|
|
||||||
val downloadStates = stringArrayResource(id = R.array.download_state)
|
|
||||||
val list = remember(filterState, invalidateKey) {
|
val list = remember(filterState, invalidateKey) {
|
||||||
downloadInfoList.filterTo(mutableStateListOf()) { info ->
|
downloadInfoList.filterTo(mutableStateListOf()) { info ->
|
||||||
filterState.take(info)
|
filterState.take(info)
|
||||||
@@ -227,10 +222,9 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
launch {
|
launch {
|
||||||
val text = awaitInputText(title = newLabel, hint = labelsStr) { text ->
|
val text = awaitInputText(title = newLabel, hint = labelsStr) { text ->
|
||||||
when {
|
when {
|
||||||
text.isBlank() -> labelEmpty
|
text.isBlank() -> raise(labelEmpty)
|
||||||
text == defaultName -> defaultInvalid
|
text == defaultName -> raise(defaultInvalid)
|
||||||
DownloadManager.containLabel(text) -> labelExists
|
DownloadManager.containLabel(text) -> raise(labelExists)
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DownloadManager.addLabel(text)
|
DownloadManager.addLabel(text)
|
||||||
@@ -337,7 +331,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
val index by rememberUpdatedState(itemIndex)
|
val index by rememberUpdatedState(itemIndex)
|
||||||
val item by rememberUpdatedState(label)
|
val item by rememberUpdatedState(label)
|
||||||
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
||||||
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
|
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density) }
|
||||||
LaunchedEffect(dismissState) {
|
LaunchedEffect(dismissState) {
|
||||||
snapshotFlow { dismissState.currentValue }.collect {
|
snapshotFlow { dismissState.currentValue }.collect {
|
||||||
if (it == SwipeToDismissBoxValue.EndToStart) {
|
if (it == SwipeToDismissBoxValue.EndToStart) {
|
||||||
@@ -357,7 +351,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ReorderableItem(reorderableLabelState, enabled = editEnable, key = id) { isDragging ->
|
ReorderableItem(reorderableLabelState, id, enabled = editEnable, animated = animateItems) { isDragging ->
|
||||||
SwipeToDismissBox(
|
SwipeToDismissBox(
|
||||||
state = dismissState,
|
state = dismissState,
|
||||||
backgroundContent = {},
|
backgroundContent = {},
|
||||||
@@ -390,10 +384,9 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
launch {
|
launch {
|
||||||
val new = awaitInputText(initial = item, title = renameLabel, hint = labelsStr) { text ->
|
val new = awaitInputText(initial = item, title = renameLabel, hint = labelsStr) { text ->
|
||||||
when {
|
when {
|
||||||
text.isBlank() -> labelEmpty
|
text.isBlank() -> raise(labelEmpty)
|
||||||
text == defaultName -> defaultInvalid
|
text == defaultName -> raise(defaultInvalid)
|
||||||
DownloadManager.containLabel(text) -> labelExists
|
DownloadManager.containLabel(text) -> raise(labelExists)
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DownloadManager.renameLabel(item, new)
|
DownloadManager.renameLabel(item, new)
|
||||||
@@ -544,9 +537,9 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
FastScrollLazyVerticalStaggeredGrid(
|
FastScrollLazyVerticalStaggeredGrid(
|
||||||
columns = StaggeredGridCells.Fixed(thumbColumns),
|
columns = StaggeredGridCells.Fixed(thumbColumns),
|
||||||
modifier = Modifier.nestedScroll(searchBarConnection).fillMaxSize(),
|
modifier = Modifier.nestedScroll(searchBarConnection).fillMaxSize(),
|
||||||
|
contentPadding = realPadding,
|
||||||
verticalItemSpacing = gridInterval,
|
verticalItemSpacing = gridInterval,
|
||||||
horizontalArrangement = Arrangement.spacedBy(gridInterval),
|
horizontalArrangement = Arrangement.spacedBy(gridInterval),
|
||||||
contentPadding = realPadding,
|
|
||||||
) {
|
) {
|
||||||
items(list, key = { it.gid }) { info ->
|
items(list, key = { it.gid }) { info ->
|
||||||
GalleryInfoGridItem(
|
GalleryInfoGridItem(
|
||||||
@@ -653,8 +646,9 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}
|
}
|
||||||
onClick(Icons.AutoMirrored.Default.Sort) {
|
onClick(Icons.AutoMirrored.Default.Sort) {
|
||||||
val oldMode = SortMode.from(sortMode)
|
val oldMode = SortMode.from(sortMode)
|
||||||
|
val sortModes = resources.getStringArray(R.array.download_sort_modes).toList()
|
||||||
val (selected, checked) = awaitSelectItemWithCheckBox(
|
val (selected, checked) = awaitSelectItemWithCheckBox(
|
||||||
sortModes.toList(),
|
sortModes,
|
||||||
R.string.sort_by,
|
R.string.sort_by,
|
||||||
R.string.group_by_download_label,
|
R.string.group_by_download_label,
|
||||||
SortMode.All.indexOfFirst { it.field == oldMode.field && it.order == oldMode.order },
|
SortMode.All.indexOfFirst { it.field == oldMode.field && it.order == oldMode.order },
|
||||||
@@ -666,8 +660,9 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
invalidateKey = !invalidateKey
|
invalidateKey = !invalidateKey
|
||||||
}
|
}
|
||||||
onClick(Icons.Default.FilterList) {
|
onClick(Icons.Default.FilterList) {
|
||||||
|
val downloadStates = resources.getStringArray(R.array.download_state).toList()
|
||||||
val state = awaitSingleChoice(
|
val state = awaitSingleChoice(
|
||||||
downloadStates.toList(),
|
downloadStates,
|
||||||
filterState.state + 1,
|
filterState.state + 1,
|
||||||
R.string.download_filter,
|
R.string.download_filter,
|
||||||
) - 1
|
) - 1
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ import com.hippo.ehviewer.util.displayString
|
|||||||
import com.hippo.ehviewer.util.findActivity
|
import com.hippo.ehviewer.util.findActivity
|
||||||
import com.hippo.ehviewer.util.isAtLeastQ
|
import com.hippo.ehviewer.util.isAtLeastQ
|
||||||
import com.hippo.ehviewer.util.requestPermission
|
import com.hippo.ehviewer.util.requestPermission
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.files.delete
|
||||||
|
import com.hippo.files.toOkioPath
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
@@ -184,21 +185,19 @@ data class TokenArgs(
|
|||||||
) : GalleryDetailScreenArgs
|
) : GalleryDetailScreenArgs
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
private fun getRatingText(rating: Float): Int {
|
private fun getRatingText(rating: Float): Int = when ((rating * 2).roundToInt()) {
|
||||||
return when ((rating * 2).roundToInt()) {
|
0 -> R.string.rating0
|
||||||
0 -> R.string.rating0
|
1 -> R.string.rating1
|
||||||
1 -> R.string.rating1
|
2 -> R.string.rating2
|
||||||
2 -> R.string.rating2
|
3 -> R.string.rating3
|
||||||
3 -> R.string.rating3
|
4 -> R.string.rating4
|
||||||
4 -> R.string.rating4
|
5 -> R.string.rating5
|
||||||
5 -> R.string.rating5
|
6 -> R.string.rating6
|
||||||
6 -> R.string.rating6
|
7 -> R.string.rating7
|
||||||
7 -> R.string.rating7
|
8 -> R.string.rating8
|
||||||
8 -> R.string.rating8
|
9 -> R.string.rating9
|
||||||
9 -> R.string.rating9
|
10 -> R.string.rating10
|
||||||
10 -> R.string.rating10
|
else -> R.string.rating_none
|
||||||
else -> R.string.rating_none
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<GalleryTagGroup>.getArtistTag(): String? {
|
private fun List<GalleryTagGroup>.getArtistTag(): String? {
|
||||||
@@ -588,7 +587,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
AppConfig.APP_DIRNAME + "/" + FileUtils.sanitizeFilename(name),
|
AppConfig.APP_DIRNAME + "/" + FileUtils.sanitizeFilename(name),
|
||||||
)
|
)
|
||||||
r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
r.addRequestHeader("Cookie", EhCookieStore.getCookieHeader(url))
|
EhCookieStore.getCookieHeader(url)?.let { r.addRequestHeader("Cookie", it) }
|
||||||
downloadManager.enqueue(r)
|
downloadManager.enqueue(r)
|
||||||
showSnackbar(downloadTorrentStarted)
|
showSnackbar(downloadTorrentStarted)
|
||||||
}
|
}
|
||||||
@@ -618,14 +617,12 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.size(keylineMargin))
|
Spacer(modifier = Modifier.size(keylineMargin))
|
||||||
fun getAllRatingText(rating: Float, ratingCount: Int): String {
|
fun getAllRatingText(rating: Float, ratingCount: Int): String = getString(
|
||||||
return getString(
|
R.string.rating_text,
|
||||||
R.string.rating_text,
|
getString(getRatingText(rating)),
|
||||||
getString(getRatingText(rating)),
|
rating,
|
||||||
rating,
|
ratingCount,
|
||||||
ratingCount,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
var ratingText by rememberSaveable {
|
var ratingText by rememberSaveable {
|
||||||
mutableStateOf(getAllRatingText(galleryDetail.rating, galleryDetail.ratingCount))
|
mutableStateOf(getAllRatingText(galleryDetail.rating, galleryDetail.ratingCount))
|
||||||
}
|
}
|
||||||
@@ -1079,7 +1076,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
)
|
)
|
||||||
val dirname = downloadInfo?.dirname
|
val dirname = downloadInfo?.dirname
|
||||||
if (uri != null && dirname != null) {
|
if (uri != null && dirname != null) {
|
||||||
val file = uri.asUniFile()
|
val file = uri.toOkioPath()
|
||||||
val msg = runCatching {
|
val msg = runCatching {
|
||||||
bgWork {
|
bgWork {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.hippo.ehviewer.ui.screen
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.ViewConfiguration
|
import android.view.ViewConfiguration
|
||||||
|
import androidx.activity.compose.ReportDrawnWhen
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
@@ -39,12 +40,10 @@ import androidx.compose.material.icons.filled.Shuffle
|
|||||||
import androidx.compose.material.icons.outlined.Bookmarks
|
import androidx.compose.material.icons.outlined.Bookmarks
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -77,16 +76,18 @@ import androidx.compose.ui.res.stringArrayResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastAny
|
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import arrow.core.raise.ensure
|
||||||
|
import arrow.core.raise.ensureNotNull
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
@@ -134,8 +135,8 @@ import com.hippo.ehviewer.ui.tools.rememberMutableStateInDataStore
|
|||||||
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
||||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||||
import com.hippo.ehviewer.util.pickVisualMedia
|
import com.hippo.ehviewer.util.pickVisualMedia
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.ehviewer.util.sha1
|
||||||
import com.hippo.unifile.sha1
|
import com.hippo.files.toOkioPath
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
@@ -176,6 +177,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
|
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
||||||
var showSearchLayout by rememberSaveable { mutableStateOf(false) }
|
var showSearchLayout by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val animateItems by Settings.animateItems.collectAsState()
|
||||||
|
|
||||||
var category by rememberMutableStateInDataStore("SearchCategory") { EhUtils.ALL_CATEGORY }
|
var category by rememberMutableStateInDataStore("SearchCategory") { EhUtils.ALL_CATEGORY }
|
||||||
var advancedSearchOption by rememberMutableStateInDataStore("AdvancedSearchOption") { AdvancedSearchOption() }
|
var advancedSearchOption by rememberMutableStateInDataStore("AdvancedSearchOption") { AdvancedSearchOption() }
|
||||||
@@ -202,7 +204,6 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
onBack = { showSearchLayout = false },
|
onBack = { showSearchLayout = false },
|
||||||
)
|
)
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
|
|
||||||
val listState = rememberLazyGridState()
|
val listState = rememberLazyGridState()
|
||||||
val gridState = rememberLazyStaggeredGridState()
|
val gridState = rememberLazyStaggeredGridState()
|
||||||
val isTopList = remember(urlBuilder) { urlBuilder.mode == MODE_TOPLIST }
|
val isTopList = remember(urlBuilder) { urlBuilder.mode == MODE_TOPLIST }
|
||||||
@@ -253,6 +254,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
}
|
}
|
||||||
}.flow.cachedIn(viewModelScope)
|
}.flow.cachedIn(viewModelScope)
|
||||||
}.collectAsLazyPagingItems()
|
}.collectAsLazyPagingItems()
|
||||||
|
ReportDrawnWhen { data.loadState.refresh !is LoadState.Loading }
|
||||||
FavouriteStatusRouter.Observe(data)
|
FavouriteStatusRouter.Observe(data)
|
||||||
val listMode by Settings.listMode.collectAsState()
|
val listMode by Settings.listMode.collectAsState()
|
||||||
|
|
||||||
@@ -262,6 +264,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
val toplists = remember { entries zip values }
|
val toplists = remember { entries zip values }
|
||||||
val quickSearchName = getSuitableTitleForUrlBuilder(urlBuilder, false)
|
val quickSearchName = getSuitableTitleForUrlBuilder(urlBuilder, false)
|
||||||
var saveProgress by Settings.qSSaveProgress.asMutableState()
|
var saveProgress by Settings.qSSaveProgress.asMutableState()
|
||||||
|
var languageFilter by Settings.languageFilter.asMutableState()
|
||||||
|
|
||||||
fun getFirstVisibleItemIndex() = if (listMode == 0) {
|
fun getFirstVisibleItemIndex() = if (listMode == 0) {
|
||||||
listState.firstVisibleItemIndex
|
listState.firstVisibleItemIndex
|
||||||
@@ -340,20 +343,15 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
checkBoxText = R.string.save_progress,
|
checkBoxText = R.string.save_progress,
|
||||||
) { input, checked ->
|
) { input, checked ->
|
||||||
var text = input.trim()
|
var text = input.trim()
|
||||||
nameEmpty.takeIf {
|
ensure(text.isNotBlank()) { nameEmpty }
|
||||||
text.isEmpty()
|
if (checked) text += "@$next"
|
||||||
} ?: dupName.takeIf {
|
ensure(quickSearchList.none { it.name == text }) { dupName }
|
||||||
if (checked) text += "@$next"
|
val quickSearch = urlBuilder.toQuickSearch(text)
|
||||||
quickSearchList.fastAny { it.name == text }
|
quickSearch.position = quickSearchList.size
|
||||||
} ?: run {
|
// Insert to DB first to update the id
|
||||||
val quickSearch = urlBuilder.toQuickSearch(text)
|
EhDB.insertQuickSearch(quickSearch)
|
||||||
quickSearch.position = quickSearchList.size
|
quickSearchList.add(quickSearch)
|
||||||
// Insert to DB first to update the id
|
saveProgress = checked
|
||||||
EhDB.insertQuickSearch(quickSearch)
|
|
||||||
quickSearchList.add(quickSearch)
|
|
||||||
saveProgress = checked
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,9 +368,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
val quickSearchListState = rememberLazyListState()
|
val quickSearchListState = rememberLazyListState()
|
||||||
val hapticFeedback = rememberHapticFeedback()
|
val hapticFeedback = rememberHapticFeedback()
|
||||||
val reorderableLazyListState = rememberReorderableLazyListState(quickSearchListState) { from, to ->
|
val reorderableLazyListState = rememberReorderableLazyListState(quickSearchListState) { from, to ->
|
||||||
val fromIndex = from.index - 1
|
quickSearchList.apply { add(to.index, removeAt(from.index)) }
|
||||||
val toIndex = to.index - 1
|
|
||||||
quickSearchList.apply { add(toIndex, removeAt(fromIndex)) }
|
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
|
||||||
}
|
}
|
||||||
var fromIndex by remember { mutableIntStateOf(-1) }
|
var fromIndex by remember { mutableIntStateOf(-1) }
|
||||||
@@ -381,15 +377,11 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
state = quickSearchListState,
|
state = quickSearchListState,
|
||||||
contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
|
contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
|
||||||
) {
|
) {
|
||||||
// Fix the first item's reorder animation
|
|
||||||
stickyHeader {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
itemsIndexed(quickSearchList, key = { _, item -> item.id!! }) { itemIndex, item ->
|
itemsIndexed(quickSearchList, key = { _, item -> item.id!! }) { itemIndex, item ->
|
||||||
val index by rememberUpdatedState(itemIndex)
|
val index by rememberUpdatedState(itemIndex)
|
||||||
ReorderableItem(reorderableLazyListState, key = item.id!!) { isDragging ->
|
ReorderableItem(reorderableLazyListState, item.id!!, animated = animateItems) { isDragging ->
|
||||||
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
||||||
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
|
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density) }
|
||||||
LaunchedEffect(dismissState) {
|
LaunchedEffect(dismissState) {
|
||||||
snapshotFlow { dismissState.currentValue }.collect { value ->
|
snapshotFlow { dismissState.currentValue }.collect { value ->
|
||||||
if (value == SwipeToDismissBoxValue.EndToStart) {
|
if (value == SwipeToDismissBoxValue.EndToStart) {
|
||||||
@@ -427,10 +419,14 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
if (urlBuilder.mode == MODE_WHATS_HOT) {
|
if (urlBuilder.mode == MODE_WHATS_HOT) {
|
||||||
val builder = ListUrlBuilder(item)
|
val builder = ListUrlBuilder(item).apply {
|
||||||
|
language = languageFilter
|
||||||
|
}
|
||||||
navigator.navigate(builder.asDst())
|
navigator.navigate(builder.asDst())
|
||||||
} else {
|
} else {
|
||||||
urlBuilder = ListUrlBuilder(item)
|
urlBuilder = ListUrlBuilder(item).apply {
|
||||||
|
language = languageFilter
|
||||||
|
}
|
||||||
data.refresh()
|
data.refresh()
|
||||||
}
|
}
|
||||||
showSearchLayout = false
|
showSearchLayout = false
|
||||||
@@ -515,6 +511,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
builder.mode = newMode
|
builder.mode = newMode
|
||||||
builder.keyword = query
|
builder.keyword = query
|
||||||
builder.category = category
|
builder.category = category
|
||||||
|
builder.language = languageFilter
|
||||||
builder.advanceSearch = advancedSearchOption.advanceSearch
|
builder.advanceSearch = advancedSearchOption.advanceSearch
|
||||||
builder.minRating = advancedSearchOption.minRating
|
builder.minRating = advancedSearchOption.minRating
|
||||||
builder.pageFrom = advancedSearchOption.fromPage
|
builder.pageFrom = advancedSearchOption.fromPage
|
||||||
@@ -525,7 +522,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
return@launchIO
|
return@launchIO
|
||||||
}
|
}
|
||||||
builder.mode = MODE_IMAGE_SEARCH
|
builder.mode = MODE_IMAGE_SEARCH
|
||||||
builder.hash = imageUri!!.asUniFile().sha1()
|
builder.hash = imageUri!!.toOkioPath().sha1()
|
||||||
}
|
}
|
||||||
when (oldMode) {
|
when (oldMode) {
|
||||||
MODE_TOPLIST, MODE_WHATS_HOT -> {
|
MODE_TOPLIST, MODE_WHATS_HOT -> {
|
||||||
@@ -578,11 +575,11 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
filter = {
|
filter = {
|
||||||
SearchFilter(
|
SearchFilter(
|
||||||
category = category,
|
category = category,
|
||||||
onCategoryChanged = { category = it },
|
onCategoryChange = { category = it },
|
||||||
|
language = languageFilter,
|
||||||
|
onLanguageChange = { languageFilter = it },
|
||||||
advancedOption = advancedSearchOption,
|
advancedOption = advancedSearchOption,
|
||||||
onAdvancedOptionChanged = {
|
onAdvancedOptionChange = { advancedSearchOption = it },
|
||||||
advancedSearchOption = it
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -735,11 +732,8 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
val page = urlBuilder.mJumpTo?.toIntOrNull() ?: 0
|
val page = urlBuilder.mJumpTo?.toIntOrNull() ?: 0
|
||||||
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
|
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
|
||||||
val text = awaitInputText(title = gotoTitle, hint = hint, isNumber = true) { oriText ->
|
val text = awaitInputText(title = gotoTitle, hint = hint, isNumber = true) { oriText ->
|
||||||
when (oriText.trim().toIntOrNull()?.let { it - 1 }) {
|
val goto = ensureNotNull(oriText.trim().toIntOrNull()) { invalidNum } - 1
|
||||||
null -> invalidNum
|
ensure(goto in 0..<TOPLIST_PAGES) { outOfRange }
|
||||||
!in 0..<TOPLIST_PAGES -> outOfRange
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}.trim().toInt() - 1
|
}.trim().toInt() - 1
|
||||||
urlBuilder.setJumpTo(text)
|
urlBuilder.setJumpTo(text)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ fun GalleryPreviewScreen(detail: GalleryDetail, toNextPage: Boolean, navigator:
|
|||||||
modifier = Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection).padding(horizontal = dimensionResource(id = R.dimen.gallery_list_margin_h)),
|
modifier = Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection).padding(horizontal = dimensionResource(id = R.dimen.gallery_list_margin_h)),
|
||||||
state = state,
|
state = state,
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
count = data.itemCount,
|
count = data.itemCount,
|
||||||
|
|||||||
@@ -227,12 +227,14 @@ fun SearchBarScreen(
|
|||||||
onExpandedChange = onExpandedChange,
|
onExpandedChange = onExpandedChange,
|
||||||
modifier = Modifier.widthIn(max = maxWidth - SearchBarHorizontalPadding * 2),
|
modifier = Modifier.widthIn(max = maxWidth - SearchBarHorizontalPadding * 2),
|
||||||
label = title.ifNotNullThen {
|
label = title.ifNotNullThen {
|
||||||
Text(title!!, overflow = TextOverflow.Ellipsis)
|
Text(title!!, overflow = TextOverflow.Ellipsis, maxLines = 1)
|
||||||
}.takeUnless {
|
}.takeUnless {
|
||||||
val contentActive by activeState.state
|
val contentActive by activeState.state
|
||||||
expanded || contentActive || searchFieldState.text.isNotEmpty()
|
expanded || contentActive || searchFieldState.text.isNotEmpty()
|
||||||
},
|
},
|
||||||
placeholder = searchFieldHint.ifNotNullThen { Text(searchFieldHint!!) },
|
placeholder = searchFieldHint.ifNotNullThen {
|
||||||
|
Text(searchFieldHint!!, overflow = TextOverflow.Ellipsis, maxLines = 1)
|
||||||
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
IconButton(onClick = { hideSearchView() }) {
|
IconButton(onClick = { hideSearchView() }) {
|
||||||
@@ -298,22 +300,20 @@ fun SearchBarScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
|
fun wrapTagKeyword(keyword: String, translate: Boolean = false): String = if (keyword.endsWith(':')) {
|
||||||
return if (keyword.endsWith(':')) {
|
keyword
|
||||||
keyword
|
} else {
|
||||||
|
val tag = keyword.substringAfter(':')
|
||||||
|
val prefix = keyword.dropLast(tag.length + 1)
|
||||||
|
if (translate) {
|
||||||
|
val namespacePrefix = TagNamespace(prefix).toPrefix()
|
||||||
|
val newPrefix = EhTagDatabase.getTranslation(tag = prefix) ?: prefix
|
||||||
|
val newTag = EhTagDatabase.getTranslation(namespacePrefix, tag) ?: tag
|
||||||
|
"$newPrefix:$newTag"
|
||||||
|
} else if (keyword.contains(' ')) {
|
||||||
|
"$prefix:\"$tag$\""
|
||||||
} else {
|
} else {
|
||||||
val tag = keyword.substringAfter(':')
|
"$keyword$"
|
||||||
val prefix = keyword.dropLast(tag.length + 1)
|
|
||||||
if (translate) {
|
|
||||||
val namespacePrefix = TagNamespace(prefix).toPrefix()
|
|
||||||
val newPrefix = EhTagDatabase.getTranslation(tag = prefix) ?: prefix
|
|
||||||
val newTag = EhTagDatabase.getTranslation(namespacePrefix, tag) ?: tag
|
|
||||||
"$newPrefix:$newTag"
|
|
||||||
} else if (keyword.contains(' ')) {
|
|
||||||
"$prefix:\"$tag$\""
|
|
||||||
} else {
|
|
||||||
"$keyword$"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,9 +154,7 @@ suspend fun DialogState.showNewVersion(context: Context, release: Release) {
|
|||||||
}
|
}
|
||||||
if (Settings.backupBeforeUpdate) {
|
if (Settings.backupBeforeUpdate) {
|
||||||
val time = ReadableTime.getFilenamableTime()
|
val time = ReadableTime.getFilenamableTime()
|
||||||
downloadLocation.createFile("$time.db")?.let {
|
EhDB.exportDB(context, (downloadLocation / "$time.db"))
|
||||||
EhDB.exportDB(context, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// TODO: Download in the background and show progress in notification
|
// TODO: Download in the background and show progress in notification
|
||||||
val file = File(AppConfig.tempDir, "update.apk").apply { delete() }
|
val file = File(AppConfig.tempDir, "update.apk").apply { delete() }
|
||||||
|
|||||||
@@ -44,11 +44,13 @@ import com.hippo.ehviewer.ui.tools.observed
|
|||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.Crash
|
import com.hippo.ehviewer.util.Crash
|
||||||
import com.hippo.ehviewer.util.ReadableTime
|
import com.hippo.ehviewer.util.ReadableTime
|
||||||
|
import com.hippo.ehviewer.util.displayPath
|
||||||
import com.hippo.ehviewer.util.getAppLanguage
|
import com.hippo.ehviewer.util.getAppLanguage
|
||||||
import com.hippo.ehviewer.util.getLanguages
|
import com.hippo.ehviewer.util.getLanguages
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastO
|
||||||
import com.hippo.ehviewer.util.isAtLeastV
|
import com.hippo.ehviewer.util.isAtLeastV
|
||||||
import com.hippo.ehviewer.util.setAppLanguage
|
import com.hippo.ehviewer.util.setAppLanguage
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.files.toOkioPath
|
||||||
import com.jamal.composeprefs3.ui.prefs.DropDownPref
|
import com.jamal.composeprefs3.ui.prefs.DropDownPref
|
||||||
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
@@ -129,7 +131,7 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
Crash.collectInfo(zipOs.writer())
|
Crash.collectInfo(zipOs.writer())
|
||||||
Runtime.getRuntime().exec("logcat -d").inputStream.use { it.copyTo(zipOs) }
|
Runtime.getRuntime().exec("logcat -d").inputStream.use { it.copyTo(zipOs) }
|
||||||
}
|
}
|
||||||
launchSnackBar(getString(R.string.settings_advanced_dump_logcat_to, uri.toString()))
|
launchSnackBar(getString(R.string.settings_advanced_dump_logcat_to, uri.displayPath))
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
launchSnackBar(dumpLogError)
|
launchSnackBar(dumpLogError)
|
||||||
@@ -155,6 +157,15 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
useSelectedAsSummary = true,
|
useSelectedAsSummary = true,
|
||||||
entries = languages,
|
entries = languages,
|
||||||
)
|
)
|
||||||
|
if (isAtLeastO) {
|
||||||
|
IntSliderPreference(
|
||||||
|
maxValue = 16384,
|
||||||
|
step = 3,
|
||||||
|
title = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold),
|
||||||
|
summary = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold_summary),
|
||||||
|
value = Settings::hardwareBitmapThreshold,
|
||||||
|
)
|
||||||
|
}
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = stringResource(id = R.string.preload_thumb_aggressively),
|
title = stringResource(id = R.string.preload_thumb_aggressively),
|
||||||
value = Settings::preloadThumbAggressively,
|
value = Settings::preloadThumbAggressively,
|
||||||
@@ -190,8 +201,8 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
uri?.let {
|
uri?.let {
|
||||||
context.runCatching {
|
context.runCatching {
|
||||||
grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
EhDB.exportDB(context, uri.asUniFile())
|
EhDB.exportDB(context, uri.toOkioPath())
|
||||||
launchSnackBar(getString(R.string.settings_advanced_export_data_to, uri.toString()))
|
launchSnackBar(getString(R.string.settings_advanced_export_data_to, uri.displayPath))
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
logcat(it)
|
logcat(it)
|
||||||
launchSnackBar(exportFailed)
|
launchSnackBar(exportFailed)
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package com.hippo.ehviewer.ui.settings
|
|||||||
|
|
||||||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -13,20 +16,16 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import arrow.fx.coroutines.parMap
|
import arrow.fx.coroutines.parMap
|
||||||
import arrow.fx.coroutines.parMapNotNull
|
import arrow.fx.coroutines.parMapNotNull
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
@@ -46,32 +45,36 @@ import com.hippo.ehviewer.spider.COMIC_INFO_FILE
|
|||||||
import com.hippo.ehviewer.spider.SpiderDen
|
import com.hippo.ehviewer.spider.SpiderDen
|
||||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.SPIDER_INFO_FILENAME
|
import com.hippo.ehviewer.spider.SpiderQueen.Companion.SPIDER_INFO_FILENAME
|
||||||
import com.hippo.ehviewer.spider.readComicInfo
|
import com.hippo.ehviewer.spider.readComicInfo
|
||||||
import com.hippo.ehviewer.spider.readCompatFromUniFile
|
import com.hippo.ehviewer.spider.readCompatFromPath
|
||||||
|
import com.hippo.ehviewer.ui.composing
|
||||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||||
import com.hippo.ehviewer.ui.tools.observed
|
import com.hippo.ehviewer.ui.tools.observed
|
||||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||||
|
import com.hippo.ehviewer.util.displayPath
|
||||||
import com.hippo.ehviewer.util.displayString
|
import com.hippo.ehviewer.util.displayString
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.files.delete
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.files.find
|
||||||
import com.hippo.unifile.displayPath
|
import com.hippo.files.isDirectory
|
||||||
|
import com.hippo.files.list
|
||||||
|
import com.hippo.files.metadataOrNull
|
||||||
|
import com.hippo.files.toOkioPath
|
||||||
|
import com.hippo.files.toUri
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
import okio.Path
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadScreen(navigator: DestinationsNavigator) {
|
fun DownloadScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
val context = LocalContext.current
|
fun launchSnackBar(content: String) = launch { showSnackbar(content) }
|
||||||
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -84,18 +87,19 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
|
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
|
||||||
var downloadLocationState by ::downloadLocation.observed
|
var downloadLocationState by ::downloadLocation.observed
|
||||||
val cannotGetDownloadLocation = stringResource(id = R.string.settings_download_cant_get_download_location)
|
val cannotGetDownloadLocation = stringResource(id = R.string.settings_download_cant_get_download_location)
|
||||||
val selectDownloadDirLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { treeUri ->
|
val selectDownloadDirLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { treeUri ->
|
||||||
treeUri?.run {
|
treeUri?.run {
|
||||||
coroutineScope.launch {
|
launchIO {
|
||||||
context.runCatching {
|
runCatching {
|
||||||
contentResolver.takePersistableUriPermission(treeUri, FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
|
contentResolver.takePersistableUriPermission(treeUri, FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
downloadLocationState = treeUri.asUniFile()
|
val path = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)).toOkioPath()
|
||||||
coroutineScope.launchNonCancellable {
|
check(path.isDirectory) { "$path is not a directory" }
|
||||||
|
downloadLocationState = path
|
||||||
|
launchNonCancellable {
|
||||||
keepNoMediaFileStatus(downloadLocationState)
|
keepNoMediaFileStatus(downloadLocationState)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
@@ -107,7 +111,7 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
Preference(
|
Preference(
|
||||||
title = stringResource(id = R.string.settings_download_download_location),
|
title = stringResource(id = R.string.settings_download_download_location),
|
||||||
summary = downloadLocationState.uri.displayPath,
|
summary = downloadLocationState.toUri().displayPath,
|
||||||
) {
|
) {
|
||||||
selectDownloadDirLauncher.launch(null)
|
selectDownloadDirLauncher.launch(null)
|
||||||
}
|
}
|
||||||
@@ -167,7 +171,7 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
summary = stringResource(id = R.string.settings_download_reload_metadata_summary),
|
summary = stringResource(id = R.string.settings_download_reload_metadata_summary),
|
||||||
) {
|
) {
|
||||||
fun DownloadInfo.isStable(): Boolean {
|
fun DownloadInfo.isStable(): Boolean {
|
||||||
val downloadTime = downloadDir?.findFile(COMIC_INFO_FILE)?.lastModified() ?: return false
|
val downloadTime = downloadDir?.resolve(COMIC_INFO_FILE)?.metadataOrNull()?.lastModifiedAtMillis ?: return false
|
||||||
val postedTime = posted?.let { ParserUtils.parseDate(it) } ?: return false
|
val postedTime = posted?.let { ParserUtils.parseDate(it) } ?: return false
|
||||||
// stable 30 days after posted
|
// stable 30 days after posted
|
||||||
val stableTime = postedTime + 30L * 24L * 60L * 60L * 1000L
|
val stableTime = postedTime + 30L * 24L * 60L * 60L * 1000L
|
||||||
@@ -183,10 +187,10 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
di.galleryInfo.also { SpiderDen(it, di.dirname!!).writeComicInfo(false) }
|
di.galleryInfo.also { SpiderDen(it, di.dirname!!).writeComicInfo(false) }
|
||||||
}
|
}
|
||||||
EhDB.updateGalleryInfo(toUpdate)
|
EhDB.updateGalleryInfo(toUpdate)
|
||||||
launchSnackBar(context.getString(R.string.settings_download_reload_metadata_successfully, toUpdate.size))
|
launchSnackBar(getString(R.string.settings_download_reload_metadata_successfully, toUpdate.size))
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
launchSnackBar(context.getString(R.string.settings_download_reload_metadata_failed, it.displayString()))
|
launchSnackBar(getString(R.string.settings_download_reload_metadata_failed, it.displayString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val restoreFailed = stringResource(id = R.string.settings_download_restore_failed)
|
val restoreFailed = stringResource(id = R.string.settings_download_restore_failed)
|
||||||
@@ -195,19 +199,19 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
summary = stringResource(id = R.string.settings_download_restore_download_items_summary),
|
summary = stringResource(id = R.string.settings_download_restore_download_items_summary),
|
||||||
) {
|
) {
|
||||||
var restoreDirCount = 0
|
var restoreDirCount = 0
|
||||||
suspend fun getRestoreItem(file: UniFile): RestoreItem? {
|
suspend fun getRestoreItem(file: Path): RestoreItem? {
|
||||||
if (!file.isDirectory) return null
|
if (!file.isDirectory) return null
|
||||||
return runSuspendCatching {
|
return runSuspendCatching {
|
||||||
val (gid, token) = file.findFile(SPIDER_INFO_FILENAME)?.let {
|
val (gid, token) = file.find(SPIDER_INFO_FILENAME)?.let {
|
||||||
readCompatFromUniFile(it)?.run {
|
readCompatFromPath(it)?.run {
|
||||||
GalleryDetailUrlParser.Result(gid, token)
|
GalleryDetailUrlParser.Result(gid, token)
|
||||||
}
|
}
|
||||||
} ?: file.findFile(COMIC_INFO_FILE)?.let {
|
} ?: file.find(COMIC_INFO_FILE)?.let {
|
||||||
readComicInfo(it)?.run {
|
readComicInfo(it)?.run {
|
||||||
GalleryDetailUrlParser.parse(web)
|
GalleryDetailUrlParser.parse(web)
|
||||||
}
|
}
|
||||||
} ?: return null
|
} ?: return null
|
||||||
val dirname = file.name ?: return null
|
val dirname = file.name
|
||||||
if (DownloadManager.containDownloadInfo(gid)) {
|
if (DownloadManager.containDownloadInfo(gid)) {
|
||||||
// Restore download dir to avoid redownload
|
// Restore download dir to avoid redownload
|
||||||
val dbdirname = EhDB.getDownloadDirname(gid)
|
val dbdirname = EhDB.getDownloadDirname(gid)
|
||||||
@@ -222,8 +226,8 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
logcat(it)
|
logcat(it)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
runCatching {
|
runSuspendCatching {
|
||||||
val result = downloadLocation.listFiles().parMapNotNull { getRestoreItem(it) }.also {
|
val result = downloadLocation.list().parMapNotNull { getRestoreItem(it) }.also {
|
||||||
fillGalleryListByApi(it, EhUrl.referer)
|
fillGalleryListByApi(it, EhUrl.referer)
|
||||||
}
|
}
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
@@ -247,25 +251,27 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
title = stringResource(id = R.string.settings_download_clean_redundancy),
|
title = stringResource(id = R.string.settings_download_clean_redundancy),
|
||||||
summary = stringResource(id = R.string.settings_download_clean_redundancy_summary),
|
summary = stringResource(id = R.string.settings_download_clean_redundancy_summary),
|
||||||
) {
|
) {
|
||||||
fun clearFile(file: UniFile): Boolean {
|
fun isRedundant(file: Path): Boolean {
|
||||||
var name = file.name ?: return false
|
if (!file.isDirectory) return false
|
||||||
val index = name.indexOf('-')
|
val name = file.name
|
||||||
if (index >= 0) {
|
val gid = name.substringBefore('-').toLongOrNull() ?: return false
|
||||||
name = name.substring(0, index)
|
return name != DownloadManager.getDownloadInfo(gid)?.dirname
|
||||||
}
|
|
||||||
val gid = name.toLongOrNull() ?: return false
|
|
||||||
if (DownloadManager.containDownloadInfo(gid)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
file.delete()
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
runSuspendCatching {
|
val list = downloadLocation.list().filter(::isRedundant)
|
||||||
val cnt = downloadLocation.listFiles().sumOf { clearFile(it).compareTo(false) }
|
if (list.isNotEmpty()) {
|
||||||
launchSnackBar(FINAL_CLEAR_REDUNDANCY_MSG(cnt))
|
awaitPermissionOrCancel(
|
||||||
}.onFailure {
|
confirmText = R.string.delete,
|
||||||
logcat(it)
|
title = R.string.settings_download_clean_redundancy,
|
||||||
|
) {
|
||||||
|
LazyColumn {
|
||||||
|
items(list) {
|
||||||
|
Text(it.name, modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
val cnt = list.count { runCatching { it.delete() }.getOrNull() != null }
|
||||||
|
launchSnackBar(FINAL_CLEAR_REDUNDANCY_MSG(cnt))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ import androidx.compose.material.icons.filled.Info
|
|||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.MenuAnchorType
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -127,7 +127,7 @@ fun FilterScreen(navigator: DestinationsNavigator) {
|
|||||||
onExpandedChange = { expanded = !expanded },
|
onExpandedChange = { expanded = !expanded },
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable),
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
value = type,
|
value = type,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
package com.hippo.ehviewer.ui.theme
|
package com.hippo.ehviewer.ui.theme
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.scrollbar.LocalScrollbarStyle
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.collectAsState
|
import com.hippo.ehviewer.collectAsState
|
||||||
|
import com.hippo.ehviewer.ui.tools.scrollbarStyle
|
||||||
import com.hippo.ehviewer.util.isAtLeastS
|
import com.hippo.ehviewer.util.isAtLeastS
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -44,8 +48,12 @@ fun EhTheme(content: @Composable () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(colorScheme = colors) {
|
||||||
colorScheme = colors,
|
val scrollbarStyle = scrollbarStyle(color = MaterialTheme.colorScheme.primary)
|
||||||
content = content,
|
CompositionLocalProvider(
|
||||||
)
|
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
||||||
|
LocalScrollbarStyle provides scrollbarStyle,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -129,8 +129,9 @@ fun AnnotatedString.toBBCode() = buildString {
|
|||||||
val stack = ArrayDeque<AnnotatedString.Range<SpanStyle>>()
|
val stack = ArrayDeque<AnnotatedString.Range<SpanStyle>>()
|
||||||
val spans = spanStyles.groupBy { it.start }
|
val spans = spanStyles.groupBy { it.start }
|
||||||
while (true) {
|
while (true) {
|
||||||
while (stack.lastOrNull()?.end == current)
|
while (stack.lastOrNull()?.end == current) {
|
||||||
stack.removeLast().item.pop()
|
stack.removeLast().item.pop()
|
||||||
|
}
|
||||||
if (current == len) break
|
if (current == len) break
|
||||||
spans[current]?.sortedByDescending { it.end }?.forEach {
|
spans[current]?.sortedByDescending { it.end }?.forEach {
|
||||||
it.item.push()
|
it.item.push()
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.compose.ui.window.SecureFlagPolicy
|
import androidx.compose.ui.window.SecureFlagPolicy
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
|
import arrow.core.raise.Raise
|
||||||
|
import arrow.core.raise.either
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||||
import com.jamal.composeprefs3.ui.ifTrueThen
|
import com.jamal.composeprefs3.ui.ifTrueThen
|
||||||
@@ -109,45 +111,43 @@ class DialogState {
|
|||||||
suspend fun <R> awaitResult(
|
suspend fun <R> awaitResult(
|
||||||
initial: R,
|
initial: R,
|
||||||
@StringRes title: Int? = null,
|
@StringRes title: Int? = null,
|
||||||
invalidator: (suspend (R) -> String?)? = null,
|
invalidator: (suspend Raise<String>.(R) -> Unit)? = null,
|
||||||
block: @Composable DialogScope<R>.(String?) -> Unit,
|
block: @Composable DialogScope<R>.(String?) -> Unit,
|
||||||
): R {
|
): R = dialog { cont ->
|
||||||
return dialog { cont ->
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val state = remember(cont) { mutableStateOf(initial) }
|
||||||
val state = remember(cont) { mutableStateOf(initial) }
|
var errorMsg by remember(cont) { mutableStateOf<String?>(null) }
|
||||||
var error by remember(cont) { mutableStateOf<String?>(null) }
|
val impl = remember(cont) {
|
||||||
val impl = remember(cont) {
|
object : DialogScope<R> {
|
||||||
object : DialogScope<R> {
|
override var expectedValue by state
|
||||||
override var expectedValue by state
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { cont.cancel() },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
if (invalidator == null) {
|
|
||||||
cont.resume(state.value)
|
|
||||||
} else {
|
|
||||||
coroutineScope.launch {
|
|
||||||
error = invalidator.invoke(state.value)
|
|
||||||
error ?: cont.resume(state.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Text(text = stringResource(id = android.R.string.ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
cont.cancel()
|
|
||||||
}) {
|
|
||||||
Text(text = stringResource(id = android.R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
|
|
||||||
text = { block(impl, error) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { cont.cancel() },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
if (invalidator == null) {
|
||||||
|
cont.resume(state.value)
|
||||||
|
} else {
|
||||||
|
coroutineScope.launch {
|
||||||
|
errorMsg = either { invalidator(state.value) }.leftOrNull()
|
||||||
|
errorMsg ?: cont.resume(state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(id = android.R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
cont.cancel()
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(id = android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
|
||||||
|
text = { block(impl, errorMsg) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitInputText(
|
suspend fun awaitInputText(
|
||||||
@@ -156,35 +156,90 @@ class DialogState {
|
|||||||
hint: String? = null,
|
hint: String? = null,
|
||||||
isNumber: Boolean = false,
|
isNumber: Boolean = false,
|
||||||
@StringRes confirmText: Int = android.R.string.ok,
|
@StringRes confirmText: Int = android.R.string.ok,
|
||||||
invalidator: (suspend (String) -> String?)? = null,
|
invalidator: (suspend Raise<String>.(String) -> Unit)? = null,
|
||||||
): String {
|
) = dialog { cont ->
|
||||||
return dialog { cont ->
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
var state by remember(cont) { mutableStateOf(initial) }
|
||||||
var state by remember(cont) { mutableStateOf(initial) }
|
var error by remember(cont) { mutableStateOf<String?>(null) }
|
||||||
var error by remember(cont) { mutableStateOf<String?>(null) }
|
AlertDialog(
|
||||||
AlertDialog(
|
onDismissRequest = { cont.cancel() },
|
||||||
onDismissRequest = { cont.cancel() },
|
confirmButton = {
|
||||||
confirmButton = {
|
TextButton(onClick = {
|
||||||
TextButton(onClick = {
|
if (invalidator == null) {
|
||||||
if (invalidator == null) {
|
cont.resume(state)
|
||||||
cont.resume(state)
|
} else {
|
||||||
} else {
|
coroutineScope.launch {
|
||||||
coroutineScope.launch {
|
error = either { invalidator(state) }.leftOrNull()
|
||||||
error = invalidator.invoke(state)
|
error ?: cont.resume(state)
|
||||||
error ?: cont.resume(state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
Text(text = stringResource(id = confirmText))
|
|
||||||
}
|
}
|
||||||
},
|
}) {
|
||||||
title = title.ifNotNullThen { Text(text = title!!) },
|
Text(text = stringResource(id = confirmText))
|
||||||
text = {
|
}
|
||||||
|
},
|
||||||
|
title = title.ifNotNullThen { Text(text = title!!) },
|
||||||
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state,
|
||||||
|
onValueChange = { state = it },
|
||||||
|
label = hint.ifNotNullThen {
|
||||||
|
Text(text = hint!!)
|
||||||
|
},
|
||||||
|
trailingIcon = error.ifNotNullThen {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingText = error.ifNotNullThen {
|
||||||
|
Text(text = error!!)
|
||||||
|
},
|
||||||
|
isError = error != null,
|
||||||
|
keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun awaitInputTextWithCheckBox(
|
||||||
|
initial: String = "",
|
||||||
|
@StringRes title: Int? = null,
|
||||||
|
@StringRes hint: Int? = null,
|
||||||
|
checked: Boolean,
|
||||||
|
@StringRes checkBoxText: Int,
|
||||||
|
isNumber: Boolean = false,
|
||||||
|
invalidator: (suspend Raise<String>.(String, Boolean) -> Unit)? = null,
|
||||||
|
): Pair<String, Boolean> = dialog { cont ->
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var state by remember(cont) { mutableStateOf(initial) }
|
||||||
|
var error by remember(cont) { mutableStateOf<String?>(null) }
|
||||||
|
var checkedState by remember { mutableStateOf(checked) }
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { cont.cancel() },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
if (invalidator == null) {
|
||||||
|
cont.resume(state to checkedState)
|
||||||
|
} else {
|
||||||
|
coroutineScope.launch {
|
||||||
|
error = either { invalidator(state, checkedState) }.leftOrNull()
|
||||||
|
error ?: cont.resume(state to checkedState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(id = android.R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state,
|
value = state,
|
||||||
onValueChange = { state = it },
|
onValueChange = { state = it },
|
||||||
label = hint.ifNotNullThen {
|
label = hint.ifNotNullThen {
|
||||||
Text(text = hint!!)
|
Text(text = stringResource(id = hint!!))
|
||||||
},
|
},
|
||||||
trailingIcon = error.ifNotNullThen {
|
trailingIcon = error.ifNotNullThen {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -198,74 +253,15 @@ class DialogState {
|
|||||||
isError = error != null,
|
isError = error != null,
|
||||||
keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default,
|
keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default,
|
||||||
)
|
)
|
||||||
},
|
LabeledCheckbox(
|
||||||
)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
}
|
checked = checkedState,
|
||||||
}
|
onCheckedChange = { checkedState = !checkedState },
|
||||||
|
label = stringResource(checkBoxText),
|
||||||
suspend fun awaitInputTextWithCheckBox(
|
)
|
||||||
initial: String = "",
|
}
|
||||||
@StringRes title: Int? = null,
|
},
|
||||||
@StringRes hint: Int? = null,
|
)
|
||||||
checked: Boolean,
|
|
||||||
@StringRes checkBoxText: Int,
|
|
||||||
isNumber: Boolean = false,
|
|
||||||
invalidator: (suspend (String, Boolean) -> String?)? = null,
|
|
||||||
): Pair<String, Boolean> {
|
|
||||||
return dialog { cont ->
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
var state by remember(cont) { mutableStateOf(initial) }
|
|
||||||
var error by remember(cont) { mutableStateOf<String?>(null) }
|
|
||||||
var checkedState by remember { mutableStateOf(checked) }
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { cont.cancel() },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
if (invalidator == null) {
|
|
||||||
cont.resume(state to checkedState)
|
|
||||||
} else {
|
|
||||||
coroutineScope.launch {
|
|
||||||
error = invalidator.invoke(state, checkedState)
|
|
||||||
error ?: cont.resume(state to checkedState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Text(text = stringResource(id = android.R.string.ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state,
|
|
||||||
onValueChange = { state = it },
|
|
||||||
label = hint.ifNotNullThen {
|
|
||||||
Text(text = stringResource(id = hint!!))
|
|
||||||
},
|
|
||||||
trailingIcon = error.ifNotNullThen {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Info,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
supportingText = error.ifNotNullThen {
|
|
||||||
Text(text = error!!)
|
|
||||||
},
|
|
||||||
isError = error != null,
|
|
||||||
keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default,
|
|
||||||
)
|
|
||||||
LabeledCheckbox(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
checked = checkedState,
|
|
||||||
onCheckedChange = { checkedState = !checkedState },
|
|
||||||
label = stringResource(checkBoxText),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitPermissionOrCancel(
|
suspend fun awaitPermissionOrCancel(
|
||||||
@@ -277,32 +273,30 @@ class DialogState {
|
|||||||
onCancelButtonClick: () -> Unit = {},
|
onCancelButtonClick: () -> Unit = {},
|
||||||
secure: Boolean = false,
|
secure: Boolean = false,
|
||||||
text: @Composable (() -> Unit)? = null,
|
text: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) = dialog { cont ->
|
||||||
return dialog { cont ->
|
AlertDialog(
|
||||||
AlertDialog(
|
onDismissRequest = { cont.cancel() },
|
||||||
onDismissRequest = { cont.cancel() },
|
confirmButton = {
|
||||||
confirmButton = {
|
TextButton(onClick = { cont.resume(Unit) }, enabled = confirmButtonEnabled) {
|
||||||
TextButton(onClick = { cont.resume(Unit) }, enabled = confirmButtonEnabled) {
|
Text(text = stringResource(id = confirmText))
|
||||||
Text(text = stringResource(id = confirmText))
|
}
|
||||||
}
|
},
|
||||||
},
|
dismissButton = showCancelButton.ifTrueThen {
|
||||||
dismissButton = showCancelButton.ifTrueThen {
|
TextButton(onClick = {
|
||||||
TextButton(onClick = {
|
onCancelButtonClick()
|
||||||
onCancelButtonClick()
|
cont.cancel()
|
||||||
cont.cancel()
|
}) {
|
||||||
}) {
|
Text(text = stringResource(id = dismissText))
|
||||||
Text(text = stringResource(id = dismissText))
|
}
|
||||||
}
|
},
|
||||||
},
|
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
|
||||||
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
|
text = text,
|
||||||
text = text,
|
properties = if (secure) {
|
||||||
properties = if (secure) {
|
DialogProperties(securePolicy = SecureFlagPolicy.SecureOn)
|
||||||
DialogProperties(securePolicy = SecureFlagPolicy.SecureOn)
|
} else {
|
||||||
} else {
|
DialogProperties()
|
||||||
DialogProperties()
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitSelectDate(
|
suspend fun awaitSelectDate(
|
||||||
@@ -313,63 +307,59 @@ class DialogState {
|
|||||||
initialDisplayMode: DisplayMode = DisplayMode.Picker,
|
initialDisplayMode: DisplayMode = DisplayMode.Picker,
|
||||||
selectableDates: SelectableDates = DatePickerDefaults.AllDates,
|
selectableDates: SelectableDates = DatePickerDefaults.AllDates,
|
||||||
showModeToggle: Boolean = true,
|
showModeToggle: Boolean = true,
|
||||||
): Long? {
|
): Long? = dialog { cont ->
|
||||||
return dialog { cont ->
|
val state = rememberDatePickerState(
|
||||||
val state = rememberDatePickerState(
|
initialSelectedDateMillis,
|
||||||
initialSelectedDateMillis,
|
initialDisplayedMonthMillis,
|
||||||
initialDisplayedMonthMillis,
|
yearRange,
|
||||||
yearRange,
|
initialDisplayMode,
|
||||||
initialDisplayMode,
|
selectableDates,
|
||||||
selectableDates,
|
)
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { cont.cancel() },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { cont.resume(state.selectedDateMillis) }) {
|
||||||
|
Text(text = stringResource(id = android.R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { cont.cancel() }) {
|
||||||
|
Text(text = stringResource(id = android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
DatePicker(
|
||||||
|
state = state,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = title),
|
||||||
|
modifier = Modifier.padding(DatePickerTitlePadding),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
showModeToggle = showModeToggle,
|
||||||
)
|
)
|
||||||
DatePickerDialog(
|
|
||||||
onDismissRequest = { cont.cancel() },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = { cont.resume(state.selectedDateMillis) }) {
|
|
||||||
Text(text = stringResource(id = android.R.string.ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { cont.cancel() }) {
|
|
||||||
Text(text = stringResource(id = android.R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
DatePicker(
|
|
||||||
state = state,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = title),
|
|
||||||
modifier = Modifier.padding(DatePickerTitlePadding),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
showModeToggle = showModeToggle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <R> showNoButton(respectDefaultWidth: Boolean = true, block: @Composable DismissDialogScope<R>.() -> Unit): R {
|
suspend fun <R> showNoButton(respectDefaultWidth: Boolean = true, block: @Composable DismissDialogScope<R>.() -> Unit): R = dialog { cont ->
|
||||||
return dialog { cont ->
|
val impl = remember(cont) {
|
||||||
val impl = remember(cont) {
|
DismissDialogScope<R> {
|
||||||
DismissDialogScope<R> {
|
cont.resume(it)
|
||||||
cont.resume(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
BasicAlertDialog(
|
|
||||||
onDismissRequest = { cont.cancel() },
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth),
|
|
||||||
content = {
|
|
||||||
Surface(
|
|
||||||
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
|
|
||||||
shape = AlertDialogDefaults.shape,
|
|
||||||
color = AlertDialogDefaults.containerColor,
|
|
||||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
|
||||||
content = { block(impl) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
BasicAlertDialog(
|
||||||
|
onDismissRequest = { cont.cancel() },
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth),
|
||||||
|
content = {
|
||||||
|
Surface(
|
||||||
|
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
|
||||||
|
shape = AlertDialogDefaults.shape,
|
||||||
|
color = AlertDialogDefaults.containerColor,
|
||||||
|
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||||
|
content = { block(impl) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitSelectTime(
|
suspend fun awaitSelectTime(
|
||||||
@@ -542,6 +532,7 @@ class DialogState {
|
|||||||
items: List<Pair<ImageVector, String>>,
|
items: List<Pair<ImageVector, String>>,
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
@StringRes hint: Int,
|
@StringRes hint: Int,
|
||||||
|
initialNote: String,
|
||||||
maxChar: Int,
|
maxChar: Int,
|
||||||
): Pair<Int, String> = showNoButton(false) {
|
): Pair<Int, String> = showNoButton(false) {
|
||||||
Column {
|
Column {
|
||||||
@@ -550,7 +541,7 @@ class DialogState {
|
|||||||
modifier = Modifier.fillMaxWidth().aspectRatio(1F),
|
modifier = Modifier.fillMaxWidth().aspectRatio(1F),
|
||||||
placeFirstItemInCenter = true,
|
placeFirstItemInCenter = true,
|
||||||
) {
|
) {
|
||||||
var note by remember { mutableStateOf("") }
|
var note by remember { mutableStateOf(initialNote) }
|
||||||
TextField(
|
TextField(
|
||||||
value = note,
|
value = note,
|
||||||
onValueChange = { note = it },
|
onValueChange = { note = it },
|
||||||
|
|||||||
Loaded 100 of 187 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user