Compare commits

...
71 Commits
Author SHA1 Message Date
FooIbar d58e08e24c Release 1.8.9.6
Releases / Build (push) Has been cancelled
2023-09-20 19:49:48 +08:00
FooIbar e111ec74be Merge branch '1.8.x.x' into 1.8.9.x 2023-09-20 19:49:25 +08:00
FooIbar 68155e6a6b Refine modify favorites dialog layout 2023-09-16 18:36:44 +08:00
FooIbar bfbceefef2 Fix respectDefaultWidth not working 2023-09-16 17:08:13 +08:00
FooIbar 21b39135fe Tweak modify favorites dialog layout 2023-09-15 21:10:51 +08:00
FooIbar 6adc7f7221 Add option to disable category color harmonization 2023-09-13 20:21:06 +08:00
FooIbar 2c01acb4a6 Release 1.8.9.5
Releases / Build (push) Has been cancelled
2023-09-10 21:20:49 +08:00
FooIbar c0f6d0ab04 Merge branch '1.8.x.x' into 1.8.9.x
# Conflicts:
#	app/build.gradle.kts
2023-09-10 21:20:31 +08:00
FooIbar 6bfa8d1ea4 Fix crashing when reordering downloads 2023-09-10 21:18:23 +08:00
FooIbar 6f22269d93 Disable swiping and dragging when in selection mode 2023-09-10 21:18:05 +08:00
FooIbar b68dc98ae0 Fix drawer lock states saving 2023-09-10 19:58:12 +08:00
FooIbar f9ad1f724b Don't show search fab when search view is expanded 2023-09-10 18:30:46 +08:00
FooIbar afd22635bd Update releases.yml 2023-09-10 16:41:46 +08:00
FooIbar a9f9706be3 Migrate from intFromStrPref to intPref 2023-09-09 17:39:29 +08:00
FooIbar 41f35498fe Update syn to v2.0 2023-09-09 16:06:46 +08:00
FooIbar 58628b6556 Update jni_fn to v0.1.2 2023-09-09 16:05:17 +08:00
FooIbar 875858618e Maintenance mode 2023-09-08 21:39:56 +08:00
FooIbar 70b7bf390a Sync with 1.8.9.x versionCode 2023-09-08 18:24:00 +08:00
FooIbar de3d6de4cf Revert "Sync rootProjName with repo name"
This reverts commit de9fa5be5a.
2023-09-08 18:01:58 +08:00
FooIbar e3b4caa37d Ignore expunged torrents 2023-09-08 17:15:46 +08:00
renovate[bot] f8e19f7772 Update paging to v3.2.1 2023-09-08 16:52:03 +08:00
renovate[bot] ea5dd8b1bf Update nav_version to v2.7.2 2023-09-08 16:51:29 +08:00
renovate[bot] bd7db1a016 Update dependency io.arrow-kt:arrow-stack to v1.2.1 2023-09-08 15:30:07 +08:00
renovate[bot] 02ac048d74 Update dependency androidx.lifecycle:lifecycle-process to v2.6.2 2023-09-08 15:26:41 +08:00
renovate[bot] 9a3c9b9cbd Update dependency androidx.fragment:fragment-ktx to v1.7.0-alpha04 2023-09-08 14:55:29 +08:00
renovate[bot] 80c77c740e Update dependency androidx.core:core-ktx to v1.12.0 2023-09-08 14:55:05 +08:00
renovate[bot] 0f62662963 Update dependency androidx.collection:collection-ktx to v1.3.0-rc01 2023-09-08 14:54:23 +08:00
renovate[bot] 576d6515db Update dependency androidx.activity:activity-compose to v1.8.0-beta01 2023-09-08 14:53:55 +08:00
FooIbar 30d29bfa80 Fix torrent parsing 2023-09-08 14:52:04 +08:00
FooIbar c7c0446f23 Tweak ParseException 2023-09-08 14:49:28 +08:00
FooIbar 134777584d Merge branch 'Ehviewer-Overhauled:1.8.x.x' into 1.8.x.x 2023-09-06 16:52:10 +08:00
renovate[bot] 0b64eadb0c Update actions/checkout action to v4 2023-09-04 16:52:00 +00:00
FooIbar ea93960bf4 Fix pages fields in AdvancedSearch 2023-09-01 19:30:21 +08:00
renovate[bot] 71032e3010 Update ktor to v2.3.4 2023-08-31 21:07:43 +00:00
FooIbar 9746f20b60 Release 1.8.9.4
Releases / Build (push) Has been cancelled
2023-08-30 16:41:26 +08:00
FooIbar 501148179b Merge branch '1.8.x.x' into 1.8.9.x 2023-08-30 16:40:48 +08:00
FooIbar d6a3a351f5 Fix UI not updating after download state changed 2023-08-30 16:37:37 +08:00
FooIbar a574bca48e Kotlin 1.9.10 2023-08-30 16:22:18 +08:00
FooIbar 785faf1bf4 Upgrade ndk to r26 RC 1 2023-08-30 16:20:32 +08:00
FooIbar fbcc32b3ee Merge remote-tracking branch 'upstream/1.8.x.x' into 1.8.x.x
# Conflicts:
#	app/build.gradle.kts
2023-08-25 13:28:07 +08:00
renovate[bot] 46b19a9de5 Update room_version to v2.6.0-beta01 2023-08-24 15:49:05 +00:00
renovate[bot] d2f47d51f9 Update plugin com.google.devtools.ksp to v1.9.10-1.0.13 2023-08-24 10:15:46 +00:00
renovate[bot] db380507f5 Update nav_version to v2.7.1 2023-08-24 07:03:27 +00:00
renovate[bot] 84cf67d21a Update dependency composeOptions to v1.5.2 2023-08-24 03:49:47 +00:00
renovate[bot] 8f53591800 Update dependency androidx.fragment:fragment-ktx to v1.7.0-alpha03 2023-08-24 01:47:24 +00:00
renovate[bot] 65ffd78844 Update dependency androidx.activity:activity-compose to v1.8.0-alpha07 2023-08-23 23:18:25 +00:00
renovate[bot] c00b5e6635 Update serialization to v1.6.0 2023-08-22 23:06:02 +00:00
FooIbar 8160991086 Don't respond to touch events when scrolling 2023-08-22 19:27:44 +08:00
FooIbar f0c933d97a Replace deprecated method 2023-08-22 18:55:39 +08:00
renovate[bot] 3fb8d3e73b Update plugin com.android.application to v8.1.1 2023-08-22 06:12:17 +00:00
renovate[bot] d8efff9921 Update accompanist_version to v0.32.0 2023-08-21 14:07:10 +00:00
FooIbar 32170688b6 Support clearing image cache for specific gallery 2023-08-21 18:39:35 +08:00
FooIbar d08491bf7e Release 1.8.9.3
Releases / Build (push) Has been cancelled
2023-08-17 22:54:38 +08:00
FooIbar 12f6f08d81 Merge branch '1.8.x.x' into 1.8.9.x 2023-08-17 22:53:52 +08:00
FooIbar ca74bfe2e4 Merge branch 'Ehviewer-Overhauled:1.8.x.x' into 1.8.x.x 2023-08-17 22:52:55 +08:00
renovate[bot] 41767f0fd7 Update dependency gradle to v8.3 2023-08-17 14:50:21 +00:00
FooIbar c62ca4dd23 Fix DownloadsScene crash
java.lang.LinkageError: Method com.hippo.ehviewer.databinding.ActivityMainBinding com.hippo.ehviewer.ui.scene.DownloadsScene.getBinding() overrides final method in class Lcom/hippo/ehviewer/ui/scene/SearchBarScene
2023-08-17 20:24:34 +08:00
FooIbar 9c5eddb903 Remove embedded Cronet library 2023-08-17 17:51:44 +08:00
FooIbar 4a0df07805 Set User-Agent in CronetEngine.Builder 2023-08-17 00:03:13 +08:00
FooIbar 37a3edd44b Revert "Add quichint"
This reverts commit 4e427f27
2023-08-16 23:50:20 +08:00
FooIbar f117abd483 Revert "Add option for QUIC support"
This reverts commit 861c05db
2023-08-16 23:27:38 +08:00
FooIbar 4593714298 Cleanup DownloadManager 2023-08-16 23:12:18 +08:00
FooIbar a148fd6ec3 Use flow for DownloadsScene UI updates 2023-08-16 23:12:18 +08:00
FooIbar 23a44e2f3b Migrate DownloadAdapter to ListAdapter 2023-08-16 20:23:57 +08:00
FooIbar 01e03ec0f0 Support search in DownloadsScene 2023-08-15 23:03:52 +08:00
FooIbar e2345cc756 Release 1.8.9.2
Releases / Build (push) Has been cancelled
2023-08-15 17:48:26 +08:00
FooIbar 8d0b8534b2 Merge branch '1.8.x.x' into 1.8.9.x 2023-08-15 17:48:11 +08:00
FooIbar 4594ac592c Merge branch 'Ehviewer-Overhauled:1.8.x.x' into 1.8.x.x 2023-08-15 17:44:38 +08:00
FooIbar eee0f2dded Fix FavoritesScene crash 2023-08-15 17:24:21 +08:00
FooIbar b4080302c3 Refine GalleryListParser 2023-08-15 16:22:55 +08:00
renovate[bot] 337e210b5b Update dependency com.google.android.material:material to v1.11.0-alpha02 2023-08-15 05:08:26 +00:00
70 changed files with 508 additions and 1129 deletions

No files matched your search

+1 -4
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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"))
} }
} }
+1
View File
@@ -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
@@ -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
@@ -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)
} }
+7 -10
View File
@@ -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"
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+6 -4
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+20 -20
View File
@@ -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>
+6 -4
View File
@@ -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>
+1 -1
View File
@@ -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"] }
+13 -25
View File
@@ -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(),
-2
View File
@@ -1,2 +0,0 @@
/target
Cargo.lock
-22
View File
@@ -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"
-398
View File
@@ -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`)" }
}
)
);
}
}
+15 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -19,5 +19,5 @@ dependencyResolutionManagement {
} }
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "Ehviewer" rootProject.name = "EhViewer"
include(":app") include(":app")