Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d58e08e24c | ||
|
|
e111ec74be | ||
|
|
68155e6a6b | ||
|
|
bfbceefef2 | ||
|
|
21b39135fe | ||
|
|
6adc7f7221 | ||
|
|
2c01acb4a6 | ||
|
|
c0f6d0ab04 | ||
|
|
6bfa8d1ea4 | ||
|
|
6f22269d93 | ||
|
|
b68dc98ae0 | ||
|
|
f9ad1f724b | ||
|
|
afd22635bd | ||
|
|
a9f9706be3 | ||
|
|
41f35498fe | ||
|
|
58628b6556 | ||
|
|
875858618e | ||
|
|
70b7bf390a | ||
|
|
de3d6de4cf | ||
|
|
e3b4caa37d | ||
|
|
f8e19f7772 | ||
|
|
ea5dd8b1bf | ||
|
|
bd7db1a016 | ||
|
|
02ac048d74 | ||
|
|
9a3c9b9cbd | ||
|
|
80c77c740e | ||
|
|
0f62662963 | ||
|
|
576d6515db | ||
|
|
30d29bfa80 | ||
|
|
c7c0446f23 | ||
|
|
134777584d | ||
|
|
0b64eadb0c | ||
|
|
ea93960bf4 | ||
|
|
71032e3010 | ||
|
|
9746f20b60 | ||
|
|
501148179b | ||
|
|
d6a3a351f5 | ||
|
|
a574bca48e | ||
|
|
785faf1bf4 | ||
|
|
fbcc32b3ee | ||
|
|
46b19a9de5 | ||
|
|
d2f47d51f9 | ||
|
|
db380507f5 | ||
|
|
84cf67d21a | ||
|
|
8f53591800 | ||
|
|
65ffd78844 | ||
|
|
c00b5e6635 | ||
|
|
8160991086 | ||
|
|
f0c933d97a | ||
|
|
3fb8d3e73b | ||
|
|
d8efff9921 | ||
|
|
32170688b6 | ||
|
|
d08491bf7e | ||
|
|
12f6f08d81 | ||
|
|
ca74bfe2e4 | ||
|
|
41767f0fd7 | ||
|
|
c62ca4dd23 | ||
|
|
9c5eddb903 | ||
|
|
4a0df07805 | ||
|
|
37a3edd44b | ||
|
|
f117abd483 | ||
|
|
4593714298 | ||
|
|
a148fd6ec3 | ||
|
|
23a44e2f3b | ||
|
|
01e03ec0f0 | ||
|
|
e2345cc756 | ||
|
|
8d0b8534b2 | ||
|
|
4594ac592c | ||
|
|
eee0f2dded | ||
|
|
b4080302c3 | ||
|
|
337e210b5b |
No files matched your search
@@ -135,10 +135,7 @@ body:
|
|||||||
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
|
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
|
- label: 您已搜索过 [Issue Tracker](https://github.com/FooIbar/EhViewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/FooIbar/EhViewer/issues), No duplicate or related open issue has been found
|
||||||
required: true
|
|
||||||
|
|
||||||
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
|
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
|
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
|
||||||
|
|||||||
@@ -1,5 +1 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
|
||||||
- name: 所有其他问题 / All other questions
|
|
||||||
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
|
|
||||||
about: 转到 Discussions / Turn to discussions
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
name: 功能请求 / Feature request
|
|
||||||
description: 提出一个功能建议 / Suggest an idea
|
|
||||||
labels:
|
|
||||||
- "enhancement"
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
|
|
||||||
提交功能建议前,还请首先完成文末的自查步骤
|
|
||||||
|
|
||||||
Thanks for your contribution on Ehviewer-Overhauled Project!
|
|
||||||
Please finish verify steps which list in the end first before suggest an idea
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: request
|
|
||||||
attributes:
|
|
||||||
label: 需求 / Requirement
|
|
||||||
description: |
|
|
||||||
在此处描述您的需求。这通常会是一个您想要的功能
|
|
||||||
Ddescribe what you need here.
|
|
||||||
placeholder: |
|
|
||||||
我需要 XXX 功能 ...
|
|
||||||
I want ABC feature ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: impl
|
|
||||||
attributes:
|
|
||||||
label: 建议实现 / Suggested implements
|
|
||||||
description: |
|
|
||||||
在此处表述您建议的实现方式。如有可能,UI 类功能请求还请尽量附上图示
|
|
||||||
Describe your suggested implements here, It's recommend to add a photo if you are making a UI feature request.
|
|
||||||
placeholder: |
|
|
||||||
建议在 XX 处添加 XX ...
|
|
||||||
I recommend add ABC feature to DEF...
|
|
||||||
图片(如果有)/ Photos (if exists)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: more
|
|
||||||
attributes:
|
|
||||||
label: 备注 / Addition details
|
|
||||||
description: |
|
|
||||||
在此处写下其他您想说的内容
|
|
||||||
Describe addition details here
|
|
||||||
placeholder: |
|
|
||||||
其他有用的信息与附件
|
|
||||||
Additional details and attachments
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: EhViewer 版本号 / EhViewer version code
|
|
||||||
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
|
|
||||||
placeholder: 1.6.9.13
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: ci
|
|
||||||
attributes:
|
|
||||||
label: EhViewer CI 版本 / EhViewer CI Version
|
|
||||||
description: 位于设置 - 关于中的版本号后 / Next to the version code in Settings - About
|
|
||||||
placeholder: 26f05a0
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: check
|
|
||||||
attributes:
|
|
||||||
label: 自查步骤 / Verify steps
|
|
||||||
description: |
|
|
||||||
请确认您已经遵守所有必选项
|
|
||||||
Please ensure you have obtained all needed options
|
|
||||||
options:
|
|
||||||
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- label: 您确保这个 Issue 只提及一个功能。如果您有多个功能请求,烦请发起多个 Issue / Ensure there is only one feature request in this issue. Please make mutiply issue for mutiply feature request
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
|
|
||||||
required: true
|
|
||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install NDK
|
- name: Install NDK
|
||||||
run: |
|
run: |
|
||||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10636728" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install NDK
|
- name: Install NDK
|
||||||
run: |
|
run: |
|
||||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10636728" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install NDK
|
- name: Install NDK
|
||||||
run: |
|
run: |
|
||||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10636728" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# Maintenance Mode
|
||||||
|
|
||||||
|
The project is in maintenance mode, will only receive bugfixes.
|
||||||
|
|
||||||
<p align="right">
|
<p align="right">
|
||||||
<strong>English</strong>
|
<strong>English</strong>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
|
|||||||
+17
-19
@@ -21,7 +21,7 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
buildToolsVersion = "34.0.0"
|
buildToolsVersion = "34.0.0"
|
||||||
ndkVersion = "26.0.10404224"
|
ndkVersion = "26.0.10636728"
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
@@ -64,7 +64,7 @@ android {
|
|||||||
minSdk = 28
|
minSdk = 28
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 180043
|
versionCode = 180043
|
||||||
versionName = "1.8.9.1"
|
versionName = "1.8.9.6"
|
||||||
resourceConfigurations.addAll(
|
resourceConfigurations.addAll(
|
||||||
listOf(
|
listOf(
|
||||||
"zh",
|
"zh",
|
||||||
@@ -159,7 +159,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.5.1"
|
kotlinCompilerExtensionVersion = "1.5.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace = "com.hippo.ehviewer"
|
namespace = "com.hippo.ehviewer"
|
||||||
@@ -167,11 +167,11 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||||
implementation("androidx.activity:activity-compose:1.8.0-alpha06")
|
implementation("androidx.activity:activity-compose:1.8.0-beta01")
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
|
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
|
||||||
implementation("androidx.browser:browser:1.6.0")
|
implementation("androidx.browser:browser:1.6.0")
|
||||||
implementation("androidx.collection:collection-ktx:1.3.0-beta01")
|
implementation("androidx.collection:collection-ktx:1.3.0-rc01")
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
||||||
// api(platform("androidx.compose:compose-bom:2023.05.00"))
|
// api(platform("androidx.compose:compose-bom:2023.05.00"))
|
||||||
@@ -181,29 +181,29 @@ dependencies {
|
|||||||
implementation("androidx.compose.material3:material3-window-size-class")
|
implementation("androidx.compose.material3:material3-window-size-class")
|
||||||
implementation("androidx.compose.ui:ui-util")
|
implementation("androidx.compose.ui:ui-util")
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.12.0-rc01")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha12")
|
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha12")
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha02")
|
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha04")
|
||||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||||
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
|
implementation("androidx.lifecycle:lifecycle-process:2.6.2")
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||||
val nav_version = "2.7.0"
|
val nav_version = "2.7.2"
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
|
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
|
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
|
||||||
implementation("androidx.navigation:navigation-compose:$nav_version")
|
implementation("androidx.navigation:navigation-compose:$nav_version")
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||||
val paging = "3.2.0"
|
val paging = "3.2.1"
|
||||||
implementation("androidx.paging:paging-compose:$paging")
|
implementation("androidx.paging:paging-compose:$paging")
|
||||||
implementation("androidx.paging:paging-runtime-ktx:$paging")
|
implementation("androidx.paging:paging-runtime-ktx:$paging")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
||||||
implementation("androidx.recyclerview:recyclerview-selection:1.2.0-alpha01")
|
implementation("androidx.recyclerview:recyclerview-selection:1.2.0-alpha01")
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/room
|
// https://developer.android.com/jetpack/androidx/releases/room
|
||||||
val room_version = "2.6.0-alpha03"
|
val room_version = "2.6.0-beta01"
|
||||||
ksp("androidx.room:room-compiler:$room_version")
|
ksp("androidx.room:room-compiler:$room_version")
|
||||||
implementation("androidx.room:room-paging:$room_version")
|
implementation("androidx.room:room-paging:$room_version")
|
||||||
|
|
||||||
@@ -213,10 +213,10 @@ dependencies {
|
|||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
|
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
|
||||||
// https://github.com/google/accompanist/releases
|
// https://github.com/google/accompanist/releases
|
||||||
val accompanist_version = "0.31.6-rc"
|
val accompanist_version = "0.32.0"
|
||||||
implementation("com.google.accompanist:accompanist-themeadapter-material3:$accompanist_version")
|
implementation("com.google.accompanist:accompanist-themeadapter-material3:$accompanist_version")
|
||||||
implementation("com.google.accompanist:accompanist-webview:$accompanist_version")
|
implementation("com.google.accompanist:accompanist-webview:$accompanist_version")
|
||||||
implementation("com.google.android.material:material:1.11.0-alpha01")
|
implementation("com.google.android.material:material:1.11.0-alpha02")
|
||||||
|
|
||||||
val splitties = "3.0.0"
|
val splitties = "3.0.0"
|
||||||
implementation("com.louiscad.splitties:splitties-appctx:$splitties")
|
implementation("com.louiscad.splitties:splitties-appctx:$splitties")
|
||||||
@@ -238,7 +238,7 @@ dependencies {
|
|||||||
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||||
|
|
||||||
implementation(platform("io.arrow-kt:arrow-stack:1.2.0"))
|
implementation(platform("io.arrow-kt:arrow-stack:1.2.1"))
|
||||||
implementation("io.arrow-kt:arrow-fx-coroutines")
|
implementation("io.arrow-kt:arrow-fx-coroutines")
|
||||||
|
|
||||||
// https://coil-kt.github.io/coil/changelog/
|
// https://coil-kt.github.io/coil/changelog/
|
||||||
@@ -246,13 +246,11 @@ dependencies {
|
|||||||
implementation("io.coil-kt:coil-compose")
|
implementation("io.coil-kt:coil-compose")
|
||||||
implementation("io.coil-kt:coil-gif")
|
implementation("io.coil-kt:coil-gif")
|
||||||
|
|
||||||
val ktor = "2.3.3"
|
val ktor = "2.3.4"
|
||||||
implementation("io.ktor:ktor-io-jvm:$ktor")
|
implementation("io.ktor:ktor-io-jvm:$ktor")
|
||||||
implementation("io.ktor:ktor-utils-jvm:$ktor")
|
implementation("io.ktor:ktor-utils-jvm:$ktor")
|
||||||
|
|
||||||
implementation("org.chromium.net:cronet-embedded:113.5672.61")
|
val serialization = "1.6.0"
|
||||||
|
|
||||||
val serialization = "1.5.1"
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:$serialization")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:$serialization")
|
||||||
@@ -298,6 +296,6 @@ tasks.configureEach {
|
|||||||
if ((name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders")) {
|
if ((name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders")) {
|
||||||
dependsOn("cargoBuild")
|
dependsOn("cargoBuild")
|
||||||
// fix mergeDebugJniLibFolders UP-TO-DATE
|
// fix mergeDebugJniLibFolders UP-TO-DATE
|
||||||
inputs.dir(buildDir.resolve("rustJniLibs/android"))
|
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+1
@@ -17,6 +17,7 @@
|
|||||||
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
|
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
|
||||||
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
|
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
|
||||||
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
|
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
|
||||||
|
-keep class com.hippo.ehviewer.ui.scene.SearchBarScene { getBinding(); }
|
||||||
|
|
||||||
# Ktor logger
|
# Ktor logger
|
||||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
|
|||||||
@@ -30,16 +30,16 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var downloadQuery by stringOrNullPref("image_query", null)
|
var downloadQuery by stringOrNullPref("image_query", null)
|
||||||
var downloadFragment by stringOrNullPref("image_fragment", null)
|
var downloadFragment by stringOrNullPref("image_fragment", null)
|
||||||
var archivePasswds by stringSetOrNullPref("archive_passwds")
|
var archivePasswds by stringSetOrNullPref("archive_passwds")
|
||||||
var downloadDelay by intFromStrPref("download_delay", 0)
|
var downloadDelay by intPref("download_delay_2", 0)
|
||||||
var gallerySite by intFromStrPref("gallery_site", 0).observed { updateWhenGallerySiteChanges() }
|
var gallerySite by intPref("gallery_site_2", 0).observed { updateWhenGallerySiteChanges() }
|
||||||
var multiThreadDownload by intFromStrPref("download_thread", 3)
|
var multiThreadDownload by intPref("download_thread_2", 3)
|
||||||
var preloadImage by intFromStrPref("preload_image", 5)
|
var preloadImage by intPref("preload_image_2", 5)
|
||||||
var theme by intFromStrPref("theme", -1).observed { updateWhenThemeChanges() }
|
var theme by intPref("theme_2", -1).observed { updateWhenThemeChanges() }
|
||||||
var listMode by intFromStrPref("list_mode", 0)
|
var listMode by intPref("list_mode_2", 0)
|
||||||
var detailSize by intFromStrPref("detail_size", 0)
|
var detailSize by intPref("detail_size_2", 0)
|
||||||
var thumbResolution by intFromStrPref("thumb_resolution", 0)
|
var thumbResolution by intPref("thumb_resolution_2", 0)
|
||||||
var readCacheSize by intFromStrPref("read_cache_size", 640)
|
var readCacheSize by intPref("read_cache_size_2", 640)
|
||||||
var launchPage by intFromStrPref("launch_page", 0)
|
var launchPage by intPref("launch_page_2", 0)
|
||||||
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() }
|
||||||
@@ -60,9 +60,9 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var removeImageFiles by boolPref("include_pic", true)
|
var removeImageFiles by boolPref("include_pic", true)
|
||||||
var needSignIn by boolPref("need_sign_in", true)
|
var needSignIn by boolPref("need_sign_in", true)
|
||||||
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
|
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
|
||||||
|
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
||||||
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
||||||
var downloadOriginImage by boolPref("download_origin_image", false)
|
var downloadOriginImage by boolPref("download_origin_image", false)
|
||||||
var enableQuic by boolPref("enable_quic", true)
|
|
||||||
var thumbSizeDp by intPref("thumb_size_", 120)
|
var thumbSizeDp by intPref("thumb_size_", 120)
|
||||||
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
|
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
|
||||||
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
||||||
@@ -95,13 +95,6 @@ object Settings : DataStorePreferences(null) {
|
|||||||
operator fun setValue(thisRef: Any?, prop: KProperty<*>?, value: R)
|
operator fun setValue(thisRef: Any?, prop: KProperty<*>?, value: R)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun intFromStrPref(key: String, defValue: Int) = object : Delegate<Int> {
|
|
||||||
override val flowGetter: () -> Flow<Unit>
|
|
||||||
private var _value by stringPref(key, defValue.toString()).also { flowGetter = { it.changesFlow() } }
|
|
||||||
override fun getValue(thisRef: Any?, prop: KProperty<*>?) = _value.toIntOrNull() ?: defValue
|
|
||||||
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Int) { _value = value.toString() }
|
|
||||||
}
|
|
||||||
|
|
||||||
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() }
|
||||||
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
|
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ private fun rethrowExactly(code: Int, headers: Headers, body: String, e: Throwab
|
|||||||
} else if ("<" !in body) {
|
} else if ("<" !in body) {
|
||||||
throw EhException(body)
|
throw EhException(body)
|
||||||
} else {
|
} else {
|
||||||
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e)
|
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e, body)
|
||||||
throw EhException(appCtx.getString(R.string.error_parse_error))
|
throw EhException(appCtx.getString(R.string.error_parse_error), e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,12 +126,18 @@ object EhUtils {
|
|||||||
MISC -> BG_COLOR_MISC
|
MISC -> BG_COLOR_MISC
|
||||||
else -> BG_COLOR_UNKNOWN
|
else -> BG_COLOR_UNKNOWN
|
||||||
}.toInt()
|
}.toInt()
|
||||||
return harmonizeWithRole(context, primary)
|
return if (Settings.harmonizeCategoryColor) {
|
||||||
|
harmonizeWithRole(context, primary)
|
||||||
|
} else {
|
||||||
|
primary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getCategoryColor(category: Int) = Color(getCategoryColor(LocalContext.current, category))
|
fun getCategoryColor(category: Int) = Color(getCategoryColor(LocalContext.current, category))
|
||||||
|
|
||||||
|
val categoryTextColor = Color(0xffe6e0e9)
|
||||||
|
|
||||||
fun signOut() {
|
fun signOut() {
|
||||||
EhCookieStore.signOut()
|
EhCookieStore.signOut()
|
||||||
Settings.avatar = null
|
Settings.avatar = null
|
||||||
|
|||||||
@@ -16,16 +16,7 @@
|
|||||||
package com.hippo.ehviewer.client.exception
|
package com.hippo.ehviewer.client.exception
|
||||||
|
|
||||||
class ParseException : EhException {
|
class ParseException : EhException {
|
||||||
val body: String
|
constructor(detailMessage: String?) : super(detailMessage)
|
||||||
|
|
||||||
constructor(detailMessage: String?, body: String) : super(detailMessage) {
|
constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)
|
||||||
this.body = body
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(detailMessage: String?, body: String, cause: Throwable?) : super(
|
|
||||||
detailMessage,
|
|
||||||
cause,
|
|
||||||
) {
|
|
||||||
this.body = body
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ object FavoritesParser {
|
|||||||
}
|
}
|
||||||
val catArray = arrayOfNulls<String>(10)
|
val catArray = arrayOfNulls<String>(10)
|
||||||
val countArray = parseFav(body, catArray)
|
val countArray = parseFav(body, catArray)
|
||||||
if (countArray.isEmpty()) throw ParseException("Parse favorites error", body)
|
if (countArray.isEmpty()) throw ParseException("Parse favorites error")
|
||||||
val result = GalleryListParser.parse(body)
|
val result = GalleryListParser.parse(body)
|
||||||
return Result(catArray.requireNoNulls(), countArray, result)
|
return Result(catArray.requireNoNulls(), countArray, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ object GalleryDetailParser {
|
|||||||
gd.token = groupValues[2]
|
gd.token = groupValues[2]
|
||||||
gd.apiUid = groupValues[3].toLongOrNull() ?: -1L
|
gd.apiUid = groupValues[3].toLongOrNull() ?: -1L
|
||||||
gd.apiKey = groupValues[4]
|
gd.apiKey = groupValues[4]
|
||||||
} ?: throw ParseException("Can't parse gallery detail", body)
|
} ?: throw ParseException("Can't parse gallery detail")
|
||||||
if (gd.gid == -1L) {
|
if (gd.gid == -1L) {
|
||||||
throw ParseException("Can't parse gallery detail", body)
|
throw ParseException("Can't parse gallery detail")
|
||||||
}
|
}
|
||||||
PATTERN_TORRENT.find(body)?.run {
|
PATTERN_TORRENT.find(body)?.run {
|
||||||
gd.torrentUrl = groupValues[1].trim().unescapeXml()
|
gd.torrentUrl = groupValues[1].trim().unescapeXml()
|
||||||
@@ -214,7 +214,7 @@ object GalleryDetailParser {
|
|||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
ExceptionUtils.throwIfFatal(e)
|
ExceptionUtils.throwIfFatal(e)
|
||||||
throw ParseException("Can't parse gallery detail", body)
|
throw ParseException("Can't parse gallery detail")
|
||||||
}
|
}
|
||||||
|
|
||||||
// newer version
|
// newer version
|
||||||
@@ -450,7 +450,7 @@ object GalleryDetailParser {
|
|||||||
*/
|
*/
|
||||||
fun parsePreviewPages(body: String): Int {
|
fun parsePreviewPages(body: String): Int {
|
||||||
return 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", body)
|
?: throw ParseException("Parse preview page count error")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -458,7 +458,7 @@ object GalleryDetailParser {
|
|||||||
*/
|
*/
|
||||||
fun parsePages(body: String): Int {
|
fun parsePages(body: String): Int {
|
||||||
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||||
?: throw ParseException("Parse pages error", body)
|
?: throw ParseException("Parse pages error")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> {
|
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
|
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||||
|
import com.hippo.ehviewer.client.exception.EhException
|
||||||
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
|
|
||||||
object GalleryListParser {
|
object GalleryListParser {
|
||||||
suspend fun parse(body: String) = runCatching {
|
suspend fun parse(body: String) = runCatching {
|
||||||
@@ -15,13 +18,21 @@ object GalleryListParser {
|
|||||||
it.generateSLang()
|
it.generateSLang()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.getOrDefault(GalleryListResult())
|
}.getOrElse {
|
||||||
|
if (body.contains("<p>You do not have any watched tags")) {
|
||||||
|
throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||||
|
}
|
||||||
|
if (body.contains("No hits found</p>")) {
|
||||||
|
throw EhException(R.string.gallery_list_empty_hit)
|
||||||
|
}
|
||||||
|
throw ParseException("Can't parse gallery list", it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GalleryListResult(
|
class GalleryListResult(
|
||||||
val prev: String? = null,
|
val prev: String?,
|
||||||
val next: String? = null,
|
val next: String?,
|
||||||
val galleryInfoList: ArrayList<BaseGalleryInfo> = arrayListOf(),
|
val galleryInfoList: ArrayList<BaseGalleryInfo>,
|
||||||
)
|
)
|
||||||
|
|
||||||
private external fun parseGalleryInfoList(e: String): GalleryListResult
|
private external fun parseGalleryInfoList(e: String): GalleryListResult
|
||||||
+1
-1
@@ -33,7 +33,7 @@ object GalleryMultiPageViewerPTokenParser {
|
|||||||
imagelist.parseAs<List<Item>>().map(Item::token)
|
imagelist.parseAs<List<Item>>().map(Item::token)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
ExceptionUtils.throwIfFatal(it)
|
ExceptionUtils.throwIfFatal(it)
|
||||||
throw ParseException("Parse pToken from MPV error", body, it)
|
throw ParseException("Parse pToken from MPV error", it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ object GalleryPageApiParser {
|
|||||||
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
|
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
|
||||||
|
|
||||||
fun parse(body: String): Result {
|
fun parse(body: String): Result {
|
||||||
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error, body) }
|
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error) }
|
||||||
val res = body.parseAs<JsonResult>()
|
val res = body.parseAs<JsonResult>()
|
||||||
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
|
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
|
||||||
groupValues[1].trim().unescapeXml()
|
groupValues[1].trim().unescapeXml()
|
||||||
@@ -41,7 +41,7 @@ object GalleryPageApiParser {
|
|||||||
if (!imageUrl.isNullOrEmpty()) {
|
if (!imageUrl.isNullOrEmpty()) {
|
||||||
return Result(imageUrl, skipHathKey, originImageUrl)
|
return Result(imageUrl, skipHathKey, originImageUrl)
|
||||||
} else {
|
} else {
|
||||||
throw ParseException("Parse image url and skip hath key error", body)
|
throw ParseException("Parse image url and skip hath key error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ object GalleryPageParser {
|
|||||||
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {
|
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {
|
||||||
Result(imageUrl, skipHathKey, originImageUrl, showKey)
|
Result(imageUrl, skipHathKey, originImageUrl, showKey)
|
||||||
} else {
|
} else {
|
||||||
throw ParseException("Parse image url and show error", body)
|
throw ParseException("Parse image url and show error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ object HomeParser {
|
|||||||
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
|
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
|
||||||
return Funds(fundsGP, fundsC)
|
return Funds(fundsGP, fundsC)
|
||||||
}
|
}
|
||||||
throw ParseException("Parse funds error", body)
|
throw ParseException("Parse funds error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ object ProfileParser {
|
|||||||
Result(displayName, avatar)
|
Result(displayName, avatar)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
ExceptionUtils.throwIfFatal(it)
|
ExceptionUtils.throwIfFatal(it)
|
||||||
throw ParseException("Parse forums error", body)
|
throw ParseException("Parse forums error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ object SignInParser {
|
|||||||
if (m.find()) {
|
if (m.find()) {
|
||||||
throw EhException(m.group(1) ?: m.group(2))
|
throw EhException(m.group(1) ?: m.group(2))
|
||||||
} else {
|
} else {
|
||||||
throw ParseException("Can't parse sign in", body)
|
throw ParseException("Can't parse sign in")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
object TorrentParser {
|
object TorrentParser {
|
||||||
fun parse(body: ByteBuffer) = parseTorrent(body)
|
fun parse(body: ByteBuffer) = runCatching {
|
||||||
|
parseTorrent(body)
|
||||||
|
}.getOrElse { throw ParseException("Can't parse torrent list", it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
class Torrent(
|
class Torrent(
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
@file:Suppress("BlockingMethodInNonBlockingContext")
|
@file:Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
|
||||||
|
|
||||||
package com.hippo.ehviewer.coil
|
package com.hippo.ehviewer.coil
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.annotation.RequiresExtension
|
||||||
import coil.ComponentRegistry
|
import coil.ComponentRegistry
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.decode.DataSource
|
import coil.decode.DataSource
|
||||||
@@ -27,7 +30,7 @@ class CronetHttpUriFetcher(private val data: String, private val options: Option
|
|||||||
val diskCache = requireNotNull(imageLoader.diskCache)
|
val diskCache = requireNotNull(imageLoader.diskCache)
|
||||||
val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run {
|
val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run {
|
||||||
val success = cronetRequest(data) {
|
val success = cronetRequest(data) {
|
||||||
disableCache()
|
setCacheDisabled(true)
|
||||||
}.execute {
|
}.execute {
|
||||||
diskCache.suspendEdit(diskCacheKey) {
|
diskCache.suspendEdit(diskCacheKey) {
|
||||||
RandomAccessFile(data.toFile(), "rw").use {
|
RandomAccessFile(data.toFile(), "rw").use {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
|
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
|
||||||
|
|
||||||
package com.hippo.ehviewer.cronet
|
package com.hippo.ehviewer.cronet
|
||||||
|
|
||||||
|
import android.net.http.HttpEngine
|
||||||
|
import android.net.http.HttpException
|
||||||
|
import android.net.http.UrlRequest
|
||||||
|
import android.net.http.UrlResponseInfo
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresExtension
|
||||||
import com.hippo.ehviewer.EhApplication
|
import com.hippo.ehviewer.EhApplication
|
||||||
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
||||||
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
||||||
@@ -12,10 +20,6 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
import org.chromium.net.CronetEngine
|
|
||||||
import org.chromium.net.CronetException
|
|
||||||
import org.chromium.net.UrlRequest
|
|
||||||
import org.chromium.net.UrlResponseInfo
|
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.channels.FileChannel
|
import java.nio.channels.FileChannel
|
||||||
@@ -28,26 +32,23 @@ import kotlin.coroutines.resumeWithException
|
|||||||
private const val TAG = "CronetRequest"
|
private const val TAG = "CronetRequest"
|
||||||
val pool = DirectByteBufferPool(32)
|
val pool = DirectByteBufferPool(32)
|
||||||
|
|
||||||
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
|
val cronetHttpClient: HttpEngine = HttpEngine.Builder(appCtx).apply {
|
||||||
enableBrotli(true)
|
setEnableBrotli(true)
|
||||||
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
|
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
|
||||||
setStoragePath(cache.absolutePath)
|
setStoragePath(cache.absolutePath)
|
||||||
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
|
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
|
||||||
addQuicHint("e-hentai.org", 443, 443)
|
setUserAgent(CHROME_USER_AGENT)
|
||||||
addQuicHint("forums.e-hentai.org", 443, 443)
|
|
||||||
addQuicHint("exhentai.org", 443, 443)
|
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService
|
val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService
|
||||||
|
|
||||||
// TODO: Rewrite this to use android.net.http.HttpEngine and make it Android 14 only when released
|
|
||||||
class CronetRequest {
|
class CronetRequest {
|
||||||
lateinit var consumer: (ByteBuffer) -> Unit
|
lateinit var consumer: (ByteBuffer) -> Unit
|
||||||
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
|
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
|
||||||
lateinit var request: UrlRequest
|
lateinit var request: UrlRequest
|
||||||
lateinit var onError: (Throwable) -> Unit
|
lateinit var onError: (Throwable) -> Unit
|
||||||
lateinit var readerCont: Continuation<Unit>
|
lateinit var readerCont: Continuation<Unit>
|
||||||
val callback = object : UrlRequest.Callback() {
|
val callback = object : UrlRequest.Callback {
|
||||||
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
|
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
|
||||||
logcat(tag = TAG) { "Redirected to $url" }
|
logcat(tag = TAG) { "Redirected to $url" }
|
||||||
req.followRedirect()
|
req.followRedirect()
|
||||||
@@ -66,16 +67,19 @@ class CronetRequest {
|
|||||||
readerCont.resume(Unit)
|
readerCont.resume(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: CronetException) {
|
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: HttpException) {
|
||||||
onError(e)
|
onError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCanceled(req: UrlRequest, info: UrlResponseInfo?) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
|
inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
|
||||||
request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply {
|
request = cronetHttpClient.newUrlRequestBuilder(url, cronetHttpClientExecutor, callback).apply {
|
||||||
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
|
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
|
||||||
addHeader("User-Agent", CHROME_USER_AGENT)
|
|
||||||
addHeader("Accept", CHROME_ACCEPT)
|
addHeader("Accept", CHROME_ACCEPT)
|
||||||
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
|
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
|
||||||
referer?.let { addHeader("Referer", it) }
|
referer?.let { addHeader("Referer", it) }
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import java.time.Instant
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class DownloadEntity(
|
data class DownloadEntity(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@ColumnInfo(name = "GID")
|
@ColumnInfo(name = "GID")
|
||||||
var gid: Long,
|
var gid: Long,
|
||||||
@@ -44,21 +44,23 @@ class DownloadEntity(
|
|||||||
|
|
||||||
@ColumnInfo(name = "POSITION")
|
@ColumnInfo(name = "POSITION")
|
||||||
override var position: Int = 0,
|
override var position: Int = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var speed: Long = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var remaining: Long = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var finished: Int = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var downloaded: Int = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var total: Int = 0,
|
||||||
) : AbstractDownloadInfo {
|
) : AbstractDownloadInfo {
|
||||||
@Ignore
|
constructor() : this(0)
|
||||||
override var speed: Long = 0
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
override var remaining: Long = 0
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
override var finished = 0
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
override var downloaded = 0
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
override var total = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DownloadInfo(
|
data class DownloadInfo(
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
// Store download info wait to start
|
// Store download info wait to start
|
||||||
private val mWaitList = LinkedList<DownloadInfo>()
|
private val mWaitList = LinkedList<DownloadInfo>()
|
||||||
private val mSpeedReminder = SpeedReminder()
|
private val mSpeedReminder = SpeedReminder()
|
||||||
private val mDownloadInfoListeners = mutableListOf<DownloadInfoListener>()
|
private var mDownloadInfoListener: DownloadInfoListener? = null
|
||||||
private val mNotifyTaskPool = ConcurrentPool<NotifyTask?>(5)
|
private val mNotifyTaskPool = ConcurrentPool<NotifyTask?>(5)
|
||||||
private var mDownloadListener: DownloadListener? = null
|
private var mDownloadListener: DownloadListener? = null
|
||||||
private var mCurrentTask: DownloadInfo? = null
|
private var mCurrentTask: DownloadInfo? = null
|
||||||
@@ -125,14 +125,6 @@ object DownloadManager : OnSpiderListener {
|
|||||||
return info?.state ?: DownloadInfo.STATE_INVALID
|
return info?.state ?: DownloadInfo.STATE_INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
|
|
||||||
mDownloadInfoListeners.add(downloadInfoListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
|
|
||||||
mDownloadInfoListeners.remove(downloadInfoListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDownloadListener(listener: DownloadListener?) {
|
fun setDownloadListener(listener: DownloadListener?) {
|
||||||
mDownloadListener = listener
|
mDownloadListener = listener
|
||||||
}
|
}
|
||||||
@@ -166,12 +158,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
mDownloadListener!!.onStart(info)
|
mDownloadListener!!.onStart(info)
|
||||||
}
|
}
|
||||||
// Notify state update
|
// Notify state update
|
||||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
if (list != null) {
|
|
||||||
for (l in mDownloadInfoListeners) {
|
|
||||||
l.onUpdate(info, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +179,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
// Update in DB
|
// Update in DB
|
||||||
EhDB.putDownloadInfo(info)
|
EhDB.putDownloadInfo(info)
|
||||||
// Notify state update
|
// Notify state update
|
||||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
if (list != null) {
|
|
||||||
for (l in mDownloadInfoListeners) {
|
|
||||||
l.onUpdate(info, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Make sure download is running
|
// Make sure download is running
|
||||||
ensureDownload()
|
ensureDownload()
|
||||||
}
|
}
|
||||||
@@ -227,9 +209,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
EhDB.putDownloadInfo(info)
|
EhDB.putDownloadInfo(info)
|
||||||
|
|
||||||
// Notify
|
// Notify
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
l.onAdd(info, list, list.size - 1)
|
|
||||||
}
|
|
||||||
// Make sure download is running
|
// Make sure download is running
|
||||||
ensureDownload()
|
ensureDownload()
|
||||||
|
|
||||||
@@ -259,9 +239,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
}
|
}
|
||||||
if (update) {
|
if (update) {
|
||||||
// Notify Listener
|
// Notify Listener
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdateAll()
|
||||||
l.onUpdateAll()
|
|
||||||
}
|
|
||||||
// Ensure download
|
// Ensure download
|
||||||
ensureDownload()
|
ensureDownload()
|
||||||
}
|
}
|
||||||
@@ -271,33 +249,23 @@ object DownloadManager : OnSpiderListener {
|
|||||||
|
|
||||||
private val _stateFlow = callbackFlow {
|
private val _stateFlow = callbackFlow {
|
||||||
val listener = object : DownloadInfoListener {
|
val listener = object : DownloadInfoListener {
|
||||||
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
|
override fun onUpdate(info: DownloadInfo) {
|
||||||
trySend(info)
|
trySend(info.gid)
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
|
|
||||||
trySend(info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdateAll() {
|
override fun onUpdateAll() {
|
||||||
// No-op
|
trySend(0)
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReload() {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
|
|
||||||
trySend(info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addDownloadInfoListener(listener)
|
mDownloadInfoListener = listener
|
||||||
awaitClose {
|
awaitClose {
|
||||||
removeDownloadInfoListener(listener)
|
mDownloadInfoListener = null
|
||||||
}
|
}
|
||||||
}.shareIn(callbackFlowScope, SharingStarted.Eagerly)
|
}.shareIn(callbackFlowScope, SharingStarted.Eagerly)
|
||||||
|
|
||||||
fun stateFlow(gid: Long) = _stateFlow.filter { it.gid == gid }
|
fun stateFlow(gid: Long) = _stateFlow.filter { it == gid }
|
||||||
|
|
||||||
|
fun stateFlow() = _stateFlow
|
||||||
|
|
||||||
suspend fun startAllDownload() {
|
suspend fun startAllDownload() {
|
||||||
var update = false
|
var update = false
|
||||||
@@ -315,9 +283,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
}
|
}
|
||||||
if (update) {
|
if (update) {
|
||||||
// Notify Listener
|
// Notify Listener
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdateAll()
|
||||||
l.onUpdateAll()
|
|
||||||
}
|
|
||||||
// Ensure download
|
// Ensure download
|
||||||
ensureDownload()
|
ensureDownload()
|
||||||
}
|
}
|
||||||
@@ -353,9 +319,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
|
|
||||||
// Notify
|
// Notify
|
||||||
if (notify) {
|
if (notify) {
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdateAll()
|
||||||
l.onReload()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,9 +350,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
|
|
||||||
// Notify
|
// Notify
|
||||||
withUIContext {
|
withUIContext {
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
l.onAdd(info, list, list.size - 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,12 +358,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
val info = stopDownloadInternal(gid)
|
val info = stopDownloadInternal(gid)
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
// Update listener
|
// Update listener
|
||||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
if (list != null) {
|
|
||||||
for (l in mDownloadInfoListeners) {
|
|
||||||
l.onUpdate(info, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ensure download
|
// Ensure download
|
||||||
ensureDownload()
|
ensureDownload()
|
||||||
}
|
}
|
||||||
@@ -411,12 +368,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
val info = stopCurrentDownloadInternal()
|
val info = stopCurrentDownloadInternal()
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
// Update listener
|
// Update listener
|
||||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
if (list != null) {
|
|
||||||
for (l in mDownloadInfoListeners) {
|
|
||||||
l.onUpdate(info, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ensure download
|
// Ensure download
|
||||||
ensureDownload()
|
ensureDownload()
|
||||||
}
|
}
|
||||||
@@ -426,9 +378,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
stopRangeDownloadInternal(gidList)
|
stopRangeDownloadInternal(gidList)
|
||||||
|
|
||||||
// Update listener
|
// Update listener
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdateAll()
|
||||||
l.onUpdateAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure download
|
// Ensure download
|
||||||
ensureDownload()
|
ensureDownload()
|
||||||
@@ -447,9 +397,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
stopCurrentDownloadInternal()
|
stopCurrentDownloadInternal()
|
||||||
|
|
||||||
// Notify mDownloadInfoListener
|
// Notify mDownloadInfoListener
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdateAll()
|
||||||
l.onUpdateAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteDownload(gid: Long, deleteFiles: Boolean = false) {
|
suspend fun deleteDownload(gid: Long, deleteFiles: Boolean = false) {
|
||||||
@@ -470,9 +418,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
list.remove(info)
|
list.remove(info)
|
||||||
// Update listener
|
// Update listener
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
l.onRemove(info, list, index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,15 +441,13 @@ object DownloadManager : OnSpiderListener {
|
|||||||
allInfoList.removeAll(list.toSet())
|
allInfoList.removeAll(list.toSet())
|
||||||
|
|
||||||
// Update listener
|
// Update listener
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdateAll()
|
||||||
l.onReload()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure download
|
// Ensure download
|
||||||
ensureDownload()
|
ensureDownload()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
|
private fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
|
||||||
val info = allInfoList.removeAt(fromPosition)
|
val info = allInfoList.removeAt(fromPosition)
|
||||||
allInfoList.add(toPosition, info)
|
allInfoList.add(toPosition, info)
|
||||||
val range = if (fromPosition < toPosition) fromPosition..toPosition else toPosition..fromPosition
|
val range = if (fromPosition < toPosition) fromPosition..toPosition else toPosition..fromPosition
|
||||||
@@ -515,12 +459,15 @@ object DownloadManager : OnSpiderListener {
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveDownload(label: String?, fromPosition: Int, toPosition: Int) =
|
fun moveDownload(fromItem: DownloadInfo, toItem: DownloadInfo): List<DownloadInfo> {
|
||||||
getInfoListForLabel(label)!!.let {
|
val fromPosition = allInfoList.indexOf(fromItem)
|
||||||
val absFromPosition = allInfoList.indexOf(it[fromPosition])
|
val toPosition = allInfoList.indexOf(toItem)
|
||||||
val absToPosition = allInfoList.indexOf(it[toPosition])
|
return if (fromPosition != -1 && toPosition != -1) {
|
||||||
moveDownload(absFromPosition, absToPosition)
|
moveDownload(fromPosition, toPosition)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun resetAllReadingProgress() = coroutineScope {
|
suspend fun resetAllReadingProgress() = coroutineScope {
|
||||||
allInfoList.forEach { info ->
|
allInfoList.forEach { info ->
|
||||||
@@ -658,9 +605,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
// Save to DB
|
// Save to DB
|
||||||
EhDB.putDownloadInfo(info)
|
EhDB.putDownloadInfo(info)
|
||||||
}
|
}
|
||||||
for (l in mDownloadInfoListeners) {
|
mDownloadInfoListener?.onUpdateAll()
|
||||||
l.onReload()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addLabel(label: String?) {
|
suspend fun addLabel(label: String?) {
|
||||||
@@ -810,30 +755,15 @@ object DownloadManager : OnSpiderListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DownloadInfoListener {
|
interface DownloadInfoListener {
|
||||||
/**
|
|
||||||
* Add the special info to the special position
|
|
||||||
*/
|
|
||||||
fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The special info is changed
|
* The special info is changed
|
||||||
*/
|
*/
|
||||||
fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>)
|
fun onUpdate(info: DownloadInfo)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maybe all data is changed, but size is the same
|
* Maybe all data is changed, but size is the same
|
||||||
*/
|
*/
|
||||||
fun onUpdateAll()
|
fun onUpdateAll()
|
||||||
|
|
||||||
/**
|
|
||||||
* Maybe all data is changed, maybe list is changed
|
|
||||||
*/
|
|
||||||
fun onReload()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the special info from the special position
|
|
||||||
*/
|
|
||||||
fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DownloadListener {
|
interface DownloadListener {
|
||||||
@@ -940,12 +870,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
Log.e(TAG, "Current task is null, but it should not be")
|
Log.e(TAG, "Current task is null, but it should not be")
|
||||||
} else {
|
} else {
|
||||||
info.total = mPages
|
info.total = mPages
|
||||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
if (list != null) {
|
|
||||||
for (l in mDownloadInfoListeners) {
|
|
||||||
l.onUpdate(info, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,12 +899,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
if (mDownloadListener != null) {
|
if (mDownloadListener != null) {
|
||||||
mDownloadListener!!.onGetPage(info)
|
mDownloadListener!!.onGetPage(info)
|
||||||
}
|
}
|
||||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
if (list != null) {
|
|
||||||
for (l in mDownloadInfoListeners) {
|
|
||||||
l.onUpdate(info, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,12 +912,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
info.finished = mFinished
|
info.finished = mFinished
|
||||||
info.downloaded = mDownloaded
|
info.downloaded = mDownloaded
|
||||||
info.total = mTotal
|
info.total = mTotal
|
||||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
if (list != null) {
|
|
||||||
for (l in mDownloadInfoListeners) {
|
|
||||||
l.onUpdate(info, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1036,12 +951,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
if (mDownloadListener != null) {
|
if (mDownloadListener != null) {
|
||||||
mDownloadListener!!.onFinish(info)
|
mDownloadListener!!.onFinish(info)
|
||||||
}
|
}
|
||||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
if (list != null) {
|
|
||||||
for (l in mDownloadInfoListeners) {
|
|
||||||
l.onUpdate(info, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Start next download
|
// Start next download
|
||||||
ensureDownload()
|
ensureDownload()
|
||||||
}
|
}
|
||||||
@@ -1127,12 +1037,7 @@ object DownloadManager : OnSpiderListener {
|
|||||||
if (mDownloadListener != null) {
|
if (mDownloadListener != null) {
|
||||||
mDownloadListener!!.onDownload(info)
|
mDownloadListener!!.onDownload(info)
|
||||||
}
|
}
|
||||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
mDownloadInfoListener?.onUpdate(info)
|
||||||
if (list != null) {
|
|
||||||
for (l in mDownloadInfoListeners) {
|
|
||||||
l.onUpdate(info, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
mBytesRead = 0
|
mBytesRead = 0
|
||||||
if (!mStop) {
|
if (!mStop) {
|
||||||
|
|||||||
@@ -135,9 +135,9 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
|
|||||||
): Boolean {
|
): Boolean {
|
||||||
return if (isCronetSupported) {
|
return if (isCronetSupported) {
|
||||||
cronetRequest(url, referer) {
|
cronetRequest(url, referer) {
|
||||||
disableCache()
|
setCacheDisabled(true)
|
||||||
}.execute { info ->
|
}.execute { info ->
|
||||||
val headers = info.allHeaders
|
val headers = info.headers.asMap
|
||||||
val type = headers["Content-Type"]?.first()?.toMediaType()?.subtype ?: "jpg"
|
val type = headers["Content-Type"]?.first()?.toMediaType()?.subtype ?: "jpg"
|
||||||
val length = headers["Content-Length"]!!.first().toLong()
|
val length = headers["Content-Length"]!!.first().toLong()
|
||||||
saveResponseMeta(index, type, length) { file ->
|
saveResponseMeta(index, type, length) { file ->
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -71,7 +72,7 @@ class SearchLayout @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mdc3Theme {
|
Mdc3Theme {
|
||||||
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_layout_margin_h)).verticalScroll(rememberScrollState())) {
|
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_layout_margin_h)).imePadding().verticalScroll(rememberScrollState())) {
|
||||||
AnimatedVisibility(visible = isNormalMode) {
|
AnimatedVisibility(visible = isNormalMode) {
|
||||||
ElevatedCard(modifier = Modifier.fillMaxWidth().padding(vertical = dimensionResource(id = R.dimen.search_layout_margin_v))) {
|
ElevatedCard(modifier = Modifier.fillMaxWidth().padding(vertical = dimensionResource(id = R.dimen.search_layout_margin_v))) {
|
||||||
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
|
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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 com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
|
import com.hippo.ehviewer.util.toIntOrDefault
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchAdvanced(
|
fun SearchAdvanced(
|
||||||
@@ -55,9 +56,6 @@ fun SearchAdvanced(
|
|||||||
onExpandedChange = { expanded = !expanded },
|
onExpandedChange = { expanded = !expanded },
|
||||||
) {
|
) {
|
||||||
val softwareKeyboardController = LocalSoftwareKeyboardController.current
|
val softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
SideEffect {
|
|
||||||
softwareKeyboardController?.hide()
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.menuAnchor(),
|
modifier = Modifier.menuAnchor(),
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
@@ -71,6 +69,9 @@ fun SearchAdvanced(
|
|||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
) {
|
) {
|
||||||
|
SideEffect {
|
||||||
|
softwareKeyboardController?.hide()
|
||||||
|
}
|
||||||
minRatingItems.forEach { selectionOption ->
|
minRatingItems.forEach { selectionOption ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(selectionOption) },
|
text = { Text(selectionOption) },
|
||||||
@@ -88,18 +89,18 @@ fun SearchAdvanced(
|
|||||||
Checkbox(checked = enabled, onCheckedChange = { enabled = it })
|
Checkbox(checked = enabled, onCheckedChange = { enabled = it })
|
||||||
Text(text = stringResource(id = R.string.search_sp), modifier = Modifier.align(Alignment.CenterVertically))
|
Text(text = stringResource(id = R.string.search_sp), modifier = Modifier.align(Alignment.CenterVertically))
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
||||||
value = if (enabled && state.fromPage != -1) state.fromPage.toString() else "",
|
value = if (enabled && state.fromPage != -1) state.fromPage.toString() else "",
|
||||||
onValueChange = { onStateChanged(state.copy(fromPage = it.toInt())) },
|
onValueChange = { onStateChanged(state.copy(fromPage = it.toIntOrDefault(-1))) },
|
||||||
modifier = Modifier.width(96.dp).padding(16.dp),
|
modifier = Modifier.width(96.dp).padding(16.dp),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
)
|
)
|
||||||
Text(text = stringResource(id = R.string.search_sp_to), modifier = Modifier.align(Alignment.CenterVertically))
|
Text(text = stringResource(id = R.string.search_sp_to), modifier = Modifier.align(Alignment.CenterVertically))
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
||||||
value = if (enabled && state.toPage != -1) state.toPage.toString() else "",
|
value = if (enabled && state.toPage != -1) state.toPage.toString() else "",
|
||||||
onValueChange = { onStateChanged(state.copy(toPage = it.toInt())) },
|
onValueChange = { onStateChanged(state.copy(toPage = it.toIntOrDefault(-1))) },
|
||||||
modifier = Modifier.width(96.dp).padding(16.dp),
|
modifier = Modifier.width(96.dp).padding(16.dp),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.ProvideTextStyle
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
import androidx.compose.material3.ShapeDefaults
|
import androidx.compose.material3.ShapeDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
@@ -28,12 +29,14 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||||
@@ -109,6 +112,7 @@ fun GalleryInfoListItem(
|
|||||||
start.linkTo(parent.start)
|
start.linkTo(parent.start)
|
||||||
bottom.linkTo(parent.bottom)
|
bottom.linkTo(parent.bottom)
|
||||||
}.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
|
}.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
|
||||||
|
color = if (Settings.harmonizeCategoryColor) Color.Unspecified else EhUtils.categoryTextColor,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
@@ -198,10 +202,11 @@ fun GalleryInfoGridItem(
|
|||||||
modifier = Modifier.aspectRatio(aspect).fillMaxWidth(),
|
modifier = Modifier.aspectRatio(aspect).fillMaxWidth(),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
)
|
)
|
||||||
|
val categoryColor = EhUtils.getCategoryColor(info.category)
|
||||||
Badge(
|
Badge(
|
||||||
modifier = Modifier.align(Alignment.TopEnd).width(32.dp).height(24.dp),
|
modifier = Modifier.align(Alignment.TopEnd).width(32.dp).height(24.dp),
|
||||||
containerColor = EhUtils.getCategoryColor(info.category),
|
containerColor = categoryColor,
|
||||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
contentColor = if (Settings.harmonizeCategoryColor) contentColorFor(categoryColor) else EhUtils.categoryTextColor,
|
||||||
) {
|
) {
|
||||||
simpleLang?.let {
|
simpleLang?.let {
|
||||||
Text(text = it.uppercase())
|
Text(text = it.uppercase())
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ import androidx.core.view.GravityCompat
|
|||||||
import androidx.core.view.SoftwareKeyboardControllerCompat
|
import androidx.core.view.SoftwareKeyboardControllerCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||||
|
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
||||||
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||||
import com.google.android.material.sidesheet.SideSheetDialog
|
import com.google.android.material.sidesheet.SideSheetDialog
|
||||||
import com.hippo.ehviewer.ui.MainActivity
|
import com.hippo.ehviewer.ui.MainActivity
|
||||||
@@ -155,8 +158,10 @@ abstract class BaseScene : Fragment() {
|
|||||||
createDrawerView(savedInstanceState)?.let {
|
createDrawerView(savedInstanceState)?.let {
|
||||||
sideSheetDialog = SideSheetDialog(requireContext()).apply {
|
sideSheetDialog = SideSheetDialog(requireContext()).apply {
|
||||||
window?.decorView?.apply {
|
window?.decorView?.apply {
|
||||||
setViewTreeLifecycleOwner(viewLifecycleOwner)
|
val owner = viewLifecycleOwner
|
||||||
setViewTreeSavedStateRegistryOwner(this@BaseScene)
|
setViewTreeLifecycleOwner(owner)
|
||||||
|
setViewTreeViewModelStoreOwner(owner as ViewModelStoreOwner)
|
||||||
|
setViewTreeSavedStateRegistryOwner(owner as SavedStateRegistryOwner)
|
||||||
}
|
}
|
||||||
setContentView(it)
|
setContentView(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ 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.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -55,9 +56,10 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
import arrow.core.partially1
|
import arrow.core.partially1
|
||||||
@@ -79,7 +81,6 @@ import com.hippo.ehviewer.databinding.ItemDownloadBinding
|
|||||||
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
|
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
|
||||||
import com.hippo.ehviewer.databinding.SceneDownloadBinding
|
import com.hippo.ehviewer.databinding.SceneDownloadBinding
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
|
|
||||||
import com.hippo.ehviewer.download.DownloadService
|
import com.hippo.ehviewer.download.DownloadService
|
||||||
import com.hippo.ehviewer.download.DownloadService.Companion.clear
|
import com.hippo.ehviewer.download.DownloadService.Companion.clear
|
||||||
import com.hippo.ehviewer.download.downloadDir
|
import com.hippo.ehviewer.download.downloadDir
|
||||||
@@ -102,6 +103,7 @@ import com.hippo.ehviewer.ui.tools.CropDefaults
|
|||||||
import com.hippo.ehviewer.ui.tools.DialogState
|
import com.hippo.ehviewer.ui.tools.DialogState
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.LongList
|
import com.hippo.ehviewer.util.LongList
|
||||||
|
import com.hippo.ehviewer.util.containsIgnoreCase
|
||||||
import com.hippo.ehviewer.util.sendTo
|
import com.hippo.ehviewer.util.sendTo
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
@@ -110,20 +112,21 @@ import eu.kanade.tachiyomi.util.lang.launchUI
|
|||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.system.pxToDp
|
import eu.kanade.tachiyomi.util.system.pxToDp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import rikka.core.res.resolveColor
|
import rikka.core.res.resolveColor
|
||||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||||
|
|
||||||
@SuppressLint("RtlHardcoded")
|
@SuppressLint("RtlHardcoded")
|
||||||
class DownloadsScene :
|
class DownloadsScene :
|
||||||
BaseToolbarScene(),
|
SearchBarScene(),
|
||||||
DownloadInfoListener,
|
|
||||||
OnClickFabListener {
|
OnClickFabListener {
|
||||||
/*---------------
|
/*---------------
|
||||||
Whole life cycle
|
Whole life cycle
|
||||||
---------------*/
|
---------------*/
|
||||||
|
private var mKeyword: String? = null
|
||||||
private var mLabel: String? = null
|
private var mLabel: String? = null
|
||||||
private var mList: List<DownloadInfo>? = null
|
private val mList get() = mAdapter?.currentList
|
||||||
|
|
||||||
/*---------------
|
/*---------------
|
||||||
View life cycle
|
View life cycle
|
||||||
@@ -134,20 +137,23 @@ class DownloadsScene :
|
|||||||
private var mAdapter: DownloadAdapter? = null
|
private var mAdapter: DownloadAdapter? = null
|
||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||||
private val tracker get() = mAdapter!!.tracker!!
|
private val tracker get() = mAdapter!!.tracker!!
|
||||||
private var mInitPosition = -1
|
private var mGid = -1L
|
||||||
private var mLabelAdapter: DownloadLabelAdapter? = null
|
private var mLabelAdapter: DownloadLabelAdapter? = null
|
||||||
private lateinit var mLabels: MutableList<String>
|
private lateinit var mLabels: MutableList<String>
|
||||||
private var mType = -1
|
private var mType = -1
|
||||||
|
|
||||||
private val dialogState = DialogState()
|
private val dialogState = DialogState()
|
||||||
|
|
||||||
|
override val fabLayout get() = binding.fabLayout
|
||||||
|
|
||||||
private fun initLabels() {
|
private fun initLabels() {
|
||||||
context ?: return
|
context ?: return
|
||||||
val listLabel = downloadManager.labelList
|
val listLabel = downloadManager.labelList
|
||||||
mLabels = ArrayList(listLabel.size + LABEL_OFFSET)
|
|
||||||
// Add "All" and "Default" label names
|
// Add "All" and "Default" label names
|
||||||
mLabels.add(getString(R.string.download_all))
|
mLabels = arrayListOf(
|
||||||
mLabels.add(getString(R.string.default_download_label_name))
|
getString(R.string.download_all),
|
||||||
|
getString(R.string.default_download_label_name),
|
||||||
|
)
|
||||||
listLabel.forEach {
|
listLabel.forEach {
|
||||||
mLabels.add(it.label)
|
mLabels.add(it.label)
|
||||||
}
|
}
|
||||||
@@ -165,18 +171,7 @@ class DownloadsScene :
|
|||||||
val info = DownloadManager.getDownloadInfo(gid)
|
val info = DownloadManager.getDownloadInfo(gid)
|
||||||
if (null != info) {
|
if (null != info) {
|
||||||
mLabel = info.label
|
mLabel = info.label
|
||||||
updateForLabel()
|
mGid = gid
|
||||||
updateView()
|
|
||||||
|
|
||||||
// Get position
|
|
||||||
if (null != mList) {
|
|
||||||
val position = mList!!.indexOf(info)
|
|
||||||
if (position >= 0 && null != _binding) {
|
|
||||||
binding.recyclerView.scrollToPosition(position)
|
|
||||||
} else {
|
|
||||||
mInitPosition = position
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +180,6 @@ class DownloadsScene :
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
DownloadManager.addDownloadInfoListener(this)
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
onInit()
|
onInit()
|
||||||
} else {
|
} else {
|
||||||
@@ -193,50 +187,54 @@ class DownloadsScene :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
private fun filter(info: DownloadInfo) = info.run {
|
||||||
super.onDestroy()
|
(mType == -1 || state == mType) &&
|
||||||
mList = null
|
mKeyword?.let {
|
||||||
DownloadManager.removeDownloadInfoListener(this)
|
title.containsIgnoreCase(it) || titleJpn.containsIgnoreCase(it) ||
|
||||||
|
uploader.containsIgnoreCase(it)
|
||||||
|
} ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateInfoList() {
|
||||||
|
mAdapter?.run {
|
||||||
|
val list = when (mLabel) {
|
||||||
|
null -> DownloadManager.allInfoList
|
||||||
|
getString(R.string.default_download_label_name) -> DownloadManager.defaultInfoList
|
||||||
|
else -> DownloadManager.getLabelDownloadInfoList(mLabel)
|
||||||
|
?: DownloadManager.allInfoList.also { mLabel = null }
|
||||||
|
}.mapNotNull { it.takeIf(::filter)?.copy(downloadInfo = it.downloadInfo.copy()) }
|
||||||
|
submitList(list) {
|
||||||
|
if (mGid != -1L) {
|
||||||
|
val position = list.indexOfFirst { it.gid == mGid }
|
||||||
|
if (position != -1) {
|
||||||
|
binding.recyclerView.scrollToPosition(position)
|
||||||
|
}
|
||||||
|
mGid = -1L
|
||||||
|
}
|
||||||
|
updateView()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
private fun updateForLabel() {
|
private fun updateForLabel() {
|
||||||
val list = when (mLabel) {
|
updateInfoList()
|
||||||
null -> DownloadManager.allInfoList
|
|
||||||
getString(R.string.default_download_label_name) -> DownloadManager.defaultInfoList
|
|
||||||
else -> DownloadManager.getLabelDownloadInfoList(mLabel)
|
|
||||||
?: DownloadManager.allInfoList.also { mLabel = null }
|
|
||||||
}
|
|
||||||
mList = if (mType != -1) {
|
|
||||||
list.filter { it.state == mType }
|
|
||||||
} else {
|
|
||||||
list
|
|
||||||
}
|
|
||||||
if (mAdapter != null) {
|
|
||||||
mAdapter!!.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
Settings.recentDownloadLabel = mLabel
|
Settings.recentDownloadLabel = mLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTitle() {
|
private fun updateTitle() {
|
||||||
setTitle(
|
val title = getString(R.string.scene_download_title, mLabel ?: getString(R.string.download_all))
|
||||||
getString(
|
setSearchBarHint(title)
|
||||||
R.string.scene_download_title,
|
setEditTextHint(getString(R.string.search_bar_hint, title))
|
||||||
if (mLabel != null) mLabel else getString(R.string.download_all),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onInit() {
|
private fun onInit() {
|
||||||
if (!handleArguments(arguments)) {
|
if (!handleArguments(arguments)) {
|
||||||
mLabel = Settings.recentDownloadLabel
|
mLabel = Settings.recentDownloadLabel
|
||||||
updateForLabel()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRestore(savedInstanceState: Bundle) {
|
private fun onRestore(savedInstanceState: Bundle) {
|
||||||
mLabel = savedInstanceState.getString(KEY_LABEL)
|
mLabel = savedInstanceState.getString(KEY_LABEL)
|
||||||
updateForLabel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -300,20 +298,14 @@ class DownloadsScene :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
recyclerView.addItemDecoration(decoration)
|
recyclerView.addItemDecoration(decoration)
|
||||||
if (mInitPosition >= 0) {
|
|
||||||
recyclerView.scrollToPosition(mInitPosition)
|
|
||||||
mInitPosition = -1
|
|
||||||
}
|
|
||||||
mItemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
|
mItemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
|
||||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
|
||||||
super.onSelectedChanged(viewHolder, actionState)
|
|
||||||
if (actionState == ACTION_STATE_DRAG) {
|
|
||||||
tracker.clearSelection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun isLongPressDragEnabled() = false
|
override fun isLongPressDragEnabled() = false
|
||||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||||
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT)
|
return if (tracker.isInCustomChoice) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override fun onMove(
|
override fun onMove(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
@@ -325,12 +317,10 @@ class DownloadsScene :
|
|||||||
if (fromPosition == toPosition) {
|
if (fromPosition == toPosition) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val list = when (mLabel) {
|
val fromItem = mList!![fromPosition]
|
||||||
null -> DownloadManager.moveDownload(fromPosition, toPosition)
|
val toItem = mList!![toPosition]
|
||||||
getString(R.string.default_download_label_name) -> DownloadManager.moveDownload(null, fromPosition, toPosition)
|
val list = DownloadManager.moveDownload(fromItem, toItem)
|
||||||
else -> DownloadManager.moveDownload(mLabel, fromPosition, toPosition)
|
updateInfoList()
|
||||||
}
|
|
||||||
mAdapter!!.notifyItemMoved(fromPosition, toPosition)
|
|
||||||
lifecycleScope.launchIO {
|
lifecycleScope.launchIO {
|
||||||
EhDB.updateDownloadInfo(list)
|
EhDB.updateDownloadInfo(list)
|
||||||
}
|
}
|
||||||
@@ -343,7 +333,6 @@ class DownloadsScene :
|
|||||||
dialogState.confirmRemoveDownload(info) {
|
dialogState.confirmRemoveDownload(info) {
|
||||||
mAdapter!!.notifyItemChanged(position)
|
mAdapter!!.notifyItemChanged(position)
|
||||||
}
|
}
|
||||||
mAdapter!!.notifyItemRemoved(position)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -358,15 +347,23 @@ class DownloadsScene :
|
|||||||
fabLayout.setAutoCancel(false)
|
fabLayout.setAutoCancel(false)
|
||||||
fabLayout.setOnClickFabListener(this@DownloadsScene)
|
fabLayout.setOnClickFabListener(this@DownloadsScene)
|
||||||
addAboveSnackView(fabLayout)
|
addAboveSnackView(fabLayout)
|
||||||
updateView()
|
updateForLabel()
|
||||||
|
}
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
DownloadManager.stateFlow().collectLatest {
|
||||||
|
updateInfoList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOnApplySearch {
|
||||||
|
mKeyword = it.takeUnless { it.isEmpty() }
|
||||||
|
updateInfoList()
|
||||||
}
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onSearchViewExpanded() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
hideActionFab(true)
|
||||||
updateTitle()
|
super.onSearchViewExpanded()
|
||||||
setNavigationIcon(R.drawable.ic_baseline_menu_24)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -402,8 +399,7 @@ class DownloadsScene :
|
|||||||
mType + 1,
|
mType + 1,
|
||||||
) { dialog: DialogInterface, which: Int ->
|
) { dialog: DialogInterface, which: Int ->
|
||||||
mType = which - 1
|
mType = which - 1
|
||||||
updateForLabel()
|
updateInfoList()
|
||||||
updateView()
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -456,7 +452,7 @@ class DownloadsScene :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateView() {
|
private fun updateView() {
|
||||||
if (mViewTransition != null) {
|
if (mViewTransition != null) {
|
||||||
if (mList.isNullOrEmpty()) {
|
if (mList.isNullOrEmpty()) {
|
||||||
mViewTransition!!.showView(1)
|
mViewTransition!!.showView(1)
|
||||||
@@ -604,61 +600,6 @@ class DownloadsScene :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
|
|
||||||
if (mList !== list) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lifecycleScope.launchUI {
|
|
||||||
if (mAdapter != null) {
|
|
||||||
mAdapter!!.notifyItemInserted(position)
|
|
||||||
}
|
|
||||||
updateView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
|
|
||||||
if (mLabel != null && mList !== list) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val index = mList!!.indexOf(info)
|
|
||||||
lifecycleScope.launchUI {
|
|
||||||
if (index >= 0 && mAdapter != null) {
|
|
||||||
mAdapter!!.notifyItemChanged(index, PAYLOAD_STATE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
override fun onUpdateAll() {
|
|
||||||
lifecycleScope.launchUI {
|
|
||||||
if (mAdapter != null) {
|
|
||||||
mAdapter!!.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
override fun onReload() {
|
|
||||||
lifecycleScope.launchUI {
|
|
||||||
if (mAdapter != null) {
|
|
||||||
mAdapter!!.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
updateView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
|
|
||||||
if (mList !== list) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lifecycleScope.launchUI {
|
|
||||||
if (mAdapter != null) {
|
|
||||||
mAdapter!!.notifyItemRemoved(position)
|
|
||||||
}
|
|
||||||
updateView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DownloadLabelHolder(val binding: ItemDrawerListBinding) :
|
private class DownloadLabelHolder(val binding: ItemDrawerListBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root)
|
RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
@@ -669,17 +610,14 @@ class DownloadsScene :
|
|||||||
DownloadLabelHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
|
DownloadLabelHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
|
||||||
holder.itemView.setOnClickListener {
|
holder.itemView.setOnClickListener {
|
||||||
val position = holder.bindingAdapterPosition
|
val position = holder.bindingAdapterPosition
|
||||||
val label1: String? = if (position == 0) {
|
val label = mLabels[position].takeUnless { position == 0 }
|
||||||
null
|
if (mLabel != label) {
|
||||||
} else {
|
mKeyword = null
|
||||||
mLabels[position]
|
setSearchBarText(null)
|
||||||
}
|
mLabel = label
|
||||||
if (mLabel != label1) {
|
|
||||||
mLabel = label1
|
|
||||||
updateForLabel()
|
updateForLabel()
|
||||||
updateView()
|
|
||||||
closeSideSheet()
|
|
||||||
}
|
}
|
||||||
|
closeSideSheet()
|
||||||
}
|
}
|
||||||
holder.binding.edit.setOnClickListener {
|
holder.binding.edit.setOnClickListener {
|
||||||
val context = context
|
val context = context
|
||||||
@@ -717,7 +655,7 @@ class DownloadsScene :
|
|||||||
}
|
}
|
||||||
holder.binding.run {
|
holder.binding.run {
|
||||||
if (list != null) {
|
if (list != null) {
|
||||||
text.text = label + " [" + list.size + "]"
|
text.text = "$label [${list.size}]"
|
||||||
} else {
|
} else {
|
||||||
text.text = label
|
text.text = label
|
||||||
}
|
}
|
||||||
@@ -872,6 +810,7 @@ class DownloadsScene :
|
|||||||
Text(
|
Text(
|
||||||
text = categoryText,
|
text = categoryText,
|
||||||
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
|
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
|
||||||
|
color = if (Settings.harmonizeCategoryColor) Color.Unspecified else EhUtils.categoryTextColor,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
)
|
)
|
||||||
@@ -961,17 +900,23 @@ class DownloadsScene :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class DownloadAdapter : RecyclerView.Adapter<DownloadHolder>() {
|
private val diffCallback = object : DiffUtil.ItemCallback<DownloadInfo>() {
|
||||||
|
override fun areItemsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
|
||||||
|
override fun areContentsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem == newItem
|
||||||
|
override fun getChangePayload(oldItem: DownloadInfo, newItem: DownloadInfo): Any? {
|
||||||
|
return if (oldItem.downloadInfo != newItem.downloadInfo) {
|
||||||
|
PAYLOAD_DOWNLOAD_INFO
|
||||||
|
} else {
|
||||||
|
super.getChangePayload(oldItem, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DownloadAdapter : ListAdapter<DownloadInfo, DownloadHolder>(diffCallback) {
|
||||||
private val mInflater: LayoutInflater = layoutInflater
|
private val mInflater: LayoutInflater = layoutInflater
|
||||||
var tracker: GallerySelectionTracker<DownloadInfo>? = null
|
var tracker: GallerySelectionTracker<DownloadInfo>? = null
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
override fun getItemId(position: Int) = getItem(position).gid
|
||||||
return if (mList == null || position < 0 || position >= mList!!.size) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
mList!![position].gid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
|
||||||
val holder = DownloadHolder(ItemDownloadBinding.inflate(mInflater, parent, false))
|
val holder = DownloadHolder(ItemDownloadBinding.inflate(mInflater, parent, false))
|
||||||
@@ -980,10 +925,8 @@ class DownloadsScene :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
|
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
|
||||||
mList?.let {
|
val info = getItem(position)
|
||||||
val info = it[position]
|
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
|
||||||
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
override fun onBindViewHolder(
|
||||||
@@ -993,18 +936,15 @@ class DownloadsScene :
|
|||||||
) {
|
) {
|
||||||
payloads.forEach { payload ->
|
payloads.forEach { payload ->
|
||||||
when (payload) {
|
when (payload) {
|
||||||
PAYLOAD_STATE -> {
|
PAYLOAD_DOWNLOAD_INFO -> {
|
||||||
mList?.let { holder.bindForState(it[position]) }
|
val info = getItem(position)
|
||||||
|
holder.bindForState(info)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
super.onBindViewHolder(holder, position, payloads)
|
super.onBindViewHolder(holder, position, payloads)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return if (mList == null) 0 else mList!!.size
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class RenameLabelDialogHelper(
|
private inner class RenameLabelDialogHelper(
|
||||||
@@ -1039,7 +979,6 @@ class DownloadsScene :
|
|||||||
if (mLabel == mOriginalLabel) {
|
if (mLabel == mOriginalLabel) {
|
||||||
mLabel = text
|
mLabel = text
|
||||||
updateForLabel()
|
updateForLabel()
|
||||||
updateView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1128,7 +1067,6 @@ class DownloadsScene :
|
|||||||
withUIContext {
|
withUIContext {
|
||||||
mLabel = null
|
mLabel = null
|
||||||
updateForLabel()
|
updateForLabel()
|
||||||
updateView()
|
|
||||||
mLabelAdapter!!.notifyItemRemoved(position)
|
mLabelAdapter!!.notifyItemRemoved(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1146,7 +1084,7 @@ class DownloadsScene :
|
|||||||
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
|
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
|
||||||
private const val KEY_LABEL = "label"
|
private const val KEY_LABEL = "label"
|
||||||
private const val LABEL_OFFSET = 2
|
private const val LABEL_OFFSET = 2
|
||||||
private const val PAYLOAD_STATE = 0
|
private const val PAYLOAD_DOWNLOAD_INFO = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ import com.hippo.ehviewer.util.getValue
|
|||||||
import com.hippo.ehviewer.util.lazyMut
|
import com.hippo.ehviewer.util.lazyMut
|
||||||
import com.hippo.ehviewer.util.setValue
|
import com.hippo.ehviewer.util.setValue
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import rikka.core.res.resolveColor
|
import rikka.core.res.resolveColor
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -202,7 +202,7 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
} else {
|
} else {
|
||||||
setSearchBarHint(getString(R.string.favorites_title_2, favCatName, keyword))
|
setSearchBarHint(getString(R.string.favorites_title_2, favCatName, keyword))
|
||||||
}
|
}
|
||||||
setEditTextHint(getString(R.string.favorites_search_bar_hint, favCatName))
|
setEditTextHint(getString(R.string.search_bar_hint, favCatName))
|
||||||
Settings.recentFavCat = urlBuilder.favCat
|
Settings.recentFavCat = urlBuilder.favCat
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,8 +336,8 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
|
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
|
||||||
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
|
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
|
||||||
val empty = getString(R.string.gallery_list_empty_hit)
|
val empty = getString(R.string.gallery_list_empty_hit)
|
||||||
adapter.addLoadStateListener {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
lifecycleScope.launchUI {
|
adapter.loadStateFlow.collectLatest {
|
||||||
when (val state = it.refresh) {
|
when (val state = it.refresh) {
|
||||||
is LoadState.Loading -> {
|
is LoadState.Loading -> {
|
||||||
showSearchBar()
|
showSearchBar()
|
||||||
@@ -392,7 +392,11 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
}
|
}
|
||||||
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
|
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
|
||||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||||
return makeMovementFlags(0, ItemTouchHelper.LEFT)
|
return if (tracker.isInCustomChoice) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
makeMovementFlags(0, ItemTouchHelper.LEFT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override fun onMove(
|
override fun onMove(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
@@ -420,7 +424,6 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
|
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
|
||||||
allowEmptySearch = false
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,13 +482,13 @@ class FavoritesScene : SearchBarScene() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearchViewExpanded() {
|
override fun onSearchViewExpanded() {
|
||||||
|
hideActionFab(true)
|
||||||
super.onSearchViewExpanded()
|
super.onSearchViewExpanded()
|
||||||
selectSearchFab(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearchViewHidden() {
|
override fun onSearchViewHidden() {
|
||||||
super.onSearchViewHidden()
|
super.onSearchViewHidden()
|
||||||
selectActionFab(true)
|
showActionFab(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showGoToDialog() {
|
private fun showGoToDialog() {
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ import arrow.core.partially1
|
|||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
|
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
|
||||||
|
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
||||||
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
|
||||||
@@ -122,6 +123,7 @@ import com.hippo.ehviewer.client.data.GalleryTagGroup
|
|||||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||||
import com.hippo.ehviewer.client.exception.EhException
|
import com.hippo.ehviewer.client.exception.EhException
|
||||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||||
|
import com.hippo.ehviewer.client.getImageKey
|
||||||
import com.hippo.ehviewer.client.parser.ArchiveParser
|
import com.hippo.ehviewer.client.parser.ArchiveParser
|
||||||
import com.hippo.ehviewer.client.parser.HomeParser
|
import com.hippo.ehviewer.client.parser.HomeParser
|
||||||
import com.hippo.ehviewer.client.parser.TorrentResult
|
import com.hippo.ehviewer.client.parser.TorrentResult
|
||||||
@@ -341,6 +343,24 @@ class GalleryDetailScene : BaseScene() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun actionClearCache() {
|
||||||
|
val gd = composeBindingGD ?: return
|
||||||
|
lifecycleScope.launchIO {
|
||||||
|
dialogState.awaitPermissionOrCancel(
|
||||||
|
confirmText = R.string.clear_all,
|
||||||
|
dismissText = android.R.string.cancel,
|
||||||
|
title = R.string.clear_image_cache,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.clear_image_cache_confirm))
|
||||||
|
}
|
||||||
|
(0..<gd.pages).forEach {
|
||||||
|
val key = getImageKey(gd.gid, it)
|
||||||
|
imageCache.remove(key)
|
||||||
|
}
|
||||||
|
showTip(R.string.image_cache_cleared, LENGTH_SHORT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun actionOpenInOtherApp() {
|
private fun actionOpenInOtherApp() {
|
||||||
val url = galleryDetailUrl ?: return
|
val url = galleryDetailUrl ?: return
|
||||||
val activity: Activity = mainActivity ?: return
|
val activity: Activity = mainActivity ?: return
|
||||||
@@ -466,6 +486,13 @@ class GalleryDetailScene : BaseScene() {
|
|||||||
actionRefresh()
|
actionRefresh()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(id = R.string.clear_image_cache)) },
|
||||||
|
onClick = {
|
||||||
|
dropdown = false
|
||||||
|
actionClearCache()
|
||||||
|
},
|
||||||
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = stringResource(id = R.string.open_in_other_app)) },
|
text = { Text(text = stringResource(id = R.string.open_in_other_app)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
@@ -104,11 +104,9 @@ import com.hippo.ehviewer.util.getValue
|
|||||||
import com.hippo.ehviewer.util.lazyMut
|
import com.hippo.ehviewer.util.lazyMut
|
||||||
import com.hippo.ehviewer.util.setValue
|
import com.hippo.ehviewer.util.setValue
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -365,10 +363,8 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
|
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
|
||||||
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
|
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
|
||||||
val empty = getString(R.string.gallery_list_empty_hit)
|
val empty = getString(R.string.gallery_list_empty_hit)
|
||||||
val noWatch = getString(R.string.gallery_list_empty_hit_subscription)
|
launch {
|
||||||
adapter.addLoadStateListener {
|
adapter.loadStateFlow.collectLatest {
|
||||||
viewLifecycleOwner.lifecycleScope.launchUI {
|
|
||||||
ensureActive()
|
|
||||||
when (val state = it.refresh) {
|
when (val state = it.refresh) {
|
||||||
is LoadState.Loading -> {
|
is LoadState.Loading -> {
|
||||||
showSearchBar()
|
showSearchBar()
|
||||||
@@ -395,11 +391,7 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
delay(500)
|
delay(500)
|
||||||
binding.refreshLayout.isRefreshing = false
|
binding.refreshLayout.isRefreshing = false
|
||||||
if (mAdapter?.itemCount == 0) {
|
if (mAdapter?.itemCount == 0) {
|
||||||
if (mUrlBuilder.mode == MODE_SUBSCRIPTION) {
|
binding.tip.text = empty
|
||||||
binding.tip.text = noWatch
|
|
||||||
} else {
|
|
||||||
binding.tip.text = empty
|
|
||||||
}
|
|
||||||
transition.showView(2)
|
transition.showView(2)
|
||||||
} else {
|
} else {
|
||||||
transition.showView(0)
|
transition.showView(0)
|
||||||
@@ -695,13 +687,21 @@ class GalleryListScene : SearchBarScene() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearchViewExpanded() {
|
override fun onSearchViewExpanded() {
|
||||||
|
if (mState == State.NORMAL) {
|
||||||
|
hideActionFab(true)
|
||||||
|
} else {
|
||||||
|
hideSearchFab(true)
|
||||||
|
}
|
||||||
super.onSearchViewExpanded()
|
super.onSearchViewExpanded()
|
||||||
if (mState == State.NORMAL) selectSearchFab(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearchViewHidden() {
|
override fun onSearchViewHidden() {
|
||||||
super.onSearchViewHidden()
|
super.onSearchViewHidden()
|
||||||
if (mState == State.NORMAL) selectActionFab(true)
|
if (mState == State.NORMAL) {
|
||||||
|
showActionFab(true)
|
||||||
|
} else {
|
||||||
|
showSearchFab(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showActionFab() {
|
private fun showActionFab() {
|
||||||
|
|||||||
@@ -164,56 +164,43 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
|||||||
privLockModeStart = null
|
privLockModeStart = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectSearchFab(animation: Boolean) {
|
fun showSearchFab(animation: Boolean, delay: Long = 0) {
|
||||||
_binding ?: return
|
|
||||||
mShowActionFab = false
|
|
||||||
if (animation) {
|
if (animation) {
|
||||||
val fab: View? = fabLayout.primaryFab
|
|
||||||
val delay: Long
|
|
||||||
if (View.INVISIBLE == fab!!.visibility) {
|
|
||||||
delay = 0L
|
|
||||||
} else {
|
|
||||||
delay = ANIMATE_TIME
|
|
||||||
fabLayout.setExpanded(expanded = false, animation = true)
|
|
||||||
fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)
|
|
||||||
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
|
||||||
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
|
||||||
}
|
|
||||||
mSearchFab.visibility = View.VISIBLE
|
mSearchFab.visibility = View.VISIBLE
|
||||||
mSearchFab.rotation = -45.0f
|
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
|
||||||
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
|
|
||||||
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
||||||
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
||||||
} else {
|
} else {
|
||||||
fabLayout.setExpanded(expanded = false, animation = false)
|
|
||||||
val fab = fabLayout.primaryFab!!
|
|
||||||
fab.visibility = View.INVISIBLE
|
|
||||||
fab.scaleX = 0.0f
|
|
||||||
fab.scaleY = 0.0f
|
|
||||||
mSearchFab.visibility = View.VISIBLE
|
mSearchFab.visibility = View.VISIBLE
|
||||||
mSearchFab.scaleX = 1.0f
|
mSearchFab.scaleX = 1.0f
|
||||||
mSearchFab.scaleY = 1.0f
|
mSearchFab.scaleY = 1.0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectActionFab(animation: Boolean) {
|
fun hideSearchFab(animation: Boolean): Long {
|
||||||
_binding ?: return
|
return if (View.INVISIBLE == mSearchFab.visibility) {
|
||||||
mShowActionFab = true
|
0L
|
||||||
if (animation) {
|
} else {
|
||||||
val delay: Long
|
if (animation) {
|
||||||
if (View.INVISIBLE == mSearchFab.visibility) {
|
|
||||||
delay = 0L
|
|
||||||
} else {
|
|
||||||
delay = ANIMATE_TIME
|
|
||||||
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
|
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
|
||||||
.setListener(mSearchFabAnimatorListener)
|
.setListener(mSearchFabAnimatorListener)
|
||||||
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
||||||
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
||||||
|
} else {
|
||||||
|
mSearchFab.visibility = View.INVISIBLE
|
||||||
|
mSearchFab.scaleX = 0.0f
|
||||||
|
mSearchFab.scaleY = 0.0f
|
||||||
}
|
}
|
||||||
|
ANIMATE_TIME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showActionFab(animation: Boolean, delay: Long = ANIMATE_TIME) {
|
||||||
|
mShowActionFab = true
|
||||||
|
if (animation) {
|
||||||
val fab = fabLayout.primaryFab!!
|
val fab = fabLayout.primaryFab!!
|
||||||
fab.visibility = View.VISIBLE
|
fab.visibility = View.VISIBLE
|
||||||
fab.rotation = -45.0f
|
fab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
|
||||||
fab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
|
|
||||||
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
||||||
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
||||||
} else {
|
} else {
|
||||||
@@ -222,12 +209,42 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
|||||||
fab.visibility = View.VISIBLE
|
fab.visibility = View.VISIBLE
|
||||||
fab.scaleX = 1.0f
|
fab.scaleX = 1.0f
|
||||||
fab.scaleY = 1.0f
|
fab.scaleY = 1.0f
|
||||||
mSearchFab.visibility = View.INVISIBLE
|
|
||||||
mSearchFab.scaleX = 0.0f
|
|
||||||
mSearchFab.scaleY = 0.0f
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideActionFab(animation: Boolean): Long {
|
||||||
|
mShowActionFab = false
|
||||||
|
return if (animation) {
|
||||||
|
val fab = fabLayout.primaryFab!!
|
||||||
|
if (View.INVISIBLE == fab.visibility) {
|
||||||
|
0L
|
||||||
|
} else {
|
||||||
|
fabLayout.setExpanded(expanded = false, animation = true)
|
||||||
|
fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)
|
||||||
|
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
||||||
|
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
||||||
|
ANIMATE_TIME
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fabLayout.setExpanded(expanded = false, animation = false)
|
||||||
|
val fab = fabLayout.primaryFab!!
|
||||||
|
fab.visibility = View.INVISIBLE
|
||||||
|
fab.scaleX = 0.0f
|
||||||
|
fab.scaleY = 0.0f
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectSearchFab(animation: Boolean) {
|
||||||
|
val delay = hideActionFab(animation)
|
||||||
|
showSearchFab(animation, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectActionFab(animation: Boolean) {
|
||||||
|
val delay = hideSearchFab(animation)
|
||||||
|
showActionFab(animation, delay)
|
||||||
|
}
|
||||||
|
|
||||||
fun setSearchBarHint(hint: String?) {
|
fun setSearchBarHint(hint: String?) {
|
||||||
binding.toolbar.hint = hint
|
binding.toolbar.hint = hint
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ fun AboutScreen() {
|
|||||||
title = stringResource(id = R.string.use_ci_update_channel),
|
title = stringResource(id = R.string.use_ci_update_channel),
|
||||||
value = Settings::useCIUpdateChannel,
|
value = Settings::useCIUpdateChannel,
|
||||||
)
|
)
|
||||||
SimpleMenuPreferenceInt2(
|
SimpleMenuPreferenceInt(
|
||||||
title = stringResource(id = R.string.auto_updates),
|
title = stringResource(id = R.string.auto_updates),
|
||||||
entry = R.array.update_frequency,
|
entry = R.array.update_frequency,
|
||||||
entryValueRes = R.array.update_frequency_values,
|
entryValueRes = R.array.update_frequency_values,
|
||||||
|
|||||||
@@ -126,11 +126,6 @@ fun AdvancedScreen() {
|
|||||||
entryValueRes = R.array.app_language_entry_values,
|
entryValueRes = R.array.app_language_entry_values,
|
||||||
value = Settings::language,
|
value = Settings::language,
|
||||||
)
|
)
|
||||||
SwitchPreference(
|
|
||||||
title = stringResource(id = R.string.settings_advanced_enable_quic),
|
|
||||||
summary = stringResource(id = R.string.settings_advanced_enable_quic_summary),
|
|
||||||
value = Settings::enableQuic,
|
|
||||||
)
|
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = stringResource(id = R.string.preload_thumb_aggressively),
|
title = stringResource(id = R.string.preload_thumb_aggressively),
|
||||||
value = Settings::preloadThumbAggressively,
|
value = Settings::preloadThumbAggressively,
|
||||||
|
|||||||
@@ -224,6 +224,10 @@ fun EhScreen() {
|
|||||||
title = stringResource(id = R.string.black_dark_theme),
|
title = stringResource(id = R.string.black_dark_theme),
|
||||||
value = Settings::blackDarkTheme,
|
value = Settings::blackDarkTheme,
|
||||||
)
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
title = stringResource(id = R.string.harmonize_category_color),
|
||||||
|
value = Settings::harmonizeCategoryColor,
|
||||||
|
)
|
||||||
SimpleMenuPreferenceInt(
|
SimpleMenuPreferenceInt(
|
||||||
title = stringResource(id = R.string.settings_eh_launch_page),
|
title = stringResource(id = R.string.settings_eh_launch_page),
|
||||||
entry = R.array.launch_page_entries,
|
entry = R.array.launch_page_entries,
|
||||||
|
|||||||
@@ -114,20 +114,6 @@ fun HtmlPreference(title: String, summary: AnnotatedString? = null, onClick: ()
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SimpleMenuPreferenceInt(title: String, summary: String? = null, @ArrayRes entry: Int, @ArrayRes entryValueRes: Int, value: MutableState<Int>) {
|
fun SimpleMenuPreferenceInt(title: String, summary: String? = null, @ArrayRes entry: Int, @ArrayRes entryValueRes: Int, value: MutableState<Int>) {
|
||||||
val entryArray = stringArrayResource(id = entry)
|
|
||||||
val valuesArray = stringArrayResource(id = entryValueRes)
|
|
||||||
val map = remember {
|
|
||||||
val iter = entryArray.iterator()
|
|
||||||
valuesArray.associateWith { iter.next() }
|
|
||||||
}
|
|
||||||
var v by value
|
|
||||||
fun set(new: String) { v = new.toInt() }
|
|
||||||
check(entryArray.size == valuesArray.size)
|
|
||||||
DropDownPref(title = title, summary = summary, defaultValue = v.toString(), onValueChange = ::set, useSelectedAsSummary = summary.isNullOrBlank(), entries = map)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SimpleMenuPreferenceInt2(title: String, summary: String? = null, @ArrayRes entry: Int, @ArrayRes entryValueRes: Int, value: MutableState<Int>) {
|
|
||||||
val entryArray = stringArrayResource(id = entry)
|
val entryArray = stringArrayResource(id = entry)
|
||||||
val valuesArray = integerArrayResource(id = entryValueRes)
|
val valuesArray = integerArrayResource(id = entryValueRes)
|
||||||
val map = remember {
|
val map = remember {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.hippo.ehviewer.ui.tools
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -15,7 +14,6 @@ private const val QuarterCircle = PI / 2
|
|||||||
@Composable
|
@Composable
|
||||||
fun CircularLayout(
|
fun CircularLayout(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
radius: Dp,
|
|
||||||
placeFirstItemInCenter: Boolean = false,
|
placeFirstItemInCenter: Boolean = false,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -23,7 +21,7 @@ fun CircularLayout(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
content = content,
|
content = content,
|
||||||
) { measurables, constraints ->
|
) { measurables, constraints ->
|
||||||
val radiusPx = radius.toPx()
|
val radiusPx = constraints.maxWidth * 0.4
|
||||||
val itemConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
val itemConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
|
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
|
||||||
val outPlaceables = if (placeFirstItemInCenter) placeables.drop(1) else placeables
|
val outPlaceables = if (placeFirstItemInCenter) placeables.drop(1) else placeables
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||||
import kotlinx.coroutines.CancellableContinuation
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -183,6 +183,7 @@ class DialogState {
|
|||||||
}
|
}
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { cont.cancel() },
|
onDismissRequest = { cont.cancel() },
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth),
|
||||||
content = {
|
content = {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
|
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
|
||||||
@@ -248,12 +249,10 @@ class DialogState {
|
|||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
@StringRes hint: Int,
|
@StringRes hint: Int,
|
||||||
maxChar: Int,
|
maxChar: Int,
|
||||||
adjustTextPosition: Boolean = true,
|
|
||||||
): Pair<Int, String> = showNoButton(false) {
|
): Pair<Int, String> = showNoButton(false) {
|
||||||
Column {
|
Column {
|
||||||
Text(text = stringResource(id = title), modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium)
|
Text(text = stringResource(id = title), modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium)
|
||||||
CircularLayout(
|
CircularLayout(
|
||||||
radius = 112.dp,
|
|
||||||
modifier = Modifier.fillMaxWidth().aspectRatio(1F),
|
modifier = Modifier.fillMaxWidth().aspectRatio(1F),
|
||||||
placeFirstItemInCenter = true,
|
placeFirstItemInCenter = true,
|
||||||
) {
|
) {
|
||||||
@@ -261,8 +260,7 @@ class DialogState {
|
|||||||
TextField(
|
TextField(
|
||||||
value = note,
|
value = note,
|
||||||
onValueChange = { note = it },
|
onValueChange = { note = it },
|
||||||
modifier = Modifier.width(128.dp),
|
modifier = Modifier.fillMaxWidth(0.45F).aspectRatio(1F),
|
||||||
textStyle = TextStyle(fontFamily = FontFamily.Monospace),
|
|
||||||
label = { Text(text = stringResource(id = hint)) },
|
label = { Text(text = stringResource(id = hint)) },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (note.isNotEmpty()) {
|
if (note.isNotEmpty()) {
|
||||||
@@ -278,7 +276,6 @@ class DialogState {
|
|||||||
textAlign = TextAlign.End,
|
textAlign = TextAlign.End,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
maxLines = 6,
|
|
||||||
shape = ShapeDefaults.ExtraSmall,
|
shape = ShapeDefaults.ExtraSmall,
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
focusedIndicatorColor = Color.Transparent,
|
focusedIndicatorColor = Color.Transparent,
|
||||||
@@ -287,16 +284,18 @@ class DialogState {
|
|||||||
)
|
)
|
||||||
items.forEachIndexed { index, (icon, text) ->
|
items.forEachIndexed { index, (icon, text) ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note) },
|
modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note) }
|
||||||
|
.fillMaxWidth(0.2F),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
if (index == 0 && adjustTextPosition) {
|
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
|
||||||
Text(text = text, style = MaterialTheme.typography.bodySmall)
|
Text(
|
||||||
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
|
text = text,
|
||||||
} else {
|
textAlign = TextAlign.Center,
|
||||||
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
|
overflow = TextOverflow.Ellipsis,
|
||||||
Text(text = text, style = MaterialTheme.typography.bodySmall)
|
maxLines = 2,
|
||||||
}
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ object AppConfig {
|
|||||||
return FileUtils.createTempFile(tempDir, null)
|
return FileUtils.createTempFile(tempDir, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveParseErrorBody(e: ParseException) {
|
fun saveParseErrorBody(e: ParseException, body: String) {
|
||||||
val dir = externalParseErrorDir ?: return
|
val dir = externalParseErrorDir ?: return
|
||||||
File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt").writer().use {
|
File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt").writer().use {
|
||||||
it.write(e.message)
|
it.write(e.message)
|
||||||
it.appendLine()
|
it.appendLine()
|
||||||
it.append(e.body)
|
it.append(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.hippo.ehviewer.util
|
package com.hippo.ehviewer.util
|
||||||
|
|
||||||
import com.hippo.ehviewer.Settings
|
import android.os.Build
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
|
|
||||||
|
@get:ChecksSdkIntAtLeast(Build.VERSION_CODES.S, extension = 7)
|
||||||
val isCronetSupported: Boolean
|
val isCronetSupported: Boolean
|
||||||
get() = Settings.enableQuic && isAtLeastQ
|
get() = isAtLeastSExtension7
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.hippo.ehviewer.util
|
package com.hippo.ehviewer.util
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.ext.SdkExtensions
|
||||||
|
|
||||||
val isAtLeastQ = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
val isAtLeastQ = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||||
|
val isAtLeastR = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
val isAtLeastSExtension7 = isAtLeastR && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 7
|
||||||
@@ -21,3 +21,5 @@ fun String.toLongOrDefault(defaultValue: Long): Long {
|
|||||||
fun String.toFloatOrDefault(defaultValue: Float): Float {
|
fun String.toFloatOrDefault(defaultValue: Float): Float {
|
||||||
return toFloatOrNull() ?: defaultValue
|
return toFloatOrNull() ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String?.containsIgnoreCase(other: String) = this?.contains(other, true) ?: false
|
||||||
+3
-1
@@ -57,7 +57,9 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTouchEvent(e: MotionEvent): Boolean {
|
override fun onTouchEvent(e: MotionEvent): Boolean {
|
||||||
detector.onTouchEvent(e)
|
if (scrollState == SCROLL_STATE_IDLE) {
|
||||||
|
detector.onTouchEvent(e)
|
||||||
|
}
|
||||||
return super.onTouchEvent(e)
|
return super.onTouchEvent(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,29 +17,26 @@
|
|||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_filter"
|
|
||||||
android:icon="@drawable/v_filter_dark_x24"
|
|
||||||
android:title="@string/download_filter"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_open_side_sheet"
|
android:id="@+id/action_open_side_sheet"
|
||||||
android:icon="@drawable/ic_outline_label_24"
|
android:icon="@drawable/ic_outline_label_24"
|
||||||
android:title="@string/download_labels"
|
android:title="@string/download_labels"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_filter"
|
||||||
|
android:title="@string/download_filter"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_start_all"
|
android:id="@+id/action_start_all"
|
||||||
android:icon="@drawable/v_play_x24"
|
|
||||||
android:title="@string/download_start_all"
|
android:title="@string/download_start_all"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_stop_all"
|
android:id="@+id/action_stop_all"
|
||||||
android:icon="@drawable/v_pause_x24"
|
|
||||||
android:title="@string/download_stop_all"
|
android:title="@string/download_stop_all"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_reset_reading_progress"
|
android:id="@+id/action_reset_reading_progress"
|
||||||
|
|||||||
@@ -294,7 +294,7 @@
|
|||||||
<string name="settings_advanced_dump_logcat_to">Protokolldatei wurde auf %s gespeichert.</string>
|
<string name="settings_advanced_dump_logcat_to">Protokolldatei wurde auf %s gespeichert.</string>
|
||||||
<string name="cloud_favorites">Cloud-Favoriten</string>
|
<string name="cloud_favorites">Cloud-Favoriten</string>
|
||||||
<string name="local_favorites">Lokale Favoriten</string>
|
<string name="local_favorites">Lokale Favoriten</string>
|
||||||
<string name="favorites_search_bar_hint">%s durchsuchen</string>
|
<string name="search_bar_hint">%s durchsuchen</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">Aus Favoriten löschen</string>
|
<string name="delete_favorites_dialog_title">Aus Favoriten löschen</string>
|
||||||
|
|||||||
@@ -354,7 +354,7 @@
|
|||||||
<!-- Favorites -->
|
<!-- Favorites -->
|
||||||
<string name="cloud_favorites">Favoritos en la nube</string>
|
<string name="cloud_favorites">Favoritos en la nube</string>
|
||||||
<string name="local_favorites">Favoritos locales</string>
|
<string name="local_favorites">Favoritos locales</string>
|
||||||
<string name="favorites_search_bar_hint">Buscar %s</string>
|
<string name="search_bar_hint">Buscar %s</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">Eliminar de favoritos</string>
|
<string name="delete_favorites_dialog_title">Eliminar de favoritos</string>
|
||||||
|
|||||||
@@ -393,7 +393,7 @@
|
|||||||
<!-- Favorites -->
|
<!-- Favorites -->
|
||||||
<string name="cloud_favorites">Favoris de cloud</string>
|
<string name="cloud_favorites">Favoris de cloud</string>
|
||||||
<string name="local_favorites">Favoris locales</string>
|
<string name="local_favorites">Favoris locales</string>
|
||||||
<string name="favorites_search_bar_hint">Chercher dans %s</string>
|
<string name="search_bar_hint">Chercher dans %s</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">Effacer de favoris</string>
|
<string name="delete_favorites_dialog_title">Effacer de favoris</string>
|
||||||
|
|||||||
@@ -390,7 +390,7 @@
|
|||||||
<!-- Favorites -->
|
<!-- Favorites -->
|
||||||
<string name="cloud_favorites">Cloudのお気に入り</string>
|
<string name="cloud_favorites">Cloudのお気に入り</string>
|
||||||
<string name="local_favorites">ローカルのお気に入り</string>
|
<string name="local_favorites">ローカルのお気に入り</string>
|
||||||
<string name="favorites_search_bar_hint">%sを検索</string>
|
<string name="search_bar_hint">%sを検索</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">お気に入りから削除</string>
|
<string name="delete_favorites_dialog_title">お気に入りから削除</string>
|
||||||
|
|||||||
@@ -364,7 +364,7 @@
|
|||||||
<!-- Favorites -->
|
<!-- Favorites -->
|
||||||
<string name="cloud_favorites">클라우드 즐겨찾기</string>
|
<string name="cloud_favorites">클라우드 즐겨찾기</string>
|
||||||
<string name="local_favorites">로컬 즐겨찾기</string>
|
<string name="local_favorites">로컬 즐겨찾기</string>
|
||||||
<string name="favorites_search_bar_hint">%s 검색</string>
|
<string name="search_bar_hint">%s 검색</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">즐겨찾기에서 삭제</string>
|
<string name="delete_favorites_dialog_title">즐겨찾기에서 삭제</string>
|
||||||
|
|||||||
@@ -379,7 +379,7 @@
|
|||||||
<!-- Favorites -->
|
<!-- Favorites -->
|
||||||
<string name="cloud_favorites">รายการโปรดบนเว็บ</string>
|
<string name="cloud_favorites">รายการโปรดบนเว็บ</string>
|
||||||
<string name="local_favorites">รายการโปรดบนเครื่อง</string>
|
<string name="local_favorites">รายการโปรดบนเครื่อง</string>
|
||||||
<string name="favorites_search_bar_hint">ค้นหา %s</string>
|
<string name="search_bar_hint">ค้นหา %s</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">ลบออกจากรายการโปรด</string>
|
<string name="delete_favorites_dialog_title">ลบออกจากรายการโปรด</string>
|
||||||
|
|||||||
@@ -149,6 +149,9 @@
|
|||||||
<string name="refresh">刷新</string>
|
<string name="refresh">刷新</string>
|
||||||
<string name="refresh_original">刷新(原图)</string>
|
<string name="refresh_original">刷新(原图)</string>
|
||||||
<string name="open_in_other_app">在其他应用中打开</string>
|
<string name="open_in_other_app">在其他应用中打开</string>
|
||||||
|
<string name="clear_image_cache">清除图片缓存</string>
|
||||||
|
<string name="clear_image_cache_confirm">清除该画廊的所有图片缓存?</string>
|
||||||
|
<string name="image_cache_cleared">已清除图片缓存</string>
|
||||||
<string name="rate_successfully">评分成功</string>
|
<string name="rate_successfully">评分成功</string>
|
||||||
<string name="rate_failed">评分失败</string>
|
<string name="rate_failed">评分失败</string>
|
||||||
<string name="no_torrents">没有种子</string>
|
<string name="no_torrents">没有种子</string>
|
||||||
@@ -376,8 +379,6 @@
|
|||||||
<string name="settings_advanced_dump_logcat_to">已保存日志至 %s</string>
|
<string name="settings_advanced_dump_logcat_to">已保存日志至 %s</string>
|
||||||
<string name="settings_advanced_read_cache_size">阅读缓存大小</string>
|
<string name="settings_advanced_read_cache_size">阅读缓存大小</string>
|
||||||
<string name="settings_advanced_app_language_title">App 界面语言</string>
|
<string name="settings_advanced_app_language_title">App 界面语言</string>
|
||||||
<string name="settings_advanced_enable_quic">[实验性] QUIC 支持</string>
|
|
||||||
<string name="settings_advanced_enable_quic_summary">启用 QUIC 协议支持(需要重启应用)</string>
|
|
||||||
<string name="settings_advanced_export_data">导出数据</string>
|
<string name="settings_advanced_export_data">导出数据</string>
|
||||||
<string name="settings_advanced_export_data_summary">保存数据至外置存储器,例如下载列表,快速搜索列表</string>
|
<string name="settings_advanced_export_data_summary">保存数据至外置存储器,例如下载列表,快速搜索列表</string>
|
||||||
<string name="settings_advanced_export_data_to">已导出数据至 %s</string>
|
<string name="settings_advanced_export_data_to">已导出数据至 %s</string>
|
||||||
@@ -409,7 +410,7 @@
|
|||||||
<!-- Favorites -->
|
<!-- Favorites -->
|
||||||
<string name="cloud_favorites">云端收藏</string>
|
<string name="cloud_favorites">云端收藏</string>
|
||||||
<string name="local_favorites">本地收藏</string>
|
<string name="local_favorites">本地收藏</string>
|
||||||
<string name="favorites_search_bar_hint">搜索 %s</string>
|
<string name="search_bar_hint">搜索 %s</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">删除收藏</string>
|
<string name="delete_favorites_dialog_title">删除收藏</string>
|
||||||
@@ -418,7 +419,7 @@
|
|||||||
<string name="default_favorites_collection">默认收藏夹</string>
|
<string name="default_favorites_collection">默认收藏夹</string>
|
||||||
<string name="default_favorites_warning">启用此项将无法添加收藏备注</string>
|
<string name="default_favorites_warning">启用此项将无法添加收藏备注</string>
|
||||||
<string name="let_me_select">让我选择</string>
|
<string name="let_me_select">让我选择</string>
|
||||||
<string name="favorite_note">备注</string>
|
<string name="favorite_note">收藏备注</string>
|
||||||
<string name="collections">收藏夹</string>
|
<string name="collections">收藏夹</string>
|
||||||
<string name="need_sign_in">需要登录</string>
|
<string name="need_sign_in">需要登录</string>
|
||||||
<!-- Progress Scene -->
|
<!-- Progress Scene -->
|
||||||
@@ -475,6 +476,7 @@
|
|||||||
<string name="download_service">下载服务</string>
|
<string name="download_service">下载服务</string>
|
||||||
<string name="favorite_name">收藏画廊</string>
|
<string name="favorite_name">收藏画廊</string>
|
||||||
<string name="black_dark_theme">纯黑深色主题</string>
|
<string name="black_dark_theme">纯黑深色主题</string>
|
||||||
|
<string name="harmonize_category_color">类别颜色适配动态配色</string>
|
||||||
<string name="use_ci_update_channel">使用持续集成(CI)构建频道更新</string>
|
<string name="use_ci_update_channel">使用持续集成(CI)构建频道更新</string>
|
||||||
<string name="download_filter">过滤</string>
|
<string name="download_filter">过滤</string>
|
||||||
<string name="download_all">全部</string>
|
<string name="download_all">全部</string>
|
||||||
|
|||||||
@@ -419,7 +419,7 @@
|
|||||||
<!-- Favorites -->
|
<!-- Favorites -->
|
||||||
<string name="cloud_favorites">雲端收藏</string>
|
<string name="cloud_favorites">雲端收藏</string>
|
||||||
<string name="local_favorites">本地收藏</string>
|
<string name="local_favorites">本地收藏</string>
|
||||||
<string name="favorites_search_bar_hint">搜索 %s</string>
|
<string name="search_bar_hint">搜索 %s</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">刪除收藏</string>
|
<string name="delete_favorites_dialog_title">刪除收藏</string>
|
||||||
|
|||||||
@@ -400,7 +400,7 @@
|
|||||||
<!-- Favorites -->
|
<!-- Favorites -->
|
||||||
<string name="cloud_favorites">雲端收藏</string>
|
<string name="cloud_favorites">雲端收藏</string>
|
||||||
<string name="local_favorites">本機收藏</string>
|
<string name="local_favorites">本機收藏</string>
|
||||||
<string name="favorites_search_bar_hint">搜尋 %s</string>
|
<string name="search_bar_hint">搜尋 %s</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">移除收藏</string>
|
<string name="delete_favorites_dialog_title">移除收藏</string>
|
||||||
|
|||||||
@@ -21,20 +21,20 @@
|
|||||||
<item>@string/settings_eh_list_mode_thumb</item>
|
<item>@string/settings_eh_list_mode_thumb</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="list_mode_entry_values" translatable="false">
|
<integer-array name="list_mode_entry_values" translatable="false">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="gallery_site_entries" translatable="false">
|
<string-array name="gallery_site_entries" translatable="false">
|
||||||
<item>@string/site_e</item>
|
<item>@string/site_e</item>
|
||||||
<item>@string/site_ex</item>
|
<item>@string/site_ex</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="gallery_site_entry_values" translatable="false">
|
<integer-array name="gallery_site_entry_values" translatable="false">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="launch_page_entries" translatable="false">
|
<string-array name="launch_page_entries" translatable="false">
|
||||||
<item>@string/homepage</item>
|
<item>@string/homepage</item>
|
||||||
@@ -43,22 +43,22 @@
|
|||||||
<item>@string/toplist</item>
|
<item>@string/toplist</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="launch_page_entry_values" translatable="false">
|
<integer-array name="launch_page_entry_values" translatable="false">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
<item>2</item>
|
<item>2</item>
|
||||||
<item>3</item>
|
<item>3</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="detail_size_entries" translatable="false">
|
<string-array name="detail_size_entries" translatable="false">
|
||||||
<item>@string/settings_eh_detail_size_long</item>
|
<item>@string/settings_eh_detail_size_long</item>
|
||||||
<item>@string/settings_eh_detail_size_short</item>
|
<item>@string/settings_eh_detail_size_short</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="detail_size_entry_values" translatable="false">
|
<integer-array name="detail_size_entry_values" translatable="false">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="thumb_resolution_entries" translatable="false">
|
<string-array name="thumb_resolution_entries" translatable="false">
|
||||||
<item>@string/settings_eh_thumb_resolution_auto</item>
|
<item>@string/settings_eh_thumb_resolution_auto</item>
|
||||||
@@ -66,11 +66,11 @@
|
|||||||
<item>300x</item>
|
<item>300x</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="thumb_resolution_entry_values" translatable="false">
|
<integer-array name="thumb_resolution_entry_values" translatable="false">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
<item>2</item>
|
<item>2</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="search_min_rating" translatable="false">
|
<string-array name="search_min_rating" translatable="false">
|
||||||
<item>@string/unlimited</item>
|
<item>@string/unlimited</item>
|
||||||
@@ -87,12 +87,12 @@
|
|||||||
<item>7</item>
|
<item>7</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="multi_thread_download_entry_values" translatable="false">
|
<integer-array name="multi_thread_download_entry_values" translatable="false">
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
<item>3</item>
|
<item>3</item>
|
||||||
<item>5</item>
|
<item>5</item>
|
||||||
<item>7</item>
|
<item>7</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="download_delay_entries" translatable="false">
|
<string-array name="download_delay_entries" translatable="false">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
<item>20000</item>
|
<item>20000</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="download_delay_entry_values" translatable="false">
|
<integer-array name="download_delay_entry_values" translatable="false">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>500</item>
|
<item>500</item>
|
||||||
<item>1000</item>
|
<item>1000</item>
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
<item>5000</item>
|
<item>5000</item>
|
||||||
<item>10000</item>
|
<item>10000</item>
|
||||||
<item>20000</item>
|
<item>20000</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="preload_image_entries" translatable="false">
|
<string-array name="preload_image_entries" translatable="false">
|
||||||
<item>3</item>
|
<item>3</item>
|
||||||
@@ -129,14 +129,14 @@
|
|||||||
<item>17</item>
|
<item>17</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="preload_image_entry_values" translatable="false">
|
<integer-array name="preload_image_entry_values" translatable="false">
|
||||||
<item>3</item>
|
<item>3</item>
|
||||||
<item>5</item>
|
<item>5</item>
|
||||||
<item>7</item>
|
<item>7</item>
|
||||||
<item>11</item>
|
<item>11</item>
|
||||||
<item>13</item>
|
<item>13</item>
|
||||||
<item>17</item>
|
<item>17</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="read_cache_size_entries" translatable="false">
|
<string-array name="read_cache_size_entries" translatable="false">
|
||||||
<item>320 MB</item>
|
<item>320 MB</item>
|
||||||
@@ -146,13 +146,13 @@
|
|||||||
<item>5120 MB</item>
|
<item>5120 MB</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="read_cache_size_entry_values" translatable="false">
|
<integer-array name="read_cache_size_entry_values" translatable="false">
|
||||||
<item>320</item>
|
<item>320</item>
|
||||||
<item>640</item>
|
<item>640</item>
|
||||||
<item>1280</item>
|
<item>1280</item>
|
||||||
<item>2560</item>
|
<item>2560</item>
|
||||||
<item>5120</item>
|
<item>5120</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="filter_entries">
|
<string-array name="filter_entries">
|
||||||
<item>@string/filter_title</item>
|
<item>@string/filter_title</item>
|
||||||
@@ -208,11 +208,11 @@
|
|||||||
<item>@string/dark_theme_on</item>
|
<item>@string/dark_theme_on</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="night_mode_values">
|
<integer-array name="night_mode_values">
|
||||||
<item>-1</item>
|
<item>-1</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
<item>2</item>
|
<item>2</item>
|
||||||
</string-array>
|
</integer-array>
|
||||||
|
|
||||||
<string-array name="download_state">
|
<string-array name="download_state">
|
||||||
<item>@string/download_all</item>
|
<item>@string/download_all</item>
|
||||||
|
|||||||
@@ -191,6 +191,9 @@
|
|||||||
<string name="refresh">Refresh</string>
|
<string name="refresh">Refresh</string>
|
||||||
<string name="refresh_original">Refresh (Original image)</string>
|
<string name="refresh_original">Refresh (Original image)</string>
|
||||||
<string name="open_in_other_app">Open with other app</string>
|
<string name="open_in_other_app">Open with other app</string>
|
||||||
|
<string name="clear_image_cache">Clear image cache</string>
|
||||||
|
<string name="clear_image_cache_confirm">Clear all cached images for this gallery?</string>
|
||||||
|
<string name="image_cache_cleared">Image cache cleared</string>
|
||||||
<string name="rate_successfully">Rate successfully</string>
|
<string name="rate_successfully">Rate successfully</string>
|
||||||
<string name="rate_failed">Rate failed</string>
|
<string name="rate_failed">Rate failed</string>
|
||||||
<string name="no_torrents">No torrents</string>
|
<string name="no_torrents">No torrents</string>
|
||||||
@@ -420,8 +423,6 @@
|
|||||||
<string name="settings_advanced_dump_logcat_to">Logcat dumped to %s</string>
|
<string name="settings_advanced_dump_logcat_to">Logcat dumped to %s</string>
|
||||||
<string name="settings_advanced_read_cache_size">Read cache size</string>
|
<string name="settings_advanced_read_cache_size">Read cache size</string>
|
||||||
<string name="settings_advanced_app_language_title">App language</string>
|
<string name="settings_advanced_app_language_title">App language</string>
|
||||||
<string name="settings_advanced_enable_quic">[Experimental] QUIC support</string>
|
|
||||||
<string name="settings_advanced_enable_quic_summary">Enable QUIC protocol support (App restart required)</string>
|
|
||||||
<string name="settings_advanced_export_data">Export data</string>
|
<string name="settings_advanced_export_data">Export data</string>
|
||||||
<string name="settings_advanced_export_data_summary">Save data like download list, quick search list, to external storage</string>
|
<string name="settings_advanced_export_data_summary">Save data like download list, quick search list, to external storage</string>
|
||||||
<string name="settings_advanced_export_data_to">Exported data to %s</string>
|
<string name="settings_advanced_export_data_to">Exported data to %s</string>
|
||||||
@@ -467,7 +468,7 @@
|
|||||||
<!-- Favorites -->
|
<!-- Favorites -->
|
||||||
<string name="cloud_favorites">Cloud Favorites</string>
|
<string name="cloud_favorites">Cloud Favorites</string>
|
||||||
<string name="local_favorites">Local Favorites</string>
|
<string name="local_favorites">Local Favorites</string>
|
||||||
<string name="favorites_search_bar_hint">Search %s</string>
|
<string name="search_bar_hint">Search %s</string>
|
||||||
<string name="favorites_title">%s</string>
|
<string name="favorites_title">%s</string>
|
||||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||||
<string name="delete_favorites_dialog_title">Delete from favorites</string>
|
<string name="delete_favorites_dialog_title">Delete from favorites</string>
|
||||||
@@ -476,7 +477,7 @@
|
|||||||
<string name="default_favorites_collection">Default favorites collection</string>
|
<string name="default_favorites_collection">Default favorites collection</string>
|
||||||
<string name="default_favorites_warning">You won\'t be able to add favorite notes if you enable this</string>
|
<string name="default_favorites_warning">You won\'t be able to add favorite notes if you enable this</string>
|
||||||
<string name="let_me_select">Let me select</string>
|
<string name="let_me_select">Let me select</string>
|
||||||
<string name="favorite_note">Note</string>
|
<string name="favorite_note">Favorite Note</string>
|
||||||
<string name="collections">Collections</string>
|
<string name="collections">Collections</string>
|
||||||
<string name="need_sign_in">Signing in is required</string>
|
<string name="need_sign_in">Signing in is required</string>
|
||||||
<!-- Progress Scene -->
|
<!-- Progress Scene -->
|
||||||
@@ -540,6 +541,7 @@
|
|||||||
<string name="download_service">Download Service</string>
|
<string name="download_service">Download Service</string>
|
||||||
<string name="favorite_name">Favorite</string>
|
<string name="favorite_name">Favorite</string>
|
||||||
<string name="black_dark_theme">Black dark theme</string>
|
<string name="black_dark_theme">Black dark theme</string>
|
||||||
|
<string name="harmonize_category_color">Harmonize category color for Dynamic Color</string>
|
||||||
<string name="download_filter">Filter</string>
|
<string name="download_filter">Filter</string>
|
||||||
<string name="download_all">All</string>
|
<string name="download_all">All</string>
|
||||||
<string name="download_start_all_reversed">Start all (reversed)</string>
|
<string name="download_start_all_reversed">Start all (reversed)</string>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jnix = { path = "jnix" }
|
|||||||
jnix-macros = "0.4"
|
jnix-macros = "0.4"
|
||||||
catch_panic = { path = "catch_panic" }
|
catch_panic = { path = "catch_panic" }
|
||||||
catch_panic_macros = { path = "catch_panic_macros" }
|
catch_panic_macros = { path = "catch_panic_macros" }
|
||||||
jni_fn = { path = "jni_fn" }
|
jni_fn = "0.1.2"
|
||||||
quick-xml = { default-features = false, version = "0.30" }
|
quick-xml = { default-features = false, version = "0.30" }
|
||||||
tl = "0.7"
|
tl = "0.7"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ proc-macro = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
syn = { version = "1.0", features = ["full"] }
|
syn = { version = "2.0", features = ["full"] }
|
||||||
@@ -3,10 +3,7 @@ use std::ops::Deref;
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::{quote, quote_spanned, ToTokens};
|
use quote::{quote, quote_spanned, ToTokens};
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
use syn::{
|
use syn::{parse_macro_input, Expr, FnArg, ItemFn, LitStr, Pat, Path};
|
||||||
parse_macro_input, AttributeArgs, Expr, FnArg, ItemFn, Lit, Meta, MetaNameValue, NestedMeta,
|
|
||||||
Pat, Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ONLY_FUNCTIONS_MSG: &str = "#[catch_panic] can only be applied to functions";
|
const ONLY_FUNCTIONS_MSG: &str = "#[catch_panic] can only be applied to functions";
|
||||||
const FIRST_ARG_MSG: &str =
|
const FIRST_ARG_MSG: &str =
|
||||||
@@ -33,30 +30,21 @@ pub fn catch_panic(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
let item = proc_macro2::TokenStream::from(item);
|
let item = proc_macro2::TokenStream::from(item);
|
||||||
|
|
||||||
// parse macro arguments
|
// parse macro arguments
|
||||||
let args = parse_macro_input!(attr as AttributeArgs);
|
|
||||||
let mut default_value = None;
|
let mut default_value = None;
|
||||||
let mut handler = None;
|
let mut handler = None;
|
||||||
|
let parser = syn::meta::parser(|meta| {
|
||||||
for arg in args {
|
if meta.path.is_ident("default") {
|
||||||
if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
default_value = Some(meta.value()?.parse::<LitStr>()?.parse::<Expr>()?);
|
||||||
path,
|
Ok(())
|
||||||
lit: Lit::Str(str),
|
} else if meta.path.is_ident("handler") {
|
||||||
..
|
handler = Some(meta.value()?.parse::<LitStr>()?.parse::<Path>()?);
|
||||||
})) = arg
|
Ok(())
|
||||||
{
|
} else {
|
||||||
match path.get_ident() {
|
Err(meta.error("unsupported argument"))
|
||||||
Some(id) if id == "default" => match str.parse::<Expr>() {
|
|
||||||
Ok(expr) => default_value = Some(expr),
|
|
||||||
Err(err) => abort!(err.to_compile_error(), item),
|
|
||||||
},
|
|
||||||
Some(id) if id == "handler" => match str.parse::<Path>() {
|
|
||||||
Ok(expr) => handler = Some(expr),
|
|
||||||
Err(err) => abort!(err.to_compile_error(), item),
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
parse_macro_input!(attr with parser);
|
||||||
|
|
||||||
let default_value = match default_value {
|
let default_value = match default_value {
|
||||||
Some(val) => val.to_token_stream(),
|
Some(val) => val.to_token_stream(),
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
/target
|
|
||||||
Cargo.lock
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "jni_fn"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["Anton Lazarev <https://antonok.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
description = "JNI-compatible method signature generator"
|
|
||||||
repository = "https://gitlab.com/antonok/jni_fn"
|
|
||||||
readme = "README.md"
|
|
||||||
keywords = ["jni", "java", "jvm", "android", "ndk"]
|
|
||||||
categories = ["api-bindings", "development-tools::procedural-macro-helpers"]
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
syn = { version = "1.0", features = ["full"] }
|
|
||||||
quote = "1.0"
|
|
||||||
proc-macro2 = "1.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
jni = "0.20"
|
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
//! JNI-compatible method signature generator for Rust libraries.
|
|
||||||
//!
|
|
||||||
//! This crate was designed for use with the [`jni`](https://crates.io/crates/jni) crate, which
|
|
||||||
//! exposes JNI-compatible type bindings. Although it's possible to use `jni` without `jni_fn`, the
|
|
||||||
//! procedural macro defined here will make it easier to write the method signatures correctly.
|
|
||||||
//!
|
|
||||||
//! See the `jni_fn` attribute macro documentation below for more info and usage examples.
|
|
||||||
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
#![deny(unsafe_code)]
|
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use quote::ToTokens;
|
|
||||||
use syn::spanned::Spanned;
|
|
||||||
|
|
||||||
/// Annotate a function with this procedural macro attribute to expose it over the JNI.
|
|
||||||
///
|
|
||||||
/// This attribute takes a single string literal as an argument, specifying the package namespace
|
|
||||||
/// this function should be placed under.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use jni::{ JNIEnv, objects::{ JClass, JString }, sys::jstring };
|
|
||||||
/// use jni_fn::jni_fn;
|
|
||||||
///
|
|
||||||
/// #[jni_fn("com.example.RustBindings")]
|
|
||||||
/// pub fn sayHello(env: JNIEnv, _: JClass, name: JString) -> jstring {
|
|
||||||
/// let name_javastr = env.get_string(name).unwrap();
|
|
||||||
/// let name = name_javastr.to_str().unwrap();
|
|
||||||
///
|
|
||||||
/// env.new_string(format!("Hello, {}!", name))
|
|
||||||
/// .expect("Couldn't create java string!")
|
|
||||||
/// .into_inner()
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// The `sayHello` function will automatically be expanded to have the correct ABI specification
|
|
||||||
/// and the appropriate JNI-compatible name, i.e. in this case -
|
|
||||||
/// `Java_com_example_RustBindings_sayHello`.
|
|
||||||
///
|
|
||||||
/// Then it can be accessed by, for example, Kotlin code as follows:
|
|
||||||
/// ```kotlin
|
|
||||||
/// package com.example.RustBindings
|
|
||||||
///
|
|
||||||
/// class RustBindings {
|
|
||||||
/// private external fun sayHello(name: String): String
|
|
||||||
///
|
|
||||||
/// fun greetWorld() {
|
|
||||||
/// println(sayHello("world"))
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn jni_fn(
|
|
||||||
attr: proc_macro::TokenStream,
|
|
||||||
item: proc_macro::TokenStream,
|
|
||||||
) -> proc_macro::TokenStream {
|
|
||||||
jni_fn2(attr.into(), item.into()).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deals exclusively with `proc_macro2::TokenStream` instead of `proc_macro::TokenStream`,
|
|
||||||
/// allowing it and all interior functionality to be unit tested.
|
|
||||||
fn jni_fn2(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
||||||
let attr_span = attr.span();
|
|
||||||
let item_span = item.span();
|
|
||||||
|
|
||||||
let mut function: syn::ItemFn = match syn::parse2(item) {
|
|
||||||
Ok(f) => f,
|
|
||||||
Err(_e) => {
|
|
||||||
return syn::Error::new(
|
|
||||||
item_span,
|
|
||||||
"The `jni_fn` attribute can only be applied to `fn` items",
|
|
||||||
)
|
|
||||||
.to_compile_error()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let namespace = match syn::parse2::<syn::LitStr>(attr) {
|
|
||||||
Ok(n) => n,
|
|
||||||
Err(_e) => return syn::Error::new(attr_span, "The `jni_fn` attribute must have a single string literal supplied to specify the namespace").to_compile_error(),
|
|
||||||
}.value();
|
|
||||||
|
|
||||||
if !valid_namespace(&namespace) {
|
|
||||||
return syn::Error::new(
|
|
||||||
attr_span,
|
|
||||||
"Invalid package namespace supplied to `jni_fn` attribute",
|
|
||||||
)
|
|
||||||
.to_compile_error();
|
|
||||||
}
|
|
||||||
|
|
||||||
let orig_fn_name = function.sig.ident.to_string();
|
|
||||||
|
|
||||||
function.sig.ident = syn::Ident::new(
|
|
||||||
&create_jni_fn_name(&namespace, &orig_fn_name),
|
|
||||||
function.sig.ident.span(),
|
|
||||||
);
|
|
||||||
|
|
||||||
function.attrs.push(syn::Attribute {
|
|
||||||
pound_token: Default::default(),
|
|
||||||
style: syn::AttrStyle::Outer,
|
|
||||||
bracket_token: Default::default(),
|
|
||||||
path: syn::parse_str("no_mangle").unwrap(),
|
|
||||||
tokens: TokenStream::new(),
|
|
||||||
});
|
|
||||||
function.attrs.push(syn::Attribute {
|
|
||||||
pound_token: Default::default(),
|
|
||||||
style: syn::AttrStyle::Outer,
|
|
||||||
bracket_token: Default::default(),
|
|
||||||
path: syn::parse_str("allow").unwrap(),
|
|
||||||
tokens: quote::quote! { (non_snake_case) },
|
|
||||||
});
|
|
||||||
|
|
||||||
if function.sig.abi.is_some() {
|
|
||||||
return syn::Error::new(function.sig.abi.span(), "Don't specify an ABI for `jni_fn` attributed functions - the correct ABI will be added automatically").to_compile_error();
|
|
||||||
}
|
|
||||||
function.sig.abi = Some(syn::Abi {
|
|
||||||
extern_token: Default::default(),
|
|
||||||
name: Some(syn::LitStr::new("system", function.sig.ident.span())),
|
|
||||||
});
|
|
||||||
|
|
||||||
if !matches!(function.vis, syn::Visibility::Public(_)) {
|
|
||||||
return syn::Error::new(
|
|
||||||
function.vis.span(),
|
|
||||||
"`jni_fn` attributed functions must have public visibility (`pub`)",
|
|
||||||
)
|
|
||||||
.to_compile_error();
|
|
||||||
}
|
|
||||||
|
|
||||||
function.into_token_stream()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensures that `namespace` appears roughly like a valid package name.
|
|
||||||
///
|
|
||||||
/// A package name is a '.'-separated identifier list.
|
|
||||||
///
|
|
||||||
/// Identifiers are described in section 3.8 of the Java language specification, although some
|
|
||||||
/// JVM-compatible languages have slightly different restrictions on what is considered a valid
|
|
||||||
/// identifier. This function attempts to catch obviously incorrect strings.
|
|
||||||
///
|
|
||||||
/// Please submit an issue report or patch to make this more permissive if it's required for
|
|
||||||
/// valid JVM code! Otherwise, making it more restrictive is appreciated as long as it's confirmed
|
|
||||||
/// to work with multiple JVM-compatible languages.
|
|
||||||
fn valid_namespace(namespace: &str) -> bool {
|
|
||||||
/// These shouldn't occur _anywhere_ in the package name.
|
|
||||||
const FORBIDDEN_CHARS: &[char] = &[
|
|
||||||
' ', ',', ':', ';', '|', '\\', '/', '!', '@', '#', '%', '^', '&', '*', '(', ')', '{', '}',
|
|
||||||
'[', ']', '-', '`', '~', '\t', '\n', '\r',
|
|
||||||
];
|
|
||||||
|
|
||||||
for c in FORBIDDEN_CHARS {
|
|
||||||
if namespace.contains(*c) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid_ident(ident: &str) -> bool {
|
|
||||||
/// These shouldn't occur as the first character of an identifier.
|
|
||||||
const FORBIDDEN_START_CHARS: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
||||||
|
|
||||||
if ident.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for c in FORBIDDEN_START_CHARS {
|
|
||||||
if ident.starts_with(*c) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
for ident in namespace.split('.') {
|
|
||||||
if !is_valid_ident(ident) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a JNI-compatible function name from the given namespace and function name.
|
|
||||||
/// This does _not_ transform the provided function name into `snakeCase` if it's not already; but
|
|
||||||
/// `#[allow(non_snake_case)]` should be added to prevent errors.
|
|
||||||
///
|
|
||||||
/// Importantly, any underscores in the original namespace or function name need to be replaced by
|
|
||||||
/// "_1", and then dot separators need to be turned into underscores.
|
|
||||||
fn create_jni_fn_name(namespace: &str, fn_name: &str) -> String {
|
|
||||||
let namespace_underscored = namespace.replace('_', "_1").replace('.', "_");
|
|
||||||
let fn_name_underscored = fn_name.replace('_', "_1");
|
|
||||||
format!("Java_{}_{}", namespace_underscored, fn_name_underscored)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_create_jni_fn_name() {
|
|
||||||
assert_eq!(
|
|
||||||
create_jni_fn_name("com.example.Foo", "init"),
|
|
||||||
"Java_com_example_Foo_init"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
create_jni_fn_name("com.example.Bar", "closeIt"),
|
|
||||||
"Java_com_example_Bar_closeIt"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
create_jni_fn_name("com.example.Bar", "close_it"),
|
|
||||||
"Java_com_example_Bar_close_1it"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
create_jni_fn_name(
|
|
||||||
"org.signal.client.internal.Native",
|
|
||||||
"IdentityKeyPair_Deserialize"
|
|
||||||
),
|
|
||||||
"Java_org_signal_client_internal_Native_IdentityKeyPair_1Deserialize"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_valid_namespace() {
|
|
||||||
assert!(valid_namespace("com.example.Foo"));
|
|
||||||
assert!(valid_namespace("com.antonok.kb"));
|
|
||||||
assert!(valid_namespace("org.signal.client.internal.Native"));
|
|
||||||
assert!(valid_namespace("net.under_score"));
|
|
||||||
assert!(!valid_namespace("com example Foo"));
|
|
||||||
assert!(!valid_namespace(" com.example.Foo"));
|
|
||||||
assert!(!valid_namespace("com.example.Foo "));
|
|
||||||
assert!(!valid_namespace("com.example.1Foo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_code_generation() {
|
|
||||||
let attr = quote::quote! {
|
|
||||||
"com.example.Bar"
|
|
||||||
};
|
|
||||||
let source = quote::quote! {
|
|
||||||
pub fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let expanded = jni_fn2(attr.into(), source.into());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", expanded),
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
quote::quote! {
|
|
||||||
#[no_mangle]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub extern "system" fn Java_com_example_Bar_close_1it (env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unsafe_fn() {
|
|
||||||
let attr = quote::quote! {
|
|
||||||
"com.example.Bar"
|
|
||||||
};
|
|
||||||
let source = quote::quote! {
|
|
||||||
pub unsafe fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let expanded = jni_fn2(attr.into(), source.into());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", expanded),
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
quote::quote! {
|
|
||||||
#[no_mangle]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub unsafe extern "system" fn Java_com_example_Bar_close_1it (env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_non_function() {
|
|
||||||
let attr = quote::quote! { "com.example.Foo" };
|
|
||||||
let source = quote::quote! {
|
|
||||||
enum NotAFunction {
|
|
||||||
Variant1,
|
|
||||||
Variant2(u8),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let expanded = jni_fn2(attr.into(), source.into());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", expanded),
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
quote::quote! {
|
|
||||||
compile_error! { "The `jni_fn` attribute can only be applied to `fn` items" }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_empty_attribute() {
|
|
||||||
let attr = quote::quote! {};
|
|
||||||
let source = quote::quote! {
|
|
||||||
pub fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let expanded = jni_fn2(attr.into(), source.into());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", expanded),
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
quote::quote! {
|
|
||||||
compile_error! { "The `jni_fn` attribute must have a single string literal supplied to specify the namespace" }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_namespace() {
|
|
||||||
let attr = quote::quote! { "." };
|
|
||||||
let source = quote::quote! {
|
|
||||||
pub fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let expanded = jni_fn2(attr.into(), source.into());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", expanded),
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
quote::quote! {
|
|
||||||
compile_error! { "Invalid package namespace supplied to `jni_fn` attribute" }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_specified_abi() {
|
|
||||||
let attr = quote::quote! { "com.example.Foo" };
|
|
||||||
let source = quote::quote! {
|
|
||||||
pub extern "C" fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let expanded = jni_fn2(attr.into(), source.into());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", expanded),
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
quote::quote! {
|
|
||||||
compile_error! { "Don't specify an ABI for `jni_fn` attributed functions - the correct ABI will be added automatically" }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_wrong_visibility() {
|
|
||||||
let attr = quote::quote! { "com.example.Foo" };
|
|
||||||
let source = quote::quote! {
|
|
||||||
fn close_it(env: JNIEnv, _: JClass, filename: JString) -> jboolean {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let expanded = jni_fn2(attr.into(), source.into());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", expanded),
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
quote::quote! {
|
|
||||||
compile_error! { "`jni_fn` attributed functions must have public visibility (`pub`)" }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,17 +30,21 @@ pub fn parseTorrent(env: JNIEnv, _class: JClass, buffer: JByteBuffer, limit: jin
|
|||||||
parse_bytebuffer(&mut env, buffer, limit, |dom, parser, _env| {
|
parse_bytebuffer(&mut env, buffer, limit, |dom, parser, _env| {
|
||||||
Some(dom.query_selector("table")?.filter_map(|e| {
|
Some(dom.query_selector("table")?.filter_map(|e| {
|
||||||
let html = e.get(parser)?.inner_html(parser);
|
let html = e.get(parser)?.inner_html(parser);
|
||||||
let grp = regex!("</span> ([0-9-]+) [0-9:]+</td>[\\s\\S]+</span> ([0-9.]+ [KMGT]B)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span>([^<]+)</td>[\\s\\S]+onclick=\"document.location='([^\"]+)'[^<]+>([^<]+)</a>").captures(&html)?;
|
if html.contains("Expunged") {
|
||||||
let name = unescape(&grp[8]).ok()?;
|
None
|
||||||
Some(Torrent {
|
} else {
|
||||||
posted: grp[1].to_string(),
|
let grp = regex!("</span> ([0-9-]+) [0-9:]+</td>[\\s\\S]+</span> ([0-9.]+ [KMGT]iB)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span> ([0-9]+)</td>[\\s\\S]+</span>([^<]+)</td>[\\s\\S]+onclick=\"document.location='([^\"]+)'[^<]+>([^<]+)</a>").captures(&html).unwrap();
|
||||||
size: grp[2].to_string(),
|
let name = unescape(&grp[8]).ok()?;
|
||||||
seeds: grp[3].parse().ok()?,
|
Some(Torrent {
|
||||||
peers: grp[4].parse().ok()?,
|
posted: grp[1].to_string(),
|
||||||
downloads: grp[5].parse().ok()?,
|
size: grp[2].to_string(),
|
||||||
url: grp[7].to_string(),
|
seeds: grp[3].parse().ok()?,
|
||||||
name: name.to_string()
|
peers: grp[4].parse().ok()?,
|
||||||
})
|
downloads: grp[5].parse().ok()?,
|
||||||
|
url: grp[7].to_string(),
|
||||||
|
name: name.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
}).collect::<Vec<Torrent>>())
|
}).collect::<Vec<Torrent>>())
|
||||||
}).unwrap().into_java(&env).forget().into_raw()
|
}).unwrap().into_java(&env).forget().into_raw()
|
||||||
}
|
}
|
||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.1.0" apply false
|
id("com.android.application") version "8.1.1" apply false
|
||||||
kotlin("android") version "1.9.0" apply false
|
kotlin("android") version "1.9.10" apply false
|
||||||
kotlin("plugin.serialization") version "1.9.0" apply false
|
kotlin("plugin.serialization") version "1.9.10" apply false
|
||||||
id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
|
id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false
|
||||||
id("com.mikepenz.aboutlibraries.plugin") version "10.8.3" apply false
|
id("com.mikepenz.aboutlibraries.plugin") version "10.8.3" apply false
|
||||||
id("org.mozilla.rust-android-gradle.rust-android") version "0.9.3" apply false
|
id("org.mozilla.rust-android-gradle.rust-android") version "0.9.3" apply false
|
||||||
id("dev.shreyaspatil.compose-compiler-report-generator") version "1.1.0" apply false
|
id("dev.shreyaspatil.compose-compiler-report-generator") version "1.1.0" apply false
|
||||||
@@ -13,7 +13,7 @@ allprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("Delete", Delete::class) {
|
tasks.register("Delete", Delete::class) {
|
||||||
delete(rootProject.buildDir)
|
delete(rootProject.layout.buildDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
+1
-1
@@ -19,5 +19,5 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||||
rootProject.name = "Ehviewer"
|
rootProject.name = "EhViewer"
|
||||||
include(":app")
|
include(":app")
|
||||||
Reference in new issue
Block a user