Compare commits

..
81 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
FooIbar 63f9c8de79 Release 1.8.9.1
Releases / Build (push) Has been cancelled
2023-08-14 23:17:22 +08:00
FooIbar f792f2c968 Merge branch '1.8.x.x' into 1.8.9.x 2023-08-14 23:16:13 +08:00
FooIbar a05b9174d4 Tweak EhScreen layout 2023-08-14 23:12:39 +08:00
FooIbar aa8adfb6f7 Use LazyColumn in showSelectItem() 2023-08-14 23:10:32 +08:00
FooIbar 1034fcd7b0 Fix favorite button color 2023-08-14 21:11:55 +08:00
FooIbar 69a26aeedb Implement swipe-to-remove in Downloads and Favorites 2023-08-14 21:11:55 +08:00
FooIbar dd607b41e9 Show search fab in FavoritesScene 2023-08-13 19:13:08 +08:00
FooIbar fd45319715 Fix toplist 2023-08-13 00:14:44 +08:00
FooIbar c34ba56896 Start a new fragment if search was performed from Popular or Toplist 2023-08-12 22:47:20 +08:00
FooIbar 6e00a4133c Fix nav checked item 2023-08-12 17:51:52 +08:00
74 changed files with 759 additions and 1257 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
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
- 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 只提及一个问题。如果您有多个问题报告,烦请发起多个 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
contact_links:
- name: 所有其他问题 / All other questions
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
about: 转到 Discussions / Turn to discussions
blank_issues_enabled: false
@@ -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
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
with:
submodules: 'recursive'
@@ -25,7 +25,7 @@ jobs:
- name: Install NDK
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
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
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
with:
submodules: 'recursive'
@@ -20,7 +20,7 @@ jobs:
- name: Install NDK
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
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
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
with:
submodules: 'recursive'
@@ -23,7 +23,7 @@ jobs:
- name: Install NDK
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
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">
<strong>English</strong>
<span> | </span>
+17 -19
View File
@@ -21,7 +21,7 @@ plugins {
android {
compileSdk = 34
buildToolsVersion = "34.0.0"
ndkVersion = "26.0.10404224"
ndkVersion = "26.0.10636728"
splits {
abi {
@@ -64,7 +64,7 @@ android {
minSdk = 28
targetSdk = 34
versionCode = 180043
versionName = "1.8.9.0"
versionName = "1.8.9.6"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -159,7 +159,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
kotlinCompilerExtensionVersion = "1.5.3"
}
namespace = "com.hippo.ehviewer"
@@ -167,11 +167,11 @@ android {
dependencies {
// 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.biometric:biometric-ktx:1.2.0-alpha05")
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
// 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.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.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
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
implementation("androidx.lifecycle:lifecycle-process:2.6.2")
// 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-ui-ktx:$nav_version")
implementation("androidx.navigation:navigation-compose:$nav_version")
// 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-runtime-ktx:$paging")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.recyclerview:recyclerview-selection:1.2.0-alpha01")
// 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")
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.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
// 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-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"
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.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")
// https://coil-kt.github.io/coil/changelog/
@@ -246,13 +246,11 @@ dependencies {
implementation("io.coil-kt:coil-compose")
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-utils-jvm:$ktor")
implementation("org.chromium.net:cronet-embedded:113.5672.61")
val serialization = "1.5.1"
val serialization = "1.6.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:$serialization")
@@ -298,6 +296,6 @@ tasks.configureEach {
if ((name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders")) {
dependsOn("cargoBuild")
// 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.Limits { *; }
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
-keep class com.hippo.ehviewer.ui.scene.SearchBarScene { getBinding(); }
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
@@ -11,7 +11,6 @@ import androidx.navigation.NavOptions
@SuppressLint("PrivateResource")
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
@@ -30,16 +30,16 @@ object Settings : DataStorePreferences(null) {
var downloadQuery by stringOrNullPref("image_query", null)
var downloadFragment by stringOrNullPref("image_fragment", null)
var archivePasswds by stringSetOrNullPref("archive_passwds")
var downloadDelay by intFromStrPref("download_delay", 0)
var gallerySite by intFromStrPref("gallery_site", 0).observed { updateWhenGallerySiteChanges() }
var multiThreadDownload by intFromStrPref("download_thread", 3)
var preloadImage by intFromStrPref("preload_image", 5)
var theme by intFromStrPref("theme", -1).observed { updateWhenThemeChanges() }
var listMode by intFromStrPref("list_mode", 0)
var detailSize by intFromStrPref("detail_size", 0)
var thumbResolution by intFromStrPref("thumb_resolution", 0)
var readCacheSize by intFromStrPref("read_cache_size", 640)
var launchPage by intFromStrPref("launch_page", 0)
var downloadDelay by intPref("download_delay_2", 0)
var gallerySite by intPref("gallery_site_2", 0).observed { updateWhenGallerySiteChanges() }
var multiThreadDownload by intPref("download_thread_2", 3)
var preloadImage by intPref("preload_image_2", 5)
var theme by intPref("theme_2", -1).observed { updateWhenThemeChanges() }
var listMode by intPref("list_mode_2", 0)
var detailSize by intPref("detail_size_2", 0)
var thumbResolution by intPref("thumb_resolution_2", 0)
var readCacheSize by intPref("read_cache_size_2", 640)
var launchPage by intPref("launch_page_2", 0)
var forceEhThumb by boolPref("force_eh_thumb", false)
var showComments by boolPref("show_gallery_comments", true)
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 needSignIn by boolPref("need_sign_in", true)
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 downloadOriginImage by boolPref("download_origin_image", false)
var enableQuic by boolPref("enable_quic", true)
var thumbSizeDp by intPref("thumb_size_", 120)
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
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)
}
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> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
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) {
throw EhException(body)
} else {
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e)
throw EhException(appCtx.getString(R.string.error_parse_error))
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e, body)
throw EhException(appCtx.getString(R.string.error_parse_error), e)
}
}
@@ -126,12 +126,18 @@ object EhUtils {
MISC -> BG_COLOR_MISC
else -> BG_COLOR_UNKNOWN
}.toInt()
return harmonizeWithRole(context, primary)
return if (Settings.harmonizeCategoryColor) {
harmonizeWithRole(context, primary)
} else {
primary
}
}
@Composable
fun getCategoryColor(category: Int) = Color(getCategoryColor(LocalContext.current, category))
val categoryTextColor = Color(0xffe6e0e9)
fun signOut() {
EhCookieStore.signOut()
Settings.avatar = null
@@ -70,8 +70,8 @@ data class ListUrlBuilder(
mPrev = index?.takeUnless { isNext }
}
fun setJumpTo(jumpTo: String?) {
mJumpTo = jumpTo
fun setJumpTo(jumpTo: Int) {
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
}
var keyword: String?
@@ -16,16 +16,7 @@
package com.hippo.ehviewer.client.exception
class ParseException : EhException {
val body: String
constructor(detailMessage: String?) : super(detailMessage)
constructor(detailMessage: String?, body: String) : super(detailMessage) {
this.body = body
}
constructor(detailMessage: String?, body: String, cause: Throwable?) : super(
detailMessage,
cause,
) {
this.body = body
}
constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)
}
@@ -12,7 +12,7 @@ object FavoritesParser {
}
val catArray = arrayOfNulls<String>(10)
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)
return Result(catArray.requireNoNulls(), countArray, result)
}
@@ -114,9 +114,9 @@ object GalleryDetailParser {
gd.token = groupValues[2]
gd.apiUid = groupValues[3].toLongOrNull() ?: -1L
gd.apiKey = groupValues[4]
} ?: throw ParseException("Can't parse gallery detail", body)
} ?: throw ParseException("Can't parse gallery detail")
if (gd.gid == -1L) {
throw ParseException("Can't parse gallery detail", body)
throw ParseException("Can't parse gallery detail")
}
PATTERN_TORRENT.find(body)?.run {
gd.torrentUrl = groupValues[1].trim().unescapeXml()
@@ -214,7 +214,7 @@ object GalleryDetailParser {
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
throw ParseException("Can't parse gallery detail", body)
throw ParseException("Can't parse gallery detail")
}
// newer version
@@ -450,7 +450,7 @@ object GalleryDetailParser {
*/
fun parsePreviewPages(body: String): Int {
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 {
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>> {
@@ -1,9 +1,12 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
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.NOT_FAVORITED
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.ParseException
object GalleryListParser {
suspend fun parse(body: String) = runCatching {
@@ -15,13 +18,21 @@ object GalleryListParser {
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(
val prev: String? = null,
val next: String? = null,
val galleryInfoList: ArrayList<BaseGalleryInfo> = arrayListOf(),
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
)
private external fun parseGalleryInfoList(e: String): GalleryListResult
@@ -33,7 +33,7 @@ object GalleryMultiPageViewerPTokenParser {
imagelist.parseAs<List<Item>>().map(Item::token)
}.getOrElse {
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([^\"]+)\">")
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 imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
groupValues[1].trim().unescapeXml()
@@ -41,7 +41,7 @@ object GalleryPageApiParser {
if (!imageUrl.isNullOrEmpty()) {
return Result(imageUrl, skipHathKey, originImageUrl)
} 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()) {
Result(imageUrl, skipHathKey, originImageUrl, showKey)
} 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
return Funds(fundsGP, fundsC)
}
throw ParseException("Parse funds error", body)
throw ParseException("Parse funds error")
}
@Parcelize
@@ -46,7 +46,7 @@ object ProfileParser {
Result(displayName, avatar)
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw ParseException("Parse forums error", body)
throw ParseException("Parse forums error")
}
}
@@ -35,7 +35,7 @@ object SignInParser {
if (m.find()) {
throw EhException(m.group(1) ?: m.group(2))
} 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
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
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(
@@ -1,9 +1,12 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.coil
import android.net.Uri
import android.os.Build
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresExtension
import coil.ComponentRegistry
import coil.ImageLoader
import coil.decode.DataSource
@@ -27,7 +30,7 @@ class CronetHttpUriFetcher(private val data: String, private val options: Option
val diskCache = requireNotNull(imageLoader.diskCache)
val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run {
val success = cronetRequest(data) {
disableCache()
setCacheDisabled(true)
}.execute {
diskCache.suspendEdit(diskCacheKey) {
RandomAccessFile(data.toFile(), "rw").use {
@@ -1,5 +1,13 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
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.client.CHROME_ACCEPT
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
@@ -12,10 +20,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.HttpUrl.Companion.toHttpUrl
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 java.nio.ByteBuffer
import java.nio.channels.FileChannel
@@ -28,26 +32,23 @@ import kotlin.coroutines.resumeWithException
private const val TAG = "CronetRequest"
val pool = DirectByteBufferPool(32)
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
enableBrotli(true)
val cronetHttpClient: HttpEngine = HttpEngine.Builder(appCtx).apply {
setEnableBrotli(true)
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
addQuicHint("e-hentai.org", 443, 443)
addQuicHint("forums.e-hentai.org", 443, 443)
addQuicHint("exhentai.org", 443, 443)
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
setUserAgent(CHROME_USER_AGENT)
}.build()
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 {
lateinit var consumer: (ByteBuffer) -> Unit
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
lateinit var request: UrlRequest
lateinit var onError: (Throwable) -> 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) {
logcat(tag = TAG) { "Redirected to $url" }
req.followRedirect()
@@ -66,16 +67,19 @@ class CronetRequest {
readerCont.resume(Unit)
}
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: CronetException) {
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: HttpException) {
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 {
request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply {
request = cronetHttpClient.newUrlRequestBuilder(url, cronetHttpClientExecutor, callback).apply {
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
addHeader("User-Agent", CHROME_USER_AGENT)
addHeader("Accept", CHROME_ACCEPT)
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
referer?.let { addHeader("Referer", it) }
@@ -25,7 +25,7 @@ import java.time.Instant
),
],
)
class DownloadEntity(
data class DownloadEntity(
@PrimaryKey
@ColumnInfo(name = "GID")
var gid: Long,
@@ -44,21 +44,23 @@ class DownloadEntity(
@ColumnInfo(name = "POSITION")
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 {
@Ignore
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
constructor() : this(0)
}
data class DownloadInfo(
@@ -71,7 +71,7 @@ object DownloadManager : OnSpiderListener {
// Store download info wait to start
private val mWaitList = LinkedList<DownloadInfo>()
private val mSpeedReminder = SpeedReminder()
private val mDownloadInfoListeners = mutableListOf<DownloadInfoListener>()
private var mDownloadInfoListener: DownloadInfoListener? = null
private val mNotifyTaskPool = ConcurrentPool<NotifyTask?>(5)
private var mDownloadListener: DownloadListener? = null
private var mCurrentTask: DownloadInfo? = null
@@ -125,14 +125,6 @@ object DownloadManager : OnSpiderListener {
return info?.state ?: DownloadInfo.STATE_INVALID
}
fun addDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
mDownloadInfoListeners.add(downloadInfoListener)
}
fun removeDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
mDownloadInfoListeners.remove(downloadInfoListener)
}
fun setDownloadListener(listener: DownloadListener?) {
mDownloadListener = listener
}
@@ -166,12 +158,7 @@ object DownloadManager : OnSpiderListener {
mDownloadListener!!.onStart(info)
}
// Notify state update
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -192,12 +179,7 @@ object DownloadManager : OnSpiderListener {
// Update in DB
EhDB.putDownloadInfo(info)
// Notify state update
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
// Make sure download is running
ensureDownload()
}
@@ -227,9 +209,7 @@ object DownloadManager : OnSpiderListener {
EhDB.putDownloadInfo(info)
// Notify
for (l in mDownloadInfoListeners) {
l.onAdd(info, list, list.size - 1)
}
mDownloadInfoListener?.onUpdate(info)
// Make sure download is running
ensureDownload()
@@ -259,9 +239,7 @@ object DownloadManager : OnSpiderListener {
}
if (update) {
// Notify Listener
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
mDownloadInfoListener?.onUpdateAll()
// Ensure download
ensureDownload()
}
@@ -271,33 +249,23 @@ object DownloadManager : OnSpiderListener {
private val _stateFlow = callbackFlow {
val listener = object : DownloadInfoListener {
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
trySend(info)
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
trySend(info)
override fun onUpdate(info: DownloadInfo) {
trySend(info.gid)
}
override fun onUpdateAll() {
// No-op
}
override fun onReload() {
// No-op
}
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
trySend(info)
trySend(0)
}
}
addDownloadInfoListener(listener)
mDownloadInfoListener = listener
awaitClose {
removeDownloadInfoListener(listener)
mDownloadInfoListener = null
}
}.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() {
var update = false
@@ -315,9 +283,7 @@ object DownloadManager : OnSpiderListener {
}
if (update) {
// Notify Listener
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
mDownloadInfoListener?.onUpdateAll()
// Ensure download
ensureDownload()
}
@@ -353,9 +319,7 @@ object DownloadManager : OnSpiderListener {
// Notify
if (notify) {
for (l in mDownloadInfoListeners) {
l.onReload()
}
mDownloadInfoListener?.onUpdateAll()
}
}
@@ -386,9 +350,7 @@ object DownloadManager : OnSpiderListener {
// Notify
withUIContext {
for (l in mDownloadInfoListeners) {
l.onAdd(info, list, list.size - 1)
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -396,12 +358,7 @@ object DownloadManager : OnSpiderListener {
val info = stopDownloadInternal(gid)
if (info != null) {
// Update listener
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
// Ensure download
ensureDownload()
}
@@ -411,12 +368,7 @@ object DownloadManager : OnSpiderListener {
val info = stopCurrentDownloadInternal()
if (info != null) {
// Update listener
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
// Ensure download
ensureDownload()
}
@@ -426,9 +378,7 @@ object DownloadManager : OnSpiderListener {
stopRangeDownloadInternal(gidList)
// Update listener
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
mDownloadInfoListener?.onUpdateAll()
// Ensure download
ensureDownload()
@@ -447,9 +397,7 @@ object DownloadManager : OnSpiderListener {
stopCurrentDownloadInternal()
// Notify mDownloadInfoListener
for (l in mDownloadInfoListeners) {
l.onUpdateAll()
}
mDownloadInfoListener?.onUpdateAll()
}
suspend fun deleteDownload(gid: Long, deleteFiles: Boolean = false) {
@@ -470,9 +418,7 @@ object DownloadManager : OnSpiderListener {
if (index >= 0) {
list.remove(info)
// Update listener
for (l in mDownloadInfoListeners) {
l.onRemove(info, list, index)
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -495,15 +441,13 @@ object DownloadManager : OnSpiderListener {
allInfoList.removeAll(list.toSet())
// Update listener
for (l in mDownloadInfoListeners) {
l.onReload()
}
mDownloadInfoListener?.onUpdateAll()
// Ensure download
ensureDownload()
}
fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
private fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
val info = allInfoList.removeAt(fromPosition)
allInfoList.add(toPosition, info)
val range = if (fromPosition < toPosition) fromPosition..toPosition else toPosition..fromPosition
@@ -515,12 +459,15 @@ object DownloadManager : OnSpiderListener {
return list
}
fun moveDownload(label: String?, fromPosition: Int, toPosition: Int) =
getInfoListForLabel(label)!!.let {
val absFromPosition = allInfoList.indexOf(it[fromPosition])
val absToPosition = allInfoList.indexOf(it[toPosition])
moveDownload(absFromPosition, absToPosition)
fun moveDownload(fromItem: DownloadInfo, toItem: DownloadInfo): List<DownloadInfo> {
val fromPosition = allInfoList.indexOf(fromItem)
val toPosition = allInfoList.indexOf(toItem)
return if (fromPosition != -1 && toPosition != -1) {
moveDownload(fromPosition, toPosition)
} else {
emptyList()
}
}
suspend fun resetAllReadingProgress() = coroutineScope {
allInfoList.forEach { info ->
@@ -658,9 +605,7 @@ object DownloadManager : OnSpiderListener {
// Save to DB
EhDB.putDownloadInfo(info)
}
for (l in mDownloadInfoListeners) {
l.onReload()
}
mDownloadInfoListener?.onUpdateAll()
}
suspend fun addLabel(label: String?) {
@@ -810,30 +755,15 @@ object DownloadManager : OnSpiderListener {
}
interface DownloadInfoListener {
/**
* Add the special info to the special position
*/
fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int)
/**
* 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
*/
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 {
@@ -940,12 +870,7 @@ object DownloadManager : OnSpiderListener {
Log.e(TAG, "Current task is null, but it should not be")
} else {
info.total = mPages
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -974,12 +899,7 @@ object DownloadManager : OnSpiderListener {
if (mDownloadListener != null) {
mDownloadListener!!.onGetPage(info)
}
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -992,12 +912,7 @@ object DownloadManager : OnSpiderListener {
info.finished = mFinished
info.downloaded = mDownloaded
info.total = mTotal
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
}
@@ -1036,12 +951,7 @@ object DownloadManager : OnSpiderListener {
if (mDownloadListener != null) {
mDownloadListener!!.onFinish(info)
}
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
// Start next download
ensureDownload()
}
@@ -1127,12 +1037,7 @@ object DownloadManager : OnSpiderListener {
if (mDownloadListener != null) {
mDownloadListener!!.onDownload(info)
}
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
if (list != null) {
for (l in mDownloadInfoListeners) {
l.onUpdate(info, list)
}
}
mDownloadInfoListener?.onUpdate(info)
}
mBytesRead = 0
if (!mStop) {
@@ -135,9 +135,9 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
): Boolean {
return if (isCronetSupported) {
cronetRequest(url, referer) {
disableCache()
setCacheDisabled(true)
}.execute { info ->
val headers = info.allHeaders
val headers = info.headers.asMap
val type = headers["Content-Type"]?.first()?.toMediaType()?.subtype ?: "jpg"
val length = headers["Content-Length"]!!.first().toLong()
saveResponseMeta(index, type, length) { file ->
@@ -20,6 +20,8 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
@@ -28,7 +30,12 @@ import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.HeartBroken
import androidx.compose.material.icons.filled.MenuBook
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.EhDB
@@ -55,6 +62,7 @@ import com.hippo.ehviewer.util.requestPermission
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.sync.Mutex
@@ -312,7 +320,6 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
1 -> withUIContext {
if (downloaded) {
confirmRemoveDownload(info)
DownloadManager.deleteDownload(info.gid)
} else {
CommonOperations.startDownload(this@with, info, false)
}
@@ -342,12 +349,28 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
private const val MAX_FAVNOTE_CHAR = 200
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo) = awaitPermissionOrCancel(
confirmText = android.R.string.ok,
dismissText = android.R.string.cancel,
title = R.string.download_remove_dialog_title,
text = { Text(text = stringResource(id = R.string.download_remove_dialog_message, info.title.orEmpty())) },
)
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo, onDismiss: () -> Unit = {}) {
var checked by mutableStateOf(Settings.removeImageFiles)
awaitPermissionOrCancel(
confirmText = android.R.string.ok,
dismissText = android.R.string.cancel,
title = R.string.download_remove_dialog_title,
onDismiss = onDismiss,
text = {
Column {
Text(text = stringResource(id = R.string.download_remove_dialog_message, info.title.orEmpty()))
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = checked, onCheckedChange = { checked = it })
Text(text = stringResource(id = R.string.download_remove_dialog_check_text))
}
}
},
)
Settings.removeImageFiles = checked
withIOContext {
DownloadManager.deleteDownload(info.gid, checked)
}
}
suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) {
val defaultLabel = appCtx.getString(R.string.default_download_label_name)
@@ -197,7 +197,7 @@ class MainActivity : EhActivity() {
// Trick: Tweak NavigationUI to disable multiple backstack
binding.navView.setNavigationItemSelectedListener {
val navigationView = binding.navView
val handled = onNavDestinationSelected2(it, navController)
val handled = navigationView.checkedItem?.itemId == it.itemId || onNavDestinationSelected2(it, navController)
if (handled) {
val parent = navigationView.parent
if (parent is Openable) {
@@ -398,6 +398,10 @@ class MainActivity : EhActivity() {
}
}
fun clearNavCheckedItem() {
binding.navView.setCheckedItem(R.id.nav_stub)
}
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
showTip(getString(id), length, useToast)
}
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
@@ -71,7 +72,7 @@ class SearchLayout @JvmOverloads constructor(
}
}
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) {
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))) {
@@ -28,6 +28,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R
import com.hippo.ehviewer.util.toIntOrDefault
@Composable
fun SearchAdvanced(
@@ -55,9 +56,6 @@ fun SearchAdvanced(
onExpandedChange = { expanded = !expanded },
) {
val softwareKeyboardController = LocalSoftwareKeyboardController.current
SideEffect {
softwareKeyboardController?.hide()
}
OutlinedTextField(
modifier = Modifier.menuAnchor(),
readOnly = true,
@@ -71,6 +69,9 @@ fun SearchAdvanced(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
SideEffect {
softwareKeyboardController?.hide()
}
minRatingItems.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption) },
@@ -88,18 +89,18 @@ fun SearchAdvanced(
Checkbox(checked = enabled, onCheckedChange = { enabled = it })
Text(text = stringResource(id = R.string.search_sp), modifier = Modifier.align(Alignment.CenterVertically))
OutlinedTextField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
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),
singleLine = true,
enabled = enabled,
)
Text(text = stringResource(id = R.string.search_sp_to), modifier = Modifier.align(Alignment.CenterVertically))
OutlinedTextField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
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),
singleLine = true,
enabled = enabled,
@@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
@@ -28,12 +29,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
@@ -109,6 +112,7 @@ fun GalleryInfoListItem(
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
color = if (Settings.harmonizeCategoryColor) Color.Unspecified else EhUtils.categoryTextColor,
textAlign = TextAlign.Center,
)
Row(
@@ -198,10 +202,11 @@ fun GalleryInfoGridItem(
modifier = Modifier.aspectRatio(aspect).fillMaxWidth(),
contentScale = ContentScale.Crop,
)
val categoryColor = EhUtils.getCategoryColor(info.category)
Badge(
modifier = Modifier.align(Alignment.TopEnd).width(32.dp).height(24.dp),
containerColor = EhUtils.getCategoryColor(info.category),
contentColor = MaterialTheme.colorScheme.onSurface,
containerColor = categoryColor,
contentColor = if (Settings.harmonizeCategoryColor) contentColorFor(categoryColor) else EhUtils.categoryTextColor,
) {
simpleLang?.let {
Text(text = it.uppercase())
@@ -26,7 +26,10 @@ import androidx.core.view.GravityCompat
import androidx.core.view.SoftwareKeyboardControllerCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.android.material.sidesheet.SideSheetDialog
import com.hippo.ehviewer.ui.MainActivity
@@ -155,8 +158,10 @@ abstract class BaseScene : Fragment() {
createDrawerView(savedInstanceState)?.let {
sideSheetDialog = SideSheetDialog(requireContext()).apply {
window?.decorView?.apply {
setViewTreeLifecycleOwner(viewLifecycleOwner)
setViewTreeSavedStateRegistryOwner(this@BaseScene)
val owner = viewLifecycleOwner
setViewTreeLifecycleOwner(owner)
setViewTreeViewModelStoreOwner(owner as ViewModelStoreOwner)
setViewTreeSavedStateRegistryOwner(owner as SavedStateRegistryOwner)
}
setContentView(it)
}
@@ -46,7 +46,9 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -54,11 +56,11 @@ import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import arrow.core.partially1
import coil.compose.AsyncImage
@@ -79,11 +81,11 @@ import com.hippo.ehviewer.databinding.ItemDownloadBinding
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
import com.hippo.ehviewer.databinding.SceneDownloadBinding
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.Companion.clear
import com.hippo.ehviewer.download.downloadDir
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.confirmRemoveDownload
import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.CheckBoxDialogBuilder
@@ -98,8 +100,10 @@ import com.hippo.ehviewer.ui.main.requestOf
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.CropDefaults
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.LongList
import com.hippo.ehviewer.util.containsIgnoreCase
import com.hippo.ehviewer.util.sendTo
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.launchIO
@@ -108,20 +112,21 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.pxToDp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import rikka.core.res.resolveColor
import com.hippo.ehviewer.download.DownloadManager as downloadManager
@SuppressLint("RtlHardcoded")
class DownloadsScene :
BaseToolbarScene(),
DownloadInfoListener,
SearchBarScene(),
OnClickFabListener {
/*---------------
Whole life cycle
---------------*/
private var mKeyword: String? = null
private var mLabel: String? = null
private var mList: List<DownloadInfo>? = null
private val mList get() = mAdapter?.currentList
/*---------------
View life cycle
@@ -132,18 +137,23 @@ class DownloadsScene :
private var mAdapter: DownloadAdapter? = null
private var mItemTouchHelper: ItemTouchHelper? = null
private val tracker get() = mAdapter!!.tracker!!
private var mInitPosition = -1
private var mGid = -1L
private var mLabelAdapter: DownloadLabelAdapter? = null
private lateinit var mLabels: MutableList<String>
private var mType = -1
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
private fun initLabels() {
context ?: return
val listLabel = downloadManager.labelList
mLabels = ArrayList(listLabel.size + LABEL_OFFSET)
// Add "All" and "Default" label names
mLabels.add(getString(R.string.download_all))
mLabels.add(getString(R.string.default_download_label_name))
mLabels = arrayListOf(
getString(R.string.download_all),
getString(R.string.default_download_label_name),
)
listLabel.forEach {
mLabels.add(it.label)
}
@@ -161,18 +171,7 @@ class DownloadsScene :
val info = DownloadManager.getDownloadInfo(gid)
if (null != info) {
mLabel = info.label
updateForLabel()
updateView()
// Get position
if (null != mList) {
val position = mList!!.indexOf(info)
if (position >= 0 && null != _binding) {
binding.recyclerView.scrollToPosition(position)
} else {
mInitPosition = position
}
}
mGid = gid
return true
}
}
@@ -181,7 +180,6 @@ class DownloadsScene :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DownloadManager.addDownloadInfoListener(this)
if (savedInstanceState == null) {
onInit()
} else {
@@ -189,50 +187,54 @@ class DownloadsScene :
}
}
override fun onDestroy() {
super.onDestroy()
mList = null
DownloadManager.removeDownloadInfoListener(this)
private fun filter(info: DownloadInfo) = info.run {
(mType == -1 || state == mType) &&
mKeyword?.let {
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() {
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 }
}
mList = if (mType != -1) {
list.filter { it.state == mType }
} else {
list
}
if (mAdapter != null) {
mAdapter!!.notifyDataSetChanged()
}
updateInfoList()
Settings.recentDownloadLabel = mLabel
}
private fun updateTitle() {
setTitle(
getString(
R.string.scene_download_title,
if (mLabel != null) mLabel else getString(R.string.download_all),
),
)
val title = getString(R.string.scene_download_title, mLabel ?: getString(R.string.download_all))
setSearchBarHint(title)
setEditTextHint(getString(R.string.search_bar_hint, title))
}
private fun onInit() {
if (!handleArguments(arguments)) {
mLabel = Settings.recentDownloadLabel
updateForLabel()
}
}
private fun onRestore(savedInstanceState: Bundle) {
mLabel = savedInstanceState.getString(KEY_LABEL)
updateForLabel()
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -247,6 +249,7 @@ class DownloadsScene :
savedInstanceState: Bundle?,
): View {
_binding = SceneDownloadBinding.inflate(inflater, container!!)
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
binding.run {
setLiftOnScrollTargetView(recyclerView)
mViewTransition = ViewTransition(content, tip)
@@ -288,11 +291,6 @@ class DownloadsScene :
recyclerView.layoutManager = layoutManager
recyclerView.clipToPadding = false
recyclerView.clipChildren = false
// Cancel change animation
val itemAnimator = recyclerView.itemAnimator
if (itemAnimator is SimpleItemAnimator) {
itemAnimator.supportsChangeAnimations = false
}
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
val decoration = object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
@@ -300,20 +298,14 @@ class DownloadsScene :
}
}
recyclerView.addItemDecoration(decoration)
if (mInitPosition >= 0) {
recyclerView.scrollToPosition(mInitPosition)
mInitPosition = -1
}
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 getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
return if (tracker.isInCustomChoice) {
0
} else {
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT)
}
}
override fun onMove(
recyclerView: RecyclerView,
@@ -325,18 +317,24 @@ class DownloadsScene :
if (fromPosition == toPosition) {
return false
}
val list = when (mLabel) {
null -> DownloadManager.moveDownload(fromPosition, toPosition)
getString(R.string.default_download_label_name) -> DownloadManager.moveDownload(null, fromPosition, toPosition)
else -> DownloadManager.moveDownload(mLabel, fromPosition, toPosition)
}
mAdapter!!.notifyItemMoved(fromPosition, toPosition)
val fromItem = mList!![fromPosition]
val toItem = mList!![toPosition]
val list = DownloadManager.moveDownload(fromItem, toItem)
updateInfoList()
lifecycleScope.launchIO {
EhDB.updateDownloadInfo(list)
}
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val info = mList!![position]
lifecycleScope.launchUI {
dialogState.confirmRemoveDownload(info) {
mAdapter!!.notifyItemChanged(position)
}
}
}
})
mItemTouchHelper!!.attachToRecyclerView(recyclerView)
fastScroller.attachToRecyclerView(recyclerView)
@@ -349,15 +347,23 @@ class DownloadsScene :
fabLayout.setAutoCancel(false)
fabLayout.setOnClickFabListener(this@DownloadsScene)
addAboveSnackView(fabLayout)
updateView()
updateForLabel()
}
viewLifecycleOwner.lifecycleScope.launch {
DownloadManager.stateFlow().collectLatest {
updateInfoList()
}
}
setOnApplySearch {
mKeyword = it.takeUnless { it.isEmpty() }
updateInfoList()
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateTitle()
setNavigationIcon(R.drawable.ic_baseline_menu_24)
override fun onSearchViewExpanded() {
hideActionFab(true)
super.onSearchViewExpanded()
}
override fun onDestroyView() {
@@ -393,8 +399,7 @@ class DownloadsScene :
mType + 1,
) { dialog: DialogInterface, which: Int ->
mType = which - 1
updateForLabel()
updateView()
updateInfoList()
dialog.dismiss()
}
.show()
@@ -447,7 +452,7 @@ class DownloadsScene :
}
}
fun updateView() {
private fun updateView() {
if (mViewTransition != null) {
if (mList.isNullOrEmpty()) {
mViewTransition!!.showView(1)
@@ -595,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) :
RecyclerView.ViewHolder(binding.root)
@@ -660,17 +610,14 @@ class DownloadsScene :
DownloadLabelHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
holder.itemView.setOnClickListener {
val position = holder.bindingAdapterPosition
val label1: String? = if (position == 0) {
null
} else {
mLabels[position]
}
if (mLabel != label1) {
mLabel = label1
val label = mLabels[position].takeUnless { position == 0 }
if (mLabel != label) {
mKeyword = null
setSearchBarText(null)
mLabel = label
updateForLabel()
updateView()
closeSideSheet()
}
closeSideSheet()
}
holder.binding.edit.setOnClickListener {
val context = context
@@ -708,7 +655,7 @@ class DownloadsScene :
}
holder.binding.run {
if (list != null) {
text.text = label + " [" + list.size + "]"
text.text = "$label [${list.size}]"
} else {
text.text = label
}
@@ -863,6 +810,7 @@ class DownloadsScene :
Text(
text = categoryText,
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,
style = MaterialTheme.typography.labelLarge,
)
@@ -952,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
var tracker: GallerySelectionTracker<DownloadInfo>? = null
override fun getItemId(position: Int): Long {
return if (mList == null || position < 0 || position >= mList!!.size) {
0
} else {
mList!![position].gid
}
}
override fun getItemId(position: Int) = getItem(position).gid
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
val holder = DownloadHolder(ItemDownloadBinding.inflate(mInflater, parent, false))
@@ -971,10 +925,8 @@ class DownloadsScene :
}
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
mList?.let {
val info = it[position]
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
}
val info = getItem(position)
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
}
override fun onBindViewHolder(
@@ -984,18 +936,15 @@ class DownloadsScene :
) {
payloads.forEach { payload ->
when (payload) {
PAYLOAD_STATE -> {
mList?.let { holder.bindForState(it[position]) }
PAYLOAD_DOWNLOAD_INFO -> {
val info = getItem(position)
holder.bindForState(info)
return
}
}
}
super.onBindViewHolder(holder, position, payloads)
}
override fun getItemCount(): Int {
return if (mList == null) 0 else mList!!.size
}
}
private inner class RenameLabelDialogHelper(
@@ -1030,7 +979,6 @@ class DownloadsScene :
if (mLabel == mOriginalLabel) {
mLabel = text
updateForLabel()
updateView()
}
}
}
@@ -1119,7 +1067,6 @@ class DownloadsScene :
withUIContext {
mLabel = null
updateForLabel()
updateView()
mLabelAdapter!!.notifyItemRemoved(position)
}
}
@@ -1137,7 +1084,7 @@ class DownloadsScene :
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
private const val KEY_LABEL = "label"
private const val LABEL_OFFSET = 2
private const val PAYLOAD_STATE = 0
private const val PAYLOAD_DOWNLOAD_INFO = 0
}
}
@@ -51,6 +51,8 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.CalendarConstraints.DateValidator
import com.google.android.material.datepicker.CompositeDateValidator
@@ -78,17 +80,18 @@ import com.hippo.ehviewer.ui.legacy.HandlerDrawable
import com.hippo.ehviewer.ui.legacy.ViewTransition
import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.SimpleHandler
import com.hippo.ehviewer.util.getValue
import com.hippo.ehviewer.util.lazyMut
import com.hippo.ehviewer.util.setValue
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import rikka.core.res.resolveColor
import java.time.Instant
@@ -160,6 +163,10 @@ class FavoritesScene : SearchBarScene() {
}
}
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
private fun onItemClick(position: Int) {
// Skip if in search mode
if (!tracker.isInCustomChoice) {
@@ -191,10 +198,11 @@ class FavoritesScene : SearchBarScene() {
}
if (keyword.isNullOrEmpty()) {
setSearchBarHint(getString(R.string.favorites_title, favCatName))
setSearchBarText(null)
} else {
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
}
@@ -222,6 +230,7 @@ class FavoritesScene : SearchBarScene() {
savedInstanceState: Bundle?,
): View {
_binding = SceneFavoritesBinding.inflate(inflater, container!!)
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
setOnApplySearch {
if (!tracker.isInCustomChoice) {
switchFav(urlBuilder.favCat, it)
@@ -327,8 +336,8 @@ class FavoritesScene : SearchBarScene() {
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
adapter.addLoadStateListener {
lifecycleScope.launchUI {
viewLifecycleOwner.lifecycleScope.launch {
adapter.loadStateFlow.collectLatest {
when (val state = it.refresh) {
is LoadState.Loading -> {
showSearchBar()
@@ -381,7 +390,40 @@ class FavoritesScene : SearchBarScene() {
}
switchFav(Settings.recentFavCat)
}
allowEmptySearch = false
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return if (tracker.isInCustomChoice) {
0
} else {
makeMovementFlags(0, ItemTouchHelper.LEFT)
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
) = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val info = mAdapter!!.peek(position)!!
lifecycleScope.launchIO {
dialogState.awaitPermissionOrCancel(
confirmText = R.string.delete,
dismissText = android.R.string.cancel,
title = R.string.delete_favorites_dialog_title,
onDismiss = { mAdapter!!.notifyItemChanged(position) },
) {
Text(text = stringResource(id = R.string.delete_favorites_dialog_message, 1))
}
if (urlBuilder.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
EhDB.removeLocalFavorites(info)
} else {
EhEngine.modifyFavorites(info.gid, info.token)
}
}
}
})
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
return binding.root
}
@@ -439,6 +481,16 @@ class FavoritesScene : SearchBarScene() {
}
}
override fun onSearchViewExpanded() {
hideActionFab(true)
super.onSearchViewExpanded()
}
override fun onSearchViewHidden() {
super.onSearchViewHidden()
showActionFab(true)
}
private fun showGoToDialog() {
context ?: return
val local = LocalDateTime.of(2007, 3, 21, 0, 0)
@@ -528,5 +580,3 @@ class FavoritesScene : SearchBarScene() {
}
}
}
private const val ANIMATE_TIME = 300L
@@ -68,7 +68,6 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
@@ -106,6 +105,7 @@ import arrow.core.partially1
import coil.imageLoader
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -123,6 +123,7 @@ import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.exception.EhException
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.HomeParser
import com.hippo.ehviewer.client.parser.TorrentResult
@@ -159,6 +160,7 @@ import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconButton
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconToggleButton
import com.hippo.ehviewer.ui.tools.GalleryDetailRating
import com.hippo.ehviewer.util.AppHelper
import com.hippo.ehviewer.util.ExceptionUtils
@@ -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() {
val url = galleryDetailUrl ?: return
val activity: Activity = mainActivity ?: return
@@ -466,6 +486,13 @@ class GalleryDetailScene : BaseScene() {
actionRefresh()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.clear_image_cache)) },
onClick = {
dropdown = false
actionClearCache()
},
)
DropdownMenuItem(
text = { Text(text = stringResource(id = R.string.open_in_other_app)) },
onClick = {
@@ -712,7 +739,7 @@ class GalleryDetailScene : BaseScene() {
stringResource(id = R.string.not_favorited)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
FilledTonalIconToggleButton(
FilledTertiaryIconToggleButton(
checked = favored,
onCheckedChange = { modifyFavourite() },
) {
@@ -15,7 +15,6 @@
*/
package com.hippo.ehviewer.ui.scene
import android.animation.Animator
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
@@ -66,10 +65,12 @@ import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_IMAGE_SEARCH
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_NORMAL
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_SUBSCRIPTION
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TAG
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_UPLOADER
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT
import com.hippo.ehviewer.client.exception.CloudflareBypassException
import com.hippo.ehviewer.client.exception.EhException
@@ -98,13 +99,11 @@ import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.updater.AppUpdater
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.hippo.ehviewer.util.getParcelableCompat
import com.hippo.ehviewer.util.getValue
import com.hippo.ehviewer.util.lazyMut
import com.hippo.ehviewer.util.setValue
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.withUIContext
import kotlinx.coroutines.delay
@@ -130,10 +129,10 @@ class VMStorage1 : ViewModel() {
if (urlBuilder.mode == MODE_TOPLIST) {
// TODO: Since we know total pages, let pager support jump
val key = (params.key ?: urlBuilder.mJumpTo ?: "0").toInt()
val prev = if (key != 0) key - 1 else null
val next = if (key != 199) key + 1 else null
val prev = (key - 1).takeIf { it > 0 }
val next = (key + 1).takeIf { it < TOPLIST_PAGES }
runSuspendCatching {
urlBuilder.setJumpTo(key.toString())
urlBuilder.setJumpTo(key)
EhEngine.getGalleryList(urlBuilder.build())
}.onFailure {
return@withIOContext LoadResult.Error(it)
@@ -156,7 +155,7 @@ class VMStorage1 : ViewModel() {
}
}
val r = runSuspendCatching {
if (ListUrlBuilder.MODE_IMAGE_SEARCH == urlBuilder.mode) {
if (MODE_IMAGE_SEARCH == urlBuilder.mode) {
EhEngine.imageSearch(
File(urlBuilder.imagePath!!),
urlBuilder.isUseSimilarityScan,
@@ -192,23 +191,15 @@ class GalleryListScene : SearchBarScene() {
}
private var _binding: SceneGalleryListBinding? = null
private val binding get() = _binding!!
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mSearchFab.visibility = View.INVISIBLE
}
}
private val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
binding.fabLayout.primaryFab?.visibility = View.INVISIBLE
}
}
private var fabAnimator: ViewPropertyAnimator? = null
private var mViewTransition: ViewTransition? = null
private var mAdapter: GalleryAdapter? = null
lateinit var mQuickSearchList: MutableList<QuickSearch>
private var mHideActionFabSlop = 0
private var mShowActionFab = true
private var mState = State.NORMAL
override val fabLayout get() = binding.fabLayout
override fun getMenuResId(): Int {
return R.menu.scene_gallery_list_searchbar_menu
}
@@ -223,7 +214,6 @@ class GalleryListScene : SearchBarScene() {
ACTION_LIST_URL_BUILDER -> args?.getParcelableCompat<ListUrlBuilder>(KEY_LIST_URL_BUILDER)?.copy() ?: ListUrlBuilder()
else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!")
}
args?.getString(KEY_GOTO)?.let { mUrlBuilder.mNext = it }
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -266,10 +256,7 @@ class GalleryListScene : SearchBarScene() {
val category = mUrlBuilder.category
val mode = mUrlBuilder.mode
val isPopular = mode == MODE_WHATS_HOT
val isTopList = mode == MODE_TOPLIST
if (isTopList != mIsTopList) {
mIsTopList = isTopList
}
mIsTopList = mode == MODE_TOPLIST
// Update fab visibility
binding.fabLayout.setSecondaryFabVisibilityAt(0, !isPopular)
@@ -295,6 +282,14 @@ class GalleryListScene : SearchBarScene() {
title = resources.getString(R.string.search)
}
setSearchBarHint(title)
when (mode) {
MODE_NORMAL -> if (category != EhUtils.NONE || !keyword.isNullOrEmpty()) {
mainActivity?.clearNavCheckedItem()
}
MODE_TAG, MODE_UPLOADER, MODE_IMAGE_SEARCH -> mainActivity?.clearNavCheckedItem()
else -> Unit
}
}
private val dialogState = DialogState()
@@ -368,9 +363,8 @@ class GalleryListScene : SearchBarScene() {
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
val noWatch = getString(R.string.gallery_list_empty_hit_subscription)
adapter.addLoadStateListener {
lifecycleScope.launchUI {
launch {
adapter.loadStateFlow.collectLatest {
when (val state = it.refresh) {
is LoadState.Loading -> {
showSearchBar()
@@ -397,11 +391,7 @@ class GalleryListScene : SearchBarScene() {
delay(500)
binding.refreshLayout.isRefreshing = false
if (mAdapter?.itemCount == 0) {
if (mUrlBuilder.mode == MODE_SUBSCRIPTION) {
binding.tip.text = noWatch
} else {
binding.tip.text = empty
}
binding.tip.text = empty
transition.showView(2)
} else {
transition.showView(0)
@@ -452,12 +442,14 @@ class GalleryListScene : SearchBarScene() {
override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {
when (position) {
0 -> showGoToDialog()
1 -> {
1 -> { // First
mUrlBuilder.setIndex(null, true)
mUrlBuilder.mJumpTo = null
mAdapter?.refresh()
}
2 -> {
2 -> { // Last
if (mIsTopList) {
mUrlBuilder.mJumpTo = "${TOPLIST_PAGES - 1}"
mAdapter?.refresh()
} else {
mUrlBuilder.setIndex("1", false)
@@ -481,7 +473,6 @@ class GalleryListScene : SearchBarScene() {
}
binding.fabLayout.primaryFab!!.setImageDrawable(actionFabDrawable)
addAboveSnackView(binding.fabLayout)
mSearchFab.setOnClickListener { onApplySearch() }
// Update list url builder
onUpdateUrlBuilder()
@@ -520,7 +511,7 @@ class GalleryListScene : SearchBarScene() {
val context = context ?: return
// Can't add image search as quick search
if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.mode) {
if (MODE_IMAGE_SEARCH == mUrlBuilder.mode) {
showTip(R.string.image_search_not_quick_search, LENGTH_LONG)
return
}
@@ -644,9 +635,9 @@ class GalleryListScene : SearchBarScene() {
private fun showGoToDialog() {
mAdapter ?: return
if (mIsTopList) {
val pages = 200
val page = mUrlBuilder.mJumpTo?.toIntOrNull() ?: 0
val title = getString(R.string.go_to)
val hint = getString(R.string.go_to_hint, 1, 200)
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
lifecycleScope.launch {
val text = dialogState.awaitInputText(
title = title,
@@ -659,13 +650,13 @@ class GalleryListScene : SearchBarScene() {
}.onFailure {
return@awaitInputText getString(R.string.error_invalid_number)
}.getOrThrow()
if (goTo < 0 || goTo >= pages) {
if (goTo !in 0..<TOPLIST_PAGES) {
getString(R.string.error_out_of_range)
} else {
null
}
}.trim().toInt() - 1
mUrlBuilder.setJumpTo(text.toString())
mUrlBuilder.setJumpTo(text)
mAdapter?.refresh()
}
} else {
@@ -696,13 +687,21 @@ class GalleryListScene : SearchBarScene() {
}
override fun onSearchViewExpanded() {
if (mState == State.NORMAL) {
hideActionFab(true)
} else {
hideSearchFab(true)
}
super.onSearchViewExpanded()
if (mState == State.NORMAL) selectSearchFab(true)
}
override fun onSearchViewHidden() {
super.onSearchViewHidden()
if (mState == State.NORMAL) selectActionFab(true)
if (mState == State.NORMAL) {
showActionFab(true)
} else {
showSearchFab(true)
}
}
private fun showActionFab() {
@@ -738,70 +737,6 @@ class GalleryListScene : SearchBarScene() {
}
}
private fun selectSearchFab(animation: Boolean) {
_binding ?: return
mShowActionFab = false
if (animation) {
val fab: View? = binding.fabLayout.primaryFab
val delay: Long
if (View.INVISIBLE == fab!!.visibility) {
delay = 0L
} else {
delay = ANIMATE_TIME
binding.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.rotation = -45.0f
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
binding.fabLayout.setExpanded(expanded = false, animation = false)
val fab: View? = binding.fabLayout.primaryFab
fab!!.visibility = View.INVISIBLE
fab.scaleX = 0.0f
fab.scaleY = 0.0f
mSearchFab.visibility = View.VISIBLE
mSearchFab.scaleX = 1.0f
mSearchFab.scaleY = 1.0f
}
}
private fun selectActionFab(animation: Boolean) {
_binding ?: return
mShowActionFab = true
if (animation) {
val delay: Long
if (View.INVISIBLE == mSearchFab.visibility) {
delay = 0L
} else {
delay = ANIMATE_TIME
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
.setListener(mSearchFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
}
val fab: View? = binding.fabLayout.primaryFab
fab!!.visibility = View.VISIBLE
fab.rotation = -45.0f
fab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
binding.fabLayout.setExpanded(expanded = false, animation = false)
val fab: View? = binding.fabLayout.primaryFab
fab!!.visibility = View.VISIBLE
fab.scaleX = 1.0f
fab.scaleY = 1.0f
mSearchFab.visibility = View.INVISIBLE
mSearchFab.scaleX = 0.0f
mSearchFab.scaleY = 0.0f
}
}
private fun setState(state: State) {
setState(state, true)
}
@@ -863,24 +798,35 @@ class GalleryListScene : SearchBarScene() {
private fun onApplySearch(query: String?) {
_binding ?: return
lifecycleScope.launchIO {
val builder = ListUrlBuilder()
val oldMode = mUrlBuilder.mode
if (mState == State.SEARCH || mState == State.SEARCH_SHOW_LIST) {
try {
binding.searchLayout.formatListUrlBuilder(mUrlBuilder, query)
binding.searchLayout.formatListUrlBuilder(builder, query)
} catch (e: EhException) {
showTip(e.message, LENGTH_LONG)
return@launchIO
}
} else {
val oldMode = mUrlBuilder.mode
// If it's MODE_SUBSCRIPTION, keep it
val newMode = if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else MODE_NORMAL
mUrlBuilder.reset()
mUrlBuilder.mode = newMode
mUrlBuilder.keyword = query
builder.mode = newMode
builder.keyword = query
}
withUIContext {
onUpdateUrlBuilder()
mAdapter?.refresh()
when (oldMode) {
MODE_TOPLIST, MODE_WHATS_HOT -> {
// Wait for search view to hide
delay(300)
navAnimated(R.id.galleryListScene, builder.toStartArgs())
}
else -> {
mUrlBuilder = builder
onUpdateUrlBuilder()
mAdapter?.refresh()
}
}
setState(State.NORMAL)
}
}
@@ -899,26 +845,23 @@ class GalleryListScene : SearchBarScene() {
private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) :
RecyclerView.Adapter<QsDrawerHolder>() {
private val keywords = intArrayOf(11, 12, 13, 15)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {
val holder = QsDrawerHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
if (!mIsTopList) {
holder.itemView.setOnClickListener {
holder.itemView.setOnClickListener {
if (!mIsTopList) {
val q = mQuickSearchList[holder.bindingAdapterPosition]
mUrlBuilder.set(q)
onUpdateUrlBuilder()
mAdapter?.refresh()
setState(State.NORMAL)
closeSideSheet()
}
} else {
val keywords = intArrayOf(11, 12, 13, 15)
holder.itemView.setOnClickListener {
// Navigate to galleryListScene to clear nav checked item
navAnimated(R.id.galleryListScene, ListUrlBuilder().apply { set(q) }.toStartArgs())
} else {
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
mUrlBuilder.mJumpTo = null
onUpdateUrlBuilder()
mAdapter?.refresh()
setState(State.NORMAL)
closeSideSheet()
}
setState(State.NORMAL)
closeSideSheet()
}
return holder
}
@@ -1048,7 +991,6 @@ class GalleryListScene : SearchBarScene() {
companion object {
const val KEY_ACTION = "action"
const val KEY_GOTO = "goto"
const val ACTION_HOMEPAGE = "action_homepage"
const val ACTION_SUBSCRIPTION = "action_subscription"
const val ACTION_WHATS_HOT = "action_whats_hot"
@@ -1056,7 +998,6 @@ class GalleryListScene : SearchBarScene() {
const val ACTION_LIST_URL_BUILDER = "action_list_url_builder"
const val KEY_LIST_URL_BUILDER = "list_url_builder"
const val KEY_STATE = "state"
private const val ANIMATE_TIME = 300L
fun ListUrlBuilder.toStartArgs() = bundleOf(
KEY_ACTION to ACTION_LIST_URL_BUILDER,
KEY_LIST_URL_BUILDER to this,
@@ -1064,6 +1005,8 @@ class GalleryListScene : SearchBarScene() {
}
}
private const val TOPLIST_PAGES = 200
@Parcelize
enum class State : Parcelable {
NORMAL, SIMPLE_SEARCH, SEARCH, SEARCH_SHOW_LIST
@@ -1,5 +1,6 @@
package com.hippo.ehviewer.ui.scene
import android.animation.Animator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
@@ -40,7 +41,10 @@ import com.hippo.ehviewer.dao.Search
import com.hippo.ehviewer.dao.SearchDao
import com.hippo.ehviewer.databinding.SceneSearchbarBinding
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.FabLayout
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.jamal.composeprefs3.ui.ifNotNullThen
import com.jamal.composeprefs3.ui.ifTrueThen
import eu.kanade.tachiyomi.util.lang.launchIO
@@ -57,6 +61,18 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
private val mSearchDatabase = searchDatabase.searchDao()
private var onApplySearch: (String) -> Unit = {}
protected val mSearchFab get() = binding.searchFab
protected abstract val fabLayout: FabLayout
protected var mShowActionFab = true
protected val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
fabLayout.primaryFab?.visibility = View.INVISIBLE
}
}
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mSearchFab.visibility = View.INVISIBLE
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = SceneSearchbarBinding.inflate(inflater, container, false)
@@ -99,6 +115,7 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
onSearchViewHidden()
}
}
mSearchFab.setOnClickListener { onApplySearch() }
onCreateViewWithToolbar(inflater, binding.root, savedInstanceState)
// This has to be placed after onCreateViewWithToolbar() since
// callbacks are invoked in the reverse order in which they are added
@@ -147,6 +164,87 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
privLockModeStart = null
}
fun showSearchFab(animation: Boolean, delay: Long = 0) {
if (animation) {
mSearchFab.visibility = View.VISIBLE
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
mSearchFab.visibility = View.VISIBLE
mSearchFab.scaleX = 1.0f
mSearchFab.scaleY = 1.0f
}
}
fun hideSearchFab(animation: Boolean): Long {
return if (View.INVISIBLE == mSearchFab.visibility) {
0L
} else {
if (animation) {
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
.setListener(mSearchFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.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!!
fab.visibility = View.VISIBLE
fab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
fabLayout.setExpanded(expanded = false, animation = false)
val fab = fabLayout.primaryFab!!
fab.visibility = View.VISIBLE
fab.scaleX = 1.0f
fab.scaleY = 1.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?) {
binding.toolbar.hint = hint
}
@@ -329,3 +427,5 @@ fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
}
}
}
const val ANIMATE_TIME = 300L
@@ -112,7 +112,7 @@ fun AboutScreen() {
title = stringResource(id = R.string.use_ci_update_channel),
value = Settings::useCIUpdateChannel,
)
SimpleMenuPreferenceInt2(
SimpleMenuPreferenceInt(
title = stringResource(id = R.string.auto_updates),
entry = R.array.update_frequency,
entryValueRes = R.array.update_frequency_values,
@@ -126,11 +126,6 @@ fun AdvancedScreen() {
entryValueRes = R.array.app_language_entry_values,
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(
title = stringResource(id = R.string.preload_thumb_aggressively),
value = Settings::preloadThumbAggressively,
@@ -193,6 +193,27 @@ fun EhScreen() {
summary = stringResource(id = R.string.settings_my_tags_summary),
) { navController.navigate(MYTAGS_SCREEN) }
}
var defaultFavSlot by Settings::defaultFavSlot.observed
val disabled = stringResource(id = R.string.disabled_nav)
val localFav = stringResource(id = R.string.local_favorites)
val summary = when (defaultFavSlot) {
-1 -> localFav
in 0..9 -> Settings.favCat[defaultFavSlot]
else -> stringResource(id = R.string.default_favorites_warning)
}
val items = if (signin) {
arrayOf(disabled, localFav, *Settings.favCat)
} else {
arrayOf(disabled, localFav)
}
Preference(
title = stringResource(id = R.string.default_favorites_collection),
summary = summary,
) {
coroutineScope.launch {
defaultFavSlot = dialogState.showSelectItem(*items, title = R.string.default_favorites_collection) - 2
}
}
SimpleMenuPreferenceInt(
title = stringResource(id = rikka.core.R.string.dark_theme),
entry = R.array.night_mode_entries,
@@ -203,6 +224,10 @@ fun EhScreen() {
title = stringResource(id = R.string.black_dark_theme),
value = Settings::blackDarkTheme,
)
SwitchPreference(
title = stringResource(id = R.string.harmonize_category_color),
value = Settings::harmonizeCategoryColor,
)
SimpleMenuPreferenceInt(
title = stringResource(id = R.string.settings_eh_launch_page),
entry = R.array.launch_page_entries,
@@ -244,27 +269,6 @@ fun EhScreen() {
entryValueRes = R.array.thumb_resolution_entry_values,
value = thumbResolution,
)
var defaultFavSlot by Settings::defaultFavSlot.observed
val disabled = stringResource(id = R.string.disabled_nav)
val localFav = stringResource(id = R.string.local_favorites)
val summary = when (defaultFavSlot) {
-1 -> localFav
in 0..9 -> Settings.favCat[defaultFavSlot]
else -> stringResource(id = R.string.default_favorites_warning)
}
val items = if (signin) {
arrayOf(disabled, localFav, *Settings.favCat)
} else {
arrayOf(disabled, localFav)
}
Preference(
title = stringResource(id = R.string.default_favorites_collection),
summary = summary,
) {
coroutineScope.launch {
defaultFavSlot = dialogState.showSelectItem(*items, title = R.string.default_favorites_collection) - 2
}
}
SwitchPreference(
title = stringResource(id = R.string.settings_eh_force_eh_thumb),
summary = stringResource(id = R.string.settings_eh_force_eh_thumb_summary),
@@ -114,20 +114,6 @@ fun HtmlPreference(title: String, summary: AnnotatedString? = null, onClick: ()
@Composable
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 valuesArray = integerArrayResource(id = entryValueRes)
val map = remember {
@@ -3,7 +3,6 @@ package com.hippo.ehviewer.ui.tools
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.Dp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.roundToInt
@@ -15,7 +14,6 @@ private const val QuarterCircle = PI / 2
@Composable
fun CircularLayout(
modifier: Modifier = Modifier,
radius: Dp,
placeFirstItemInCenter: Boolean = false,
content: @Composable () -> Unit,
) {
@@ -23,7 +21,7 @@ fun CircularLayout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val radiusPx = radius.toPx()
val radiusPx = constraints.maxWidth * 0.4
val itemConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
val outPlaceables = if (placeFirstItemInCenter) placeables.drop(1) else placeables
@@ -3,12 +3,10 @@ package com.hippo.ehviewer.ui.tools
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@@ -43,11 +41,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
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.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.jamal.composeprefs3.ui.ifNotNullThen
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -148,18 +146,25 @@ class DialogState {
@StringRes confirmText: Int? = null,
@StringRes dismissText: Int? = null,
@StringRes title: Int? = null,
onDismiss: () -> Unit = {},
text: (@Composable () -> Unit)? = null,
) {
return dialog { cont ->
AlertDialog(
onDismissRequest = { cont.cancel() },
onDismissRequest = {
onDismiss()
cont.cancel()
},
confirmButton = {
TextButton(onClick = { cont.resume(Unit) }) {
Text(text = stringResource(id = confirmText ?: android.R.string.ok))
}
},
dismissButton = dismissText.ifNotNullThen {
TextButton(onClick = { cont.cancel() }) {
TextButton(onClick = {
onDismiss()
cont.cancel()
}) {
Text(text = stringResource(id = dismissText!!))
}
},
@@ -178,6 +183,7 @@ class DialogState {
}
AlertDialog(
onDismissRequest = { cont.cancel() },
properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth),
content = {
Surface(
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
@@ -195,12 +201,22 @@ class DialogState {
vararg items: String,
@StringRes title: Int,
): Int = showNoButton {
Column(modifier = Modifier.padding(24.dp)) {
Text(text = stringResource(id = title), style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.size(16.dp))
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
items.forEachIndexed { index, text ->
Text(text = text, modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth().padding(vertical = 8.dp), style = MaterialTheme.typography.titleMedium)
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(id = title),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
style = MaterialTheme.typography.headlineSmall,
)
LazyColumn {
itemsIndexed(items) { index, text ->
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
Text(
text = text,
modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
style = MaterialTheme.typography.titleMedium,
)
}
}
}
}
@@ -233,12 +249,10 @@ class DialogState {
@StringRes title: Int,
@StringRes hint: Int,
maxChar: Int,
adjustTextPosition: Boolean = true,
): Pair<Int, String> = showNoButton(false) {
Column {
Text(text = stringResource(id = title), modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium)
CircularLayout(
radius = 112.dp,
modifier = Modifier.fillMaxWidth().aspectRatio(1F),
placeFirstItemInCenter = true,
) {
@@ -246,8 +260,7 @@ class DialogState {
TextField(
value = note,
onValueChange = { note = it },
modifier = Modifier.width(128.dp),
textStyle = TextStyle(fontFamily = FontFamily.Monospace),
modifier = Modifier.fillMaxWidth(0.45F).aspectRatio(1F),
label = { Text(text = stringResource(id = hint)) },
trailingIcon = {
if (note.isNotEmpty()) {
@@ -263,7 +276,6 @@ class DialogState {
textAlign = TextAlign.End,
)
},
maxLines = 6,
shape = ShapeDefaults.ExtraSmall,
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
@@ -272,16 +284,18 @@ class DialogState {
)
items.forEachIndexed { index, (icon, text) ->
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,
) {
if (index == 0 && adjustTextPosition) {
Text(text = text, style = MaterialTheme.typography.bodySmall)
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
} else {
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
Text(text = text, style = MaterialTheme.typography.bodySmall)
}
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
Text(
text = text,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
style = MaterialTheme.typography.bodySmall,
)
}
}
}
@@ -60,12 +60,12 @@ object AppConfig {
return FileUtils.createTempFile(tempDir, null)
}
fun saveParseErrorBody(e: ParseException) {
fun saveParseErrorBody(e: ParseException, body: String) {
val dir = externalParseErrorDir ?: return
File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt").writer().use {
it.write(e.message)
it.appendLine()
it.append(e.body)
it.append(body)
}
}
@@ -1,6 +1,8 @@
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
get() = Settings.enableQuic && isAtLeastQ
get() = isAtLeastSExtension7
@@ -1,5 +1,8 @@
package com.hippo.ehviewer.util
import android.os.Build
import android.os.ext.SdkExtensions
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 {
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 {
detector.onTouchEvent(e)
if (scrollState == SCROLL_STATE_IDLE) {
detector.onTouchEvent(e)
}
return super.onTouchEvent(e)
}
+7 -10
View File
@@ -17,29 +17,26 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
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
android:id="@+id/action_open_side_sheet"
android:icon="@drawable/ic_outline_label_24"
android:title="@string/download_labels"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_filter"
android:title="@string/download_filter"
app:showAsAction="never" />
<item
android:id="@+id/action_start_all"
android:icon="@drawable/v_play_x24"
android:title="@string/download_start_all"
app:showAsAction="ifRoom" />
app:showAsAction="never" />
<item
android:id="@+id/action_stop_all"
android:icon="@drawable/v_pause_x24"
android:title="@string/download_stop_all"
app:showAsAction="ifRoom" />
app:showAsAction="never" />
<item
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="cloud_favorites">Cloud-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_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Aus Favoriten löschen</string>
+1 -1
View File
@@ -354,7 +354,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Favoritos en la nube</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_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Eliminar de favoritos</string>
+1 -1
View File
@@ -393,7 +393,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Favoris de cloud</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_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">Effacer de favoris</string>
+1 -1
View File
@@ -390,7 +390,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Cloudのお気に入り</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_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">お気に入りから削除</string>
+1 -1
View File
@@ -364,7 +364,7 @@
<!-- Favorites -->
<string name="cloud_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_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">즐겨찾기에서 삭제</string>
+1 -1
View File
@@ -379,7 +379,7 @@
<!-- Favorites -->
<string name="cloud_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_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">ลบออกจากรายการโปรด</string>
+6 -4
View File
@@ -149,6 +149,9 @@
<string name="refresh">刷新</string>
<string name="refresh_original">刷新(原图)</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_failed">评分失败</string>
<string name="no_torrents">没有种子</string>
@@ -376,8 +379,6 @@
<string name="settings_advanced_dump_logcat_to">已保存日志至 %s</string>
<string name="settings_advanced_read_cache_size">阅读缓存大小</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_summary">保存数据至外置存储器,例如下载列表,快速搜索列表</string>
<string name="settings_advanced_export_data_to">已导出数据至 %s</string>
@@ -409,7 +410,7 @@
<!-- Favorites -->
<string name="cloud_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_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">删除收藏</string>
@@ -418,7 +419,7 @@
<string name="default_favorites_collection">默认收藏夹</string>
<string name="default_favorites_warning">启用此项将无法添加收藏备注</string>
<string name="let_me_select">让我选择</string>
<string name="favorite_note">备注</string>
<string name="favorite_note">收藏备注</string>
<string name="collections">收藏夹</string>
<string name="need_sign_in">需要登录</string>
<!-- Progress Scene -->
@@ -475,6 +476,7 @@
<string name="download_service">下载服务</string>
<string name="favorite_name">收藏画廊</string>
<string name="black_dark_theme">纯黑深色主题</string>
<string name="harmonize_category_color">类别颜色适配动态配色</string>
<string name="use_ci_update_channel">使用持续集成(CI)构建频道更新</string>
<string name="download_filter">过滤</string>
<string name="download_all">全部</string>
+1 -1
View File
@@ -419,7 +419,7 @@
<!-- Favorites -->
<string name="cloud_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_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">刪除收藏</string>
+1 -1
View File
@@ -400,7 +400,7 @@
<!-- Favorites -->
<string name="cloud_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_2">%1$s - %2$s</string>
<string name="delete_favorites_dialog_title">移除收藏</string>
+20 -20
View File
@@ -21,20 +21,20 @@
<item>@string/settings_eh_list_mode_thumb</item>
</string-array>
<string-array name="list_mode_entry_values" translatable="false">
<integer-array name="list_mode_entry_values" translatable="false">
<item>0</item>
<item>1</item>
</string-array>
</integer-array>
<string-array name="gallery_site_entries" translatable="false">
<item>@string/site_e</item>
<item>@string/site_ex</item>
</string-array>
<string-array name="gallery_site_entry_values" translatable="false">
<integer-array name="gallery_site_entry_values" translatable="false">
<item>0</item>
<item>1</item>
</string-array>
</integer-array>
<string-array name="launch_page_entries" translatable="false">
<item>@string/homepage</item>
@@ -43,22 +43,22 @@
<item>@string/toplist</item>
</string-array>
<string-array name="launch_page_entry_values" translatable="false">
<integer-array name="launch_page_entry_values" translatable="false">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
</integer-array>
<string-array name="detail_size_entries" translatable="false">
<item>@string/settings_eh_detail_size_long</item>
<item>@string/settings_eh_detail_size_short</item>
</string-array>
<string-array name="detail_size_entry_values" translatable="false">
<integer-array name="detail_size_entry_values" translatable="false">
<item>0</item>
<item>1</item>
</string-array>
</integer-array>
<string-array name="thumb_resolution_entries" translatable="false">
<item>@string/settings_eh_thumb_resolution_auto</item>
@@ -66,11 +66,11 @@
<item>300x</item>
</string-array>
<string-array name="thumb_resolution_entry_values" translatable="false">
<integer-array name="thumb_resolution_entry_values" translatable="false">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
</integer-array>
<string-array name="search_min_rating" translatable="false">
<item>@string/unlimited</item>
@@ -87,12 +87,12 @@
<item>7</item>
</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>3</item>
<item>5</item>
<item>7</item>
</string-array>
</integer-array>
<string-array name="download_delay_entries" translatable="false">
<item>0</item>
@@ -107,7 +107,7 @@
<item>20000</item>
</string-array>
<string-array name="download_delay_entry_values" translatable="false">
<integer-array name="download_delay_entry_values" translatable="false">
<item>0</item>
<item>500</item>
<item>1000</item>
@@ -118,7 +118,7 @@
<item>5000</item>
<item>10000</item>
<item>20000</item>
</string-array>
</integer-array>
<string-array name="preload_image_entries" translatable="false">
<item>3</item>
@@ -129,14 +129,14 @@
<item>17</item>
</string-array>
<string-array name="preload_image_entry_values" translatable="false">
<integer-array name="preload_image_entry_values" translatable="false">
<item>3</item>
<item>5</item>
<item>7</item>
<item>11</item>
<item>13</item>
<item>17</item>
</string-array>
</integer-array>
<string-array name="read_cache_size_entries" translatable="false">
<item>320 MB</item>
@@ -146,13 +146,13 @@
<item>5120 MB</item>
</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>640</item>
<item>1280</item>
<item>2560</item>
<item>5120</item>
</string-array>
</integer-array>
<string-array name="filter_entries">
<item>@string/filter_title</item>
@@ -208,11 +208,11 @@
<item>@string/dark_theme_on</item>
</string-array>
<string-array name="night_mode_values">
<integer-array name="night_mode_values">
<item>-1</item>
<item>1</item>
<item>2</item>
</string-array>
</integer-array>
<string-array name="download_state">
<item>@string/download_all</item>
+6 -4
View File
@@ -191,6 +191,9 @@
<string name="refresh">Refresh</string>
<string name="refresh_original">Refresh (Original image)</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_failed">Rate failed</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_read_cache_size">Read cache size</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_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>
@@ -467,7 +468,7 @@
<!-- Favorites -->
<string name="cloud_favorites">Cloud 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_2">%1$s - %2$s</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_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="favorite_note">Note</string>
<string name="favorite_note">Favorite Note</string>
<string name="collections">Collections</string>
<string name="need_sign_in">Signing in is required</string>
<!-- Progress Scene -->
@@ -540,6 +541,7 @@
<string name="download_service">Download Service</string>
<string name="favorite_name">Favorite</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_all">All</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"
catch_panic = { path = "catch_panic" }
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" }
tl = "0.7"
log = "0.4"
@@ -19,4 +19,4 @@ proc-macro = true
[dependencies]
proc-macro2 = "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 quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::{
parse_macro_input, AttributeArgs, Expr, FnArg, ItemFn, Lit, Meta, MetaNameValue, NestedMeta,
Pat, Path,
};
use syn::{parse_macro_input, Expr, FnArg, ItemFn, LitStr, Pat, Path};
const ONLY_FUNCTIONS_MSG: &str = "#[catch_panic] can only be applied to functions";
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);
// parse macro arguments
let args = parse_macro_input!(attr as AttributeArgs);
let mut default_value = None;
let mut handler = None;
for arg in args {
if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path,
lit: Lit::Str(str),
..
})) = arg
{
match path.get_ident() {
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),
},
_ => {}
}
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("default") {
default_value = Some(meta.value()?.parse::<LitStr>()?.parse::<Expr>()?);
Ok(())
} else if meta.path.is_ident("handler") {
handler = Some(meta.value()?.parse::<LitStr>()?.parse::<Path>()?);
Ok(())
} else {
Err(meta.error("unsupported argument"))
}
}
});
parse_macro_input!(attr with parser);
let default_value = match default_value {
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| {
Some(dom.query_selector("table")?.filter_map(|e| {
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)?;
let name = unescape(&grp[8]).ok()?;
Some(Torrent {
posted: grp[1].to_string(),
size: grp[2].to_string(),
seeds: grp[3].parse().ok()?,
peers: grp[4].parse().ok()?,
downloads: grp[5].parse().ok()?,
url: grp[7].to_string(),
name: name.to_string()
})
if html.contains("Expunged") {
None
} else {
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();
let name = unescape(&grp[8]).ok()?;
Some(Torrent {
posted: grp[1].to_string(),
size: grp[2].to_string(),
seeds: grp[3].parse().ok()?,
peers: grp[4].parse().ok()?,
downloads: grp[5].parse().ok()?,
url: grp[7].to_string(),
name: name.to_string()
})
}
}).collect::<Vec<Torrent>>())
}).unwrap().into_java(&env).forget().into_raw()
}
+5 -5
View File
@@ -1,8 +1,8 @@
plugins {
id("com.android.application") version "8.1.0" apply false
kotlin("android") version "1.9.0" apply false
kotlin("plugin.serialization") version "1.9.0" apply false
id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
id("com.android.application") version "8.1.1" apply false
kotlin("android") version "1.9.10" apply false
kotlin("plugin.serialization") version "1.9.10" 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("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
@@ -13,7 +13,7 @@ allprojects {
}
tasks.register("Delete", Delete::class) {
delete(rootProject.buildDir)
delete(rootProject.layout.buildDirectory)
}
buildscript {
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+1 -1
View File
@@ -19,5 +19,5 @@ dependencyResolutionManagement {
}
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "Ehviewer"
rootProject.name = "EhViewer"
include(":app")