Compare commits

..
11 Commits
Author SHA1 Message Date
FooIbar a6d215a0bf Release 1.8.9.4-gms
Releases / Build (push) Has been cancelled
2023-08-30 16:43:42 +08:00
FooIbar cf9441adb5 Merge branch '1.8.x.x' into 1.8.9.x-gms
# Conflicts:
#	app/build.gradle.kts
2023-08-30 16:43:28 +08:00
FooIbar 36b16d7be8 Release 1.8.9.3-gms
Releases / Build (push) Has been cancelled
2023-08-17 23:00:54 +08:00
FooIbar fab5532407 Revert "Remove embedded Cronet library"
This reverts commit 9c5eddb9
2023-08-17 23:00:08 +08:00
FooIbar 8ad4e8640f Merge branch '1.8.x.x' into 1.8.9.x-gms
# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/com/hippo/ehviewer/util/CronetUtils.kt
2023-08-17 22:59:37 +08:00
FooIbar 5b5a2a9a33 Release 1.8.9.2-gms
Releases / Build (push) Has been cancelled
2023-08-15 17:49:53 +08:00
FooIbar 7719312355 Merge branch '1.8.x.x' into 1.8.9.x-gms 2023-08-15 17:49:46 +08:00
FooIbar 48f1b0a958 Release 1.8.9.1-gms
Releases / Build (push) Has been cancelled
2023-08-14 23:18:57 +08:00
FooIbar 9abeba6b15 Merge branch '1.8.x.x' into 1.8.9.x-gms 2023-08-14 23:18:34 +08:00
FooIbar 75bec4314e Release 1.8.9.0-gms
Releases / Build (push) Has been cancelled
2023-08-11 16:03:44 +08:00
FooIbar b767c041b9 Load Cronet from Google Play services 2023-08-11 15:56:21 +08:00
85 changed files with 1289 additions and 543 deletions

No files matched your search

+7 -4
View File
@@ -6,10 +6,10 @@ body:
- type: markdown
attributes:
value: |
感谢您愿意为 EhViewer 做出贡献!
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
提交问题报告前,还请首先完成文末的自查步骤
Thanks for your contribution on EhViewer Project!
Thanks for your contribution on Ehviewer-Overhauled Project!
Please finish verify steps which list in the end first before create bug report
- type: textarea
@@ -135,11 +135,14 @@ body:
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- 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
- 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 bug report in this issue. Please make mutiply issue for mutiply bugs
required: true
- label: 您确保已使用最新 [CI 版本](https://github.com/FooIbar/EhViewer/actions)测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest [CI version](https://github.com/FooIbar/EhViewer/actions)
- label: 您确保已使用最新 CI 版本测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest CI version
required: true
+5 -1
View File
@@ -1 +1,5 @@
blank_issues_enabled: false
blank_issues_enabled: false
contact_links:
- name: 所有其他问题 / All other questions
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
about: 转到 Discussions / Turn to discussions
@@ -0,0 +1,94 @@
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
+12 -25
View File
@@ -4,9 +4,8 @@ on:
push:
branches:
- '*'
- 'renovate/**'
pull_request:
workflow_dispatch:
tags-ignore:
- "*"
jobs:
linux:
@@ -14,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
uses: actions/checkout@v3
with:
submodules: 'recursive'
@@ -24,14 +23,13 @@ jobs:
distribution: temurin
java-version: 17
- name: Install NDK
run: |
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
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
@@ -40,25 +38,14 @@ jobs:
working-directory: ./app/src/main/rust
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Setup Gradle
- name: Ktlint Check
run: ./gradlew ktlintCheck
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
gradle-home-cache-cleanup: true
- name: Spotless Check
run: ./gradlew spotlessCheck
- name: CMake Cache
uses: actions/cache@v3
with:
path: |
app/.cxx
app/src/main/cpp/nettle/nettle
key: ${{ runner.os }}-cmake-${{ github.run_id }}
restore-keys: ${{ runner.os }}-cmake-
- name: Gradle Build
run: ./gradlew assembleRelease
arguments: assembleRelease
- name: Update ARM64
uses: actions/upload-artifact@v3
+66
View File
@@ -0,0 +1,66 @@
name: Pull Request Check
on: [ pull_request ]
jobs:
linux:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Install NDK
run: |
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
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
- name: Clippy Check
working-directory: ./app/src/main/rust
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Ktlint Check
run: ./gradlew ktlintCheck
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
arguments: assembleRelease
- name: Update ARM64
uses: actions/upload-artifact@v3
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
- name: Update x86_64
uses: actions/upload-artifact@v3
with:
name: x86_64-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86_64-release.apk
- name: Update ARM32
uses: actions/upload-artifact@v3
with:
name: armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
- name: Update x86
uses: actions/upload-artifact@v3
with:
name: x86-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86-release.apk
+5 -15
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
uses: actions/checkout@v3
with:
submodules: 'recursive'
@@ -21,23 +21,13 @@ jobs:
distribution: temurin
java-version: 17
- name: Install NDK
run: |
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
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache@v3
with:
path: |
app/.cxx
app/src/main/cpp/nettle/nettle
key: ${{ runner.os }}-cmake-${{ github.run_id }}
restore-keys: ${{ runner.os }}-cmake-
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
+3
View File
@@ -0,0 +1,3 @@
[submodule "app/src/main/cpp/nettle/nettle"]
path = app/src/main/cpp/nettle/nettle
url = https://github.com/Ehviewer-Overhauled/nettle
+21 -28
View File
@@ -1,64 +1,60 @@
# Maintenance Mode
The project is in maintenance mode, will only receive bugfixes.
<p align="right">
<strong>English</strong>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-cn.md">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
简体中文
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-tw.md">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
正體中文
</a>
</p>
<h1 align="center">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200">
<br>EhViewer<br>
</h1>
<p align="center">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
<img src="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/FooIbar/EhViewer/releases">
<img src="https://img.shields.io/github/downloads/FooIbar/EhViewer/total.svg" alt="Downloads">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases">
<img src="https://img.shields.io/github/downloads/Ehviewer-Overhauled/EhViewer/total.svg" alt="Downloads">
</a>
<a href="https://github.com/FooIbar/EhViewer/releases">
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases">
<img src="https://img.shields.io/github/v/release/Ehviewer-Overhauled/ehviewer?include_prereleases" alt="Release">
</a>
<a href="https://github.com/FooIbar/EhViewer/issues">
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/issues">
<img src="https://img.shields.io/github/issues/Ehviewer-Overhauled/ehviewer" alt="Issues">
</a>
</p>
<div align="center">
<h3>
<a href="https://github.com/FooIbar/EhViewer#description">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#description">
Description
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer#download">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#download">
Download
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer#screenshot">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#screenshot">
Screenshot
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer#thanks">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#thanks">
Thanks
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer#license">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#license">
License
</a>
</h3>
@@ -73,9 +69,7 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
# Download
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
# Screenshot
@@ -105,7 +99,6 @@ Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementati
Copyright 2014-2019 Hippo Seven
Copyright 2020-2022 NekoInverter
Copyright 2022-2023 Tarsin Norbin
Copyright 2023 Foolbar
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+26 -38
View File
@@ -13,7 +13,6 @@ plugins {
id("kotlin-parcelize")
kotlin("plugin.serialization")
id("com.google.devtools.ksp")
id("com.diffplug.spotless")
id("com.mikepenz.aboutlibraries.plugin")
id("org.mozilla.rust-android-gradle.rust-android")
id("dev.shreyaspatil.compose-compiler-report-generator")
@@ -22,7 +21,7 @@ plugins {
android {
compileSdk = 34
buildToolsVersion = "34.0.0"
ndkVersion = "26.0.10792818"
ndkVersion = "26.0.10636728"
splits {
abi {
@@ -65,7 +64,7 @@ android {
minSdk = 28
targetSdk = 34
versionCode = 180043
versionName = "1.8.9.7"
versionName = "1.8.9.4-gms"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -83,6 +82,7 @@ android {
),
)
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
}
@@ -146,11 +146,9 @@ android {
isShrinkResources = true
proguardFiles("proguard-rules.pro")
signingConfig = signConfig
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
}
debug {
applicationIdSuffix = ".debug"
buildConfigField("String", "BUILD_TIME", "\"\"")
}
}
@@ -169,11 +167,11 @@ android {
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation("androidx.activity:activity-compose:1.8.0-rc01")
implementation("androidx.activity:activity-compose:1.8.0-alpha07")
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-rc01")
implementation("androidx.collection:collection-ktx:1.3.0-beta01")
// https://developer.android.com/jetpack/androidx/releases/compose-material3
// api(platform("androidx.compose:compose-bom:2023.05.00"))
@@ -183,31 +181,31 @@ dependencies {
implementation("androidx.compose.material3:material3-window-size-class")
implementation("androidx.compose.ui:ui-util")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.core:core-ktx:1.12.0-rc01")
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha12")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha05")
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha03")
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation("androidx.lifecycle:lifecycle-process:2.6.2")
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
// https://developer.android.com/jetpack/androidx/releases/navigation
val navigation = "2.7.3"
implementation("androidx.navigation:navigation-fragment-ktx:$navigation")
implementation("androidx.navigation:navigation-ui-ktx:$navigation")
implementation("androidx.navigation:navigation-compose:$navigation")
val nav_version = "2.7.1"
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.1"
val paging = "3.2.0"
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 = "2.6.0-rc01"
ksp("androidx.room:room-compiler:$room")
implementation("androidx.room:room-paging:$room")
val room_version = "2.6.0-beta01"
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-paging:$room_version")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.work:work-runtime-ktx:2.8.1")
@@ -215,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 = "0.32.0"
implementation("com.google.accompanist:accompanist-themeadapter-material3:$accompanist")
implementation("com.google.accompanist:accompanist-webview:$accompanist")
implementation("com.google.android.material:material:1.11.0-alpha03")
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-alpha02")
val splitties = "3.0.0"
implementation("com.louiscad.splitties:splitties-appctx:$splitties")
@@ -233,14 +231,14 @@ dependencies {
implementation("com.squareup.okio:okio-jvm:3.5.0")
implementation("com.mikepenz:aboutlibraries-core:10.9.1")
implementation("com.mikepenz:aboutlibraries-core:10.8.3")
implementation("dev.chrisbanes.insetter:insetter:0.6.1") // Dead Dependency
implementation("dev.rikka.rikkax.core:core-ktx:1.4.1")
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
implementation(platform("io.arrow-kt:arrow-stack:1.2.1"))
implementation(platform("io.arrow-kt:arrow-stack:1.2.0"))
implementation("io.arrow-kt:arrow-fx-coroutines")
// https://coil-kt.github.io/coil/changelog/
@@ -248,10 +246,13 @@ dependencies {
implementation("io.coil-kt:coil-compose")
implementation("io.coil-kt:coil-gif")
val ktor = "2.3.4"
val ktor = "2.3.3"
implementation("io.ktor:ktor-io-jvm:$ktor")
implementation("io.ktor:ktor-utils-jvm:$ktor")
implementation("org.chromium.net:cronet-api:113.5672.61")
implementation("com.google.android.gms:play-services-cronet:18.0.1")
val serialization = "1.6.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
@@ -294,19 +295,6 @@ cargo {
if (isRelease) profile = "release"
}
spotless {
kotlin {
ktlint()
}
kotlinGradle {
ktlint().editorConfigOverride(
mapOf(
"ktlint_standard_multiline-expression-wrapping" to "disabled",
),
)
}
}
tasks.configureEach {
if ((name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders")) {
dependsOn("cargoBuild")
@@ -0,0 +1,34 @@
From 2bda36800a5424c44378bbc0cc8bb5d26a0f34fa Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 22 Jul 2023 08:30:20 +0800
Subject: [PATCH] Don't build other binarys
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 55cd358..52b57aa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -962,7 +962,7 @@ check_symbol_exists(getopt_long getopt.h HAVE_GETOPT_LONG)
# xzdec
#############################################################################
-if(HAVE_GETOPT_LONG AND HAVE_DECODERS)
+if(0)
add_executable(xzdec
src/common/sysdefs.h
src/common/tuklib_common.h
@@ -1008,7 +1008,7 @@ endif()
# xz
#############################################################################
-if(NOT MSVC AND HAVE_GETOPT_LONG)
+if(0)
add_executable(xz
src/common/mythread.h
src/common/sysdefs.h
--
2.41.0
+29
View File
@@ -0,0 +1,29 @@
From 57da49f9b0ed8538573f6cfe289f835ee0078eec Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 22 Jul 2023 17:50:08 +0800
Subject: [PATCH] Hide symbols
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 646484b7..67a71272 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2060,10 +2060,10 @@ SET(OLD_CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS}")
SET(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -fvisibility=hidden -Werror")
CHECK_C_SOURCE_COMPILES("void __attribute__((visibility(\"default\"))) foo(void);
int main() { return 0; }" HAVE_VISIBILITY_ATTR)
-IF (HAVE_VISIBILITY_ATTR)
+IF (0)
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
ADD_DEFINITIONS(-D__LIBARCHIVE_ENABLE_VISIBILITY)
-ENDIF(HAVE_VISIBILITY_ATTR)
+ENDIF(0)
SET(CMAKE_REQUIRED_FLAGS "${OLD_CMAKE_REQUIRED_FLAGS}")
# Generate "config.h" from "build/cmake/config.h.in"
--
2.41.0
@@ -0,0 +1,25 @@
From c9298fd6dc0a8b04dd4b2247118b9cb895c46e58 Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 23 Jul 2022 16:05:36 +0800
Subject: [PATCH 2/2] Insert link libs
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 713e3bc5..7247640d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -415,7 +415,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
# # endif
ENDIF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
-SET(ADDITIONAL_LIBS "")
+SET(ADDITIONAL_LIBS ${LIBARCHIVE_CUSTOM_LIBS})
#
# Find ZLIB
#
--
2.39.2
+6 -11
View File
@@ -23,21 +23,15 @@ set(FETCHCONTENT_BASE_DIR ${deps})
set(BUILD_TESTING 0)
FetchContent_Declare(liblzma
GIT_REPOSITORY https://github.com/FooIbar/xz.git
GIT_TAG v5.4.4
GIT_REPOSITORY https://git.tukaani.org/xz.git
GIT_TAG v5.4.3
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binarys.patch || cd .
)
FetchContent_MakeAvailable(liblzma)
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
# Build GNUTLS libnettle
FetchContent_Declare(nettle
URL https://ftp.gnu.org/gnu/nettle/nettle-3.9.1.tar.gz
URL_MD5 29fcd2dec6bf5b48e5e3ffb3cbc4779e
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/nettle/nettle
)
FetchContent_MakeAvailable(nettle)
add_subdirectory(nettle)
# Configure libnettle support for libarchive
@@ -68,8 +62,9 @@ option(ENABLE_TEST OFF)
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
FetchContent_Declare(libarchive
GIT_REPOSITORY https://github.com/FooIbar/libarchive.git
GIT_TAG v3.7.2
GIT_REPOSITORY https://github.com/libarchive/libarchive
GIT_TAG v3.7.0
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Hide-symbols.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || cd .
)
FetchContent_MakeAvailable(libarchive)
-1
View File
@@ -1 +0,0 @@
/nettle
+2 -1
View File
@@ -32,6 +32,7 @@ set(LIBNETTLE_SOURCES
nettle/aes256-meta.c
nettle/nist-keywrap.c
nettle/arcfour.c
nettle/arcfour-crypt.c
nettle/arctwo.c
nettle/arctwo-meta.c
nettle/blowfish.c
@@ -176,7 +177,7 @@ set(LIBNETTLE_SOURCES
nettle/sha1-compress.c
nettle/sha1-meta.c
nettle/sha256.c
nettle/sha256-compress-n.c
nettle/sha256-compress.c
nettle/sha224-meta.c
nettle/sha256-meta.c
nettle/sha512.c
+20 -25
View File
@@ -4,19 +4,13 @@
/* Define if building universal (internal helper macro) */
/* #undef AC_APPLE_UNIVERSAL_BUILD */
/* Define to one of `_getb67', `GETB67', `getb67' for Cray-2 and Cray-YMP
systems. This function is required for `alloca.c' support on those systems.
*/
/* #undef CRAY_STACKSEG_END */
/* Define to 1 if using `alloca.c'. */
/* Define to 1 if using 'alloca.c'. */
/* #undef C_ALLOCA */
/* Define to 1 if you have `alloca', as a function or macro. */
/* Define to 1 if you have 'alloca', as a function or macro. */
#define HAVE_ALLOCA 1
/* Define to 1 if you have <alloca.h> and it should be used (not on Ultrix).
*/
/* Define to 1 if <alloca.h> works. */
#define HAVE_ALLOCA_H 1
/* Define if __builtin_bswap64 is available */
@@ -52,9 +46,6 @@
/* Define to 1 if you have the <malloc.h> header file. */
#define HAVE_MALLOC_H 1
/* Define to 1 if you have the <memory.h> header file. */
#define HAVE_MEMORY_H 1
/* Define to 1 each of the following for which a native (ie. CPU specific)
implementation of the corresponding routine exists. */
/* #undef HAVE_NATIVE_memxor3 */
@@ -80,7 +71,7 @@
/* #undef HAVE_NATIVE_cbc_aes256_encrypt */
/* #undef HAVE_NATIVE_chacha_core */
/* #undef HAVE_NATIVE_chacha_2core */
#define HAVE_NATIVE_chacha_3core 1
/* #undef HAVE_NATIVE_chacha_3core */
/* #undef HAVE_NATIVE_chacha_4core */
/* #undef HAVE_NATIVE_fat_chacha_2core */
/* #undef HAVE_NATIVE_fat_chacha_3core */
@@ -97,18 +88,13 @@
/* #undef HAVE_NATIVE_ecc_secp384r1_redc */
/* #undef HAVE_NATIVE_ecc_secp521r1_modp */
/* #undef HAVE_NATIVE_ecc_secp521r1_redc */
/* #undef HAVE_NATIVE_poly1305_set_key */
/* #undef HAVE_NATIVE_poly1305_block */
/* #undef HAVE_NATIVE_poly1305_digest */
/* #undef HAVE_NATIVE_poly1305_blocks */
/* #undef HAVE_NATIVE_fat_poly1305_blocks */
/* #undef HAVE_NATIVE_ghash_set_key */
/* #undef HAVE_NATIVE_ghash_update */
/* #undef HAVE_NATIVE_salsa20_core */
#define HAVE_NATIVE_salsa20_2core 1
/* #undef HAVE_NATIVE_salsa20_2core */
/* #undef HAVE_NATIVE_fat_salsa20_2core */
/* #undef HAVE_NATIVE_sha1_compress */
/* #undef HAVE_NATIVE_sha256_compress_n */
/* #undef HAVE_NATIVE_sha256_compress */
/* #undef HAVE_NATIVE_sha512_compress */
/* #undef HAVE_NATIVE_sha3_permute */
/* #undef HAVE_NATIVE_umac_nh */
@@ -121,11 +107,14 @@
/* #undef HAVE_OPENSSL_EVP_H */
/* Define to 1 if you have the `secure_getenv' function. */
#define HAVE_SECURE_GETENV 1
/* #undef HAVE_SECURE_GETENV */
/* Define to 1 if you have the <stdint.h> header file. */
#define HAVE_STDINT_H 1
/* Define to 1 if you have the <stdio.h> header file. */
#define HAVE_STDIO_H 1
/* Define to 1 if you have the <stdlib.h> header file. */
#define HAVE_STDLIB_H 1
@@ -141,6 +130,9 @@
/* Define to 1 if you have the <sys/stat.h> header file. */
#define HAVE_SYS_STAT_H 1
/* Define to 1 if you have the <sys/time.h> header file. */
#define HAVE_SYS_TIME_H 1
/* Define to 1 if you have the <sys/types.h> header file. */
#define HAVE_SYS_TYPES_H 1
@@ -157,7 +149,7 @@
#define PACKAGE_NAME "nettle"
/* Define to the full name and version of this package. */
#define PACKAGE_STRING "nettle 3.9.1"
#define PACKAGE_STRING "nettle 3.8.1"
/* Define to the one symbol short name of this package. */
#define PACKAGE_TARNAME "nettle"
@@ -166,7 +158,7 @@
#define PACKAGE_URL ""
/* Define to the version of this package. */
#define PACKAGE_VERSION "3.9.1"
#define PACKAGE_VERSION "3.8.1"
/* The size of `long', as computed by sizeof. */
#define SIZEOF_LONG __SIZEOF_LONG__
@@ -182,10 +174,13 @@
STACK_DIRECTION = 0 => direction of growth unknown */
/* #undef STACK_DIRECTION */
/* Define to 1 if you have the ANSI C header files. */
/* Define to 1 if all of the C90 standard headers exist (not just the ones
required in a freestanding environment). This macro is provided for
backward compatibility; new code need not use it. */
#define STDC_HEADERS 1
/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. */
/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. This
macro is obsolete. */
#define TIME_WITH_SYS_TIME 1
/* Defined if public key features are enabled */
+1 -1
View File
@@ -41,7 +41,7 @@ extern "C" {
/* Individual version numbers in decimal */
#define NETTLE_VERSION_MAJOR 3
#define NETTLE_VERSION_MINOR 9
#define NETTLE_VERSION_MINOR 8
#define NETTLE_USE_MINI_GMP 0
@@ -25,6 +25,7 @@ import androidx.lifecycle.coroutineScope
import coil.ImageLoaderFactory
import coil.decode.ImageDecoderDecoder
import coil.util.DebugLogger
import com.google.android.gms.net.CronetProviderInstaller
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.data.GalleryDetail
@@ -77,6 +78,7 @@ class EhApplication : Application(), ImageLoaderFactory {
handler?.uncaughtException(t, e)
}
super.onCreate()
CronetProviderInstaller.installProvider(appCtx)
System.loadLibrary("ehviewer")
System.loadLibrary("ehviewer_rust")
ReadableTime.initialize(this)
@@ -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 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 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 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,7 +60,6 @@ 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 thumbSizeDp by intPref("thumb_size_", 120)
@@ -95,6 +94,13 @@ 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, body)
throw EhException(appCtx.getString(R.string.error_parse_error), e)
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e)
throw EhException(appCtx.getString(R.string.error_parse_error))
}
}
@@ -126,18 +126,12 @@ object EhUtils {
MISC -> BG_COLOR_MISC
else -> BG_COLOR_UNKNOWN
}.toInt()
return if (Settings.harmonizeCategoryColor) {
harmonizeWithRole(context, primary)
} else {
primary
}
return harmonizeWithRole(context, primary)
}
@Composable
fun getCategoryColor(category: Int) = Color(getCategoryColor(LocalContext.current, category))
val categoryTextColor = Color(0xffe6e0e9)
fun signOut() {
EhCookieStore.signOut()
Settings.avatar = null
@@ -16,7 +16,16 @@
package com.hippo.ehviewer.client.exception
class ParseException : EhException {
constructor(detailMessage: String?) : super(detailMessage)
val body: String
constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)
constructor(detailMessage: String?, body: String) : super(detailMessage) {
this.body = body
}
constructor(detailMessage: String?, body: String, cause: Throwable?) : super(
detailMessage,
cause,
) {
this.body = body
}
}
@@ -12,7 +12,7 @@ object FavoritesParser {
}
val catArray = arrayOfNulls<String>(10)
val countArray = parseFav(body, catArray)
if (countArray.isEmpty()) throw ParseException("Parse favorites error")
if (countArray.isEmpty()) throw ParseException("Parse favorites error", body)
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")
} ?: throw ParseException("Can't parse gallery detail", body)
if (gd.gid == -1L) {
throw ParseException("Can't parse gallery detail")
throw ParseException("Can't parse gallery detail", body)
}
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")
throw ParseException("Can't parse gallery detail", body)
}
// 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")
?: throw ParseException("Parse preview page count error", body)
}
/**
@@ -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")
?: throw ParseException("Parse pages error", body)
}
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> {
@@ -25,7 +25,7 @@ object GalleryListParser {
if (body.contains("No hits found</p>")) {
throw EhException(R.string.gallery_list_empty_hit)
}
throw ParseException("Can't parse gallery list", it)
throw ParseException("Can't parse gallery list", body, it)
}
}
@@ -33,7 +33,7 @@ object GalleryMultiPageViewerPTokenParser {
imagelist.parseAs<List<Item>>().map(Item::token)
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw ParseException("Parse pToken from MPV error", it)
throw ParseException("Parse pToken from MPV error", body, 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) }
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error, body) }
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")
throw ParseException("Parse image url and skip hath key error", body)
}
}
@@ -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")
throw ParseException("Parse image url and show error", body)
}
}
@@ -24,7 +24,7 @@ object HomeParser {
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
return Funds(fundsGP, fundsC)
}
throw ParseException("Parse funds error")
throw ParseException("Parse funds error", body)
}
@Parcelize
@@ -46,7 +46,7 @@ object ProfileParser {
Result(displayName, avatar)
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw ParseException("Parse forums error")
throw ParseException("Parse forums error", body)
}
}
@@ -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")
throw ParseException("Can't parse sign in", body)
}
}
}
@@ -1,12 +1,9 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
object TorrentParser {
fun parse(body: ByteBuffer) = runCatching {
parseTorrent(body)
}.getOrElse { throw ParseException("Can't parse torrent list", it) }
fun parse(body: ByteBuffer) = parseTorrent(body)
}
class Torrent(
@@ -1,12 +1,9 @@
@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
@@ -30,7 +27,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) {
setCacheDisabled(true)
disableCache()
}.execute {
diskCache.suspendEdit(diskCacheKey) {
RandomAccessFile(data.toFile(), "rw").use {
@@ -1,13 +1,5 @@
@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
@@ -20,6 +12,10 @@ 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
@@ -32,23 +28,24 @@ import kotlin.coroutines.resumeWithException
private const val TAG = "CronetRequest"
val pool = DirectByteBufferPool(32)
val cronetHttpClient: HttpEngine = HttpEngine.Builder(appCtx).apply {
setEnableBrotli(true)
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
enableBrotli(true)
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
enableHttpCache(CronetEngine.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()
@@ -67,18 +64,14 @@ class CronetRequest {
readerCont.resume(Unit)
}
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: HttpException) {
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: CronetException) {
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, cronetHttpClientExecutor, callback).apply {
request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply {
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
addHeader("Accept", CHROME_ACCEPT)
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
@@ -462,11 +462,7 @@ object DownloadManager : OnSpiderListener {
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()
}
return moveDownload(fromPosition, toPosition)
}
suspend fun resetAllReadingProgress() = coroutineScope {
@@ -135,9 +135,9 @@ class SpiderDen(mGalleryInfo: GalleryInfo) {
): Boolean {
return if (isCronetSupported) {
cronetRequest(url, referer) {
setCacheDisabled(true)
disableCache()
}.execute { info ->
val headers = info.headers.asMap
val headers = info.allHeaders
val type = headers["Content-Type"]?.first()?.toMediaType()?.subtype ?: "jpg"
val length = headers["Content-Length"]!!.first().toLong()
saveResponseMeta(index, type, length) { file ->
@@ -5,12 +5,10 @@ import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.imePadding
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
@@ -48,7 +46,6 @@ class ConfigureActivity : EhActivity() {
} else {
BASE_SETTINGS_SCREEN
},
modifier = Modifier.imePadding(),
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up, tween(200)) },
exitTransition = { fadeOut(tween(200)) },
popEnterTransition = { fadeIn(tween(200)) },
@@ -8,11 +8,13 @@ import android.util.AttributeSet
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ElevatedCard
@@ -31,10 +33,6 @@ import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.get
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -50,125 +48,120 @@ import com.hippo.ehviewer.util.pickVisualMedia
import com.hippo.unifile.UniFile
import kotlinx.coroutines.launch
class SearchViewModel : ViewModel() {
var isNormalMode by mutableStateOf(true) // else ImageSearch mode
var isAdvancedMode by mutableStateOf(false)
var mCategory by mutableIntStateOf(Settings.searchCategory)
var mSearchMode by mutableIntStateOf(1)
var advancedState by mutableStateOf(AdvancedSearchOption())
var uss by mutableStateOf(false)
var osc by mutableStateOf(false)
var path by mutableStateOf("")
}
class SearchLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : AbstractComposeView(context, attrs, defStyle) {
private val vm by lazy {
ViewModelProvider(findViewTreeViewModelStoreOwner()!!).get<SearchViewModel>()
}
private var isNormalMode by mutableStateOf(true) // else ImageSearch mode
private var isAdvancedMode by mutableStateOf(false)
private var mCategory by mutableIntStateOf(Settings.searchCategory)
private var mSearchMode by mutableIntStateOf(1)
private var advancedState by mutableStateOf(AdvancedSearchOption())
private var uss by mutableStateOf(false)
private var osc by mutableStateOf(false)
private var path by mutableStateOf("")
@Composable
override fun Content() {
val coroutineScope = rememberCoroutineScope()
fun selectImage() = coroutineScope.launch {
context.pickVisualMedia(ActivityResultContracts.PickVisualMedia.ImageOnly)?.let {
vm.path = it.toString()
path = it.toString()
}
}
Mdc3Theme {
Column(modifier = Modifier.imePadding().verticalScroll(rememberScrollState()).navigationBarsPadding().padding(horizontal = dimensionResource(id = R.dimen.search_layout_margin_h))) {
AnimatedVisibility(visible = vm.isNormalMode) {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_layout_margin_h)).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))) {
Text(text = stringResource(id = R.string.search_normal), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
NormalSearch(
category = vm.mCategory,
category = mCategory,
onCategoryChanged = {
Settings.searchCategory = it
vm.mCategory = it
mCategory = it
},
searchMode = vm.mSearchMode,
onSearchModeChanged = { vm.mSearchMode = it },
isAdvanced = vm.isAdvancedMode,
onAdvancedChanged = { vm.isAdvancedMode = it },
searchMode = mSearchMode,
onSearchModeChanged = { mSearchMode = it },
isAdvanced = isAdvancedMode,
onAdvancedChanged = { isAdvancedMode = it },
showInfo = { BaseDialogBuilder(context).setMessage(R.string.search_tip).show() },
)
}
}
}
AnimatedVisibility(visible = vm.isNormalMode && vm.isAdvancedMode) {
AnimatedVisibility(visible = isNormalMode && isAdvancedMode) {
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))) {
Text(text = stringResource(id = R.string.search_advance), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
SearchAdvanced(
state = vm.advancedState,
onStateChanged = { vm.advancedState = it },
state = advancedState,
onStateChanged = { advancedState = it },
)
}
}
}
AnimatedVisibility(visible = !vm.isNormalMode) {
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))) {
Text(text = stringResource(id = R.string.search_image), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
ImageSearch(
imagePath = vm.path,
imagePath = path,
onSelectImage = ::selectImage,
uss = vm.uss,
onUssChecked = { vm.uss = it },
osc = vm.osc,
onOscChecked = { vm.osc = it },
uss = uss,
onUssChecked = { uss = it },
osc = osc,
onOscChecked = { osc = it },
)
}
}
}
TabRow(
selectedTabIndex = if (vm.isNormalMode) 0 else 1,
selectedTabIndex = if (isNormalMode) 0 else 1,
divider = {},
) {
Tab(
selected = vm.isNormalMode,
onClick = { vm.isNormalMode = true },
selected = isNormalMode,
onClick = { isNormalMode = true },
text = { Text(text = stringResource(id = R.string.keyword_search)) },
)
Tab(
selected = !vm.isNormalMode,
onClick = { vm.isNormalMode = false },
selected = !isNormalMode,
onClick = { isNormalMode = false },
text = { Text(text = stringResource(id = R.string.search_image)) },
)
}
Spacer(modifier = Modifier.height(WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
}
}
}
fun setSearchMyTags(isMyTags: Boolean) {
if (isMyTags) vm.mSearchMode = 2
if (isMyTags) mSearchMode = 2
}
fun setCategory(category: Int) {
vm.mCategory = category
mCategory = category
}
fun formatListUrlBuilder(urlBuilder: ListUrlBuilder, query: String?) {
urlBuilder.reset()
when (vm.isNormalMode) {
when (isNormalMode) {
true -> {
when (vm.mSearchMode) {
when (mSearchMode) {
1 -> urlBuilder.mode = ListUrlBuilder.MODE_NORMAL
2 -> urlBuilder.mode = ListUrlBuilder.MODE_SUBSCRIPTION
3 -> urlBuilder.mode = ListUrlBuilder.MODE_UPLOADER
4 -> urlBuilder.mode = ListUrlBuilder.MODE_TAG
}
urlBuilder.keyword = query
urlBuilder.category = vm.mCategory
if (vm.isAdvancedMode) {
urlBuilder.advanceSearch = vm.advancedState.advanceSearch
urlBuilder.minRating = vm.advancedState.minRating
val pageFrom = vm.advancedState.fromPage
val pageTo = vm.advancedState.toPage
urlBuilder.category = mCategory
if (isAdvancedMode) {
urlBuilder.advanceSearch = advancedState.advanceSearch
urlBuilder.minRating = advancedState.minRating
val pageFrom = advancedState.fromPage
val pageTo = advancedState.toPage
if (pageTo != -1 && pageTo < 10) {
throw EhException(context.getString(R.string.search_sp_err1))
} else if (pageFrom != -1 && pageTo != -1 && pageTo - pageFrom < 20) {
@@ -181,15 +174,15 @@ class SearchLayout @JvmOverloads constructor(
false -> {
urlBuilder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
if (vm.path.isBlank()) throw EhException(context.getString(R.string.select_image_first))
val uri = Uri.parse(vm.path)
if (path.isBlank()) throw EhException(context.getString(R.string.select_image_first))
val uri = Uri.parse(path)
val src = UniFile.fromUri(context, uri)?.imageSource ?: return
val temp = AppConfig.createTempFile() ?: return
val bitmap = ImageDecoder.decodeBitmap(src, Image.imageSearchDecoderSampleListener)
temp.outputStream().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
urlBuilder.imagePath = temp.path
urlBuilder.isUseSimilarityScan = vm.uss
urlBuilder.isOnlySearchCovers = vm.osc
urlBuilder.isUseSimilarityScan = uss
urlBuilder.isOnlySearchCovers = osc
}
}
}
@@ -257,7 +257,7 @@ fun CookieSignInScene(windowSizeClass: WindowSizeClass) {
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(padding).padding(dimensionResource(id = R.dimen.keyline_margin)),
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin)).padding(padding).fillMaxSize().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
@@ -284,7 +284,7 @@ fun CookieSignInScene(windowSizeClass: WindowSizeClass) {
}
WindowWidthSizeClass.Expanded -> {
Row(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(padding).padding(dimensionResource(id = R.dimen.keyline_margin)),
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin)).padding(padding).verticalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
@@ -5,10 +5,12 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
@@ -43,7 +45,7 @@ fun SelectSiteScreen() {
}
Column(
modifier = Modifier.systemBarsPadding().padding(horizontal = dimensionResource(R.dimen.keyline_margin)).padding(top = dimensionResource(R.dimen.keyline_margin)).fillMaxSize(),
modifier = Modifier.padding(WindowInsets.systemBars.asPaddingValues()).padding(horizontal = dimensionResource(R.dimen.keyline_margin)).padding(top = dimensionResource(R.dimen.keyline_margin)).fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
@@ -182,7 +181,7 @@ fun SignInScreen(windowSizeClass: WindowSizeClass) {
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = R.dimen.keyline_margin)),
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin)).fillMaxSize().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
@@ -267,7 +266,7 @@ fun SignInScreen(windowSizeClass: WindowSizeClass) {
}
WindowWidthSizeClass.Expanded -> {
Row(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = R.dimen.keyline_margin)),
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin)).fillMaxSize().verticalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
@@ -10,7 +10,7 @@ object AdvanceTable {
data class AdvancedSearchOption(
val advanceSearch: Int = 0,
val minRating: Int = -1,
val minRating: Int = 0,
val fromPage: Int = -1,
val toPage: Int = -1,
)
@@ -28,7 +28,6 @@ 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(
@@ -56,10 +55,13 @@ fun SearchAdvanced(
onExpandedChange = { expanded = !expanded },
) {
val softwareKeyboardController = LocalSoftwareKeyboardController.current
SideEffect {
softwareKeyboardController?.hide()
}
OutlinedTextField(
modifier = Modifier.menuAnchor(),
readOnly = true,
value = minRatingItems[if (state.minRating in 2..5) state.minRating - 1 else 0],
value = minRatingItems[state.minRating],
onValueChange = {},
label = { Text(stringResource(id = R.string.search_sr)) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
@@ -69,16 +71,12 @@ fun SearchAdvanced(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
SideEffect {
softwareKeyboardController?.hide()
}
minRatingItems.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption) },
onClick = {
expanded = false
val index = minRatingItems.indexOf(selectionOption)
onStateChanged(state.copy(minRating = if (index > 0) index + 1 else -1))
onStateChanged(state.copy(minRating = minRatingItems.indexOf(selectionOption)))
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
@@ -90,18 +88,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.NumberPassword),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
value = if (enabled && state.fromPage != -1) state.fromPage.toString() else "",
onValueChange = { onStateChanged(state.copy(fromPage = it.toIntOrDefault(-1))) },
onValueChange = { onStateChanged(state.copy(fromPage = it.toInt())) },
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.NumberPassword),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
value = if (enabled && state.toPage != -1) state.toPage.toString() else "",
onValueChange = { onStateChanged(state.copy(toPage = it.toIntOrDefault(-1))) },
onValueChange = { onStateChanged(state.copy(toPage = it.toInt())) },
modifier = Modifier.width(96.dp).padding(16.dp),
singleLine = true,
enabled = enabled,
@@ -21,7 +21,6 @@ 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
@@ -29,14 +28,12 @@ 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
@@ -112,7 +109,6 @@ 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(
@@ -202,11 +198,10 @@ 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 = categoryColor,
contentColor = if (Settings.harmonizeCategoryColor) contentColorFor(categoryColor) else EhUtils.categoryTextColor,
containerColor = EhUtils.getCategoryColor(info.category),
contentColor = MaterialTheme.colorScheme.onSurface,
) {
simpleLang?.let {
Text(text = it.uppercase())
@@ -156,10 +156,7 @@ abstract class BaseScene : Fragment() {
}
hideSoftInput()
createDrawerView(savedInstanceState)?.let {
sideSheetDialog = object : SideSheetDialog(requireContext()) {
// Dirty workaround
override fun cancel() = hide()
}.apply {
sideSheetDialog = SideSheetDialog(requireContext()).apply {
window?.decorView?.apply {
val owner = viewLifecycleOwner
setViewTreeLifecycleOwner(owner)
@@ -46,7 +46,6 @@ 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
@@ -58,6 +57,7 @@ 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
@@ -299,13 +299,15 @@ class DownloadsScene :
}
recyclerView.addItemDecoration(decoration)
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 if (tracker.isInCustomChoice) {
0
} else {
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT)
}
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT)
}
override fun onMove(
recyclerView: RecyclerView,
@@ -362,8 +364,14 @@ class DownloadsScene :
}
override fun onSearchViewExpanded() {
hideActionFab(true)
super.onSearchViewExpanded()
if (tracker.isInCustomChoice) tracker.clearSelection()
showSearchFab(true)
}
override fun onSearchViewHidden() {
super.onSearchViewHidden()
hideSearchFab(true)
}
override fun onDestroyView() {
@@ -810,7 +818,6 @@ 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,
)
@@ -333,7 +333,7 @@ class FavoritesScene : SearchBarScene() {
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
binding.tip.setCompoundDrawables(null, drawable, null, null)
binding.tip.setOnClickListener { mAdapter?.refresh() }
binding.refreshLayout.setOnRefreshListener { switchFav(urlBuilder.favCat) }
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
viewLifecycleOwner.lifecycleScope.launch {
@@ -392,11 +392,7 @@ class FavoritesScene : SearchBarScene() {
}
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)
}
return makeMovementFlags(0, ItemTouchHelper.LEFT)
}
override fun onMove(
recyclerView: RecyclerView,
@@ -482,13 +478,13 @@ class FavoritesScene : SearchBarScene() {
}
override fun onSearchViewExpanded() {
hideActionFab(true)
super.onSearchViewExpanded()
selectSearchFab(true)
}
override fun onSearchViewHidden() {
super.onSearchViewHidden()
showActionFab(true)
selectActionFab(true)
}
private fun showGoToDialog() {
@@ -27,7 +27,6 @@ import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.material3.Text
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
@@ -40,7 +39,6 @@ import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.lifecycle.viewModelScope
import androidx.paging.LoadState
import androidx.paging.Pager
@@ -286,7 +284,7 @@ class GalleryListScene : SearchBarScene() {
setSearchBarHint(title)
when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> if (category != EhUtils.NONE || !keyword.isNullOrEmpty()) {
MODE_NORMAL -> if (category != EhUtils.NONE || !keyword.isNullOrEmpty()) {
mainActivity?.clearNavCheckedItem()
}
MODE_TAG, MODE_UPLOADER, MODE_IMAGE_SEARCH -> mainActivity?.clearNavCheckedItem()
@@ -333,8 +331,6 @@ class GalleryListScene : SearchBarScene() {
),
)
mViewTransition = BringOutTransition(binding.contentLayout.contentView, binding.searchLayout)
binding.searchLayout.consumeWindowInsets = false
binding.searchLayout.setViewTreeViewModelStoreOwner(this)
binding.fastScroller.setOnDragHandlerListener(object : OnDragHandlerListener {
override fun onStartDragHandler() {}
override fun onEndDragHandler() {
@@ -364,11 +360,7 @@ class GalleryListScene : SearchBarScene() {
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
binding.tip.setCompoundDrawables(null, drawable, null, null)
binding.tip.setOnClickListener { mAdapter?.refresh() }
binding.refreshLayout.setOnRefreshListener {
mUrlBuilder.setIndex(null, true)
mUrlBuilder.mJumpTo = null
mAdapter?.refresh()
}
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
launch {
@@ -517,15 +509,14 @@ class GalleryListScene : SearchBarScene() {
tip: TextView,
) {
val context = context ?: return
if (mAdapter!!.itemCount == 0) return
// Can't add image search as quick search
if (MODE_IMAGE_SEARCH == mUrlBuilder.mode) {
showTip(R.string.image_search_not_quick_search, LENGTH_LONG)
return
}
val gi = mAdapter!!.peek(binding.recyclerView.layoutManager!!.firstVisibleItemPosition)!!
val next = gi.gid + 1
val gi = mAdapter?.peek(binding.recyclerView.layoutManager!!.firstVisibleItemPosition)
val next = gi?.gid?.plus(1)
// Check duplicate
for (q in mQuickSearchList) {
@@ -560,7 +551,7 @@ class GalleryListScene : SearchBarScene() {
}
return@launchIO
}
if (checked[0]) {
if (checked[0] && next != null) {
text += "@$next"
Settings.qSSaveProgress = true
} else {
@@ -623,22 +614,20 @@ class GalleryListScene : SearchBarScene() {
drawerBinding.toolbar.setTitle(R.string.toplist)
} else {
drawerBinding.toolbar.setTitle(R.string.quick_search)
if (mUrlBuilder.mode != MODE_WHATS_HOT) {
drawerBinding.toolbar.inflateMenu(R.menu.drawer_gallery_list)
drawerBinding.toolbar.setOnMenuItemClickListener { item: MenuItem ->
val id = item.itemId
if (id == R.id.action_add) {
showAddQuickSearchDialog(
qsDrawerAdapter,
drawerBinding.recyclerViewDrawer,
drawerBinding.tip,
)
} else if (id == R.id.action_help) {
showQuickSearchTipDialog()
}
true
}
}
if (!mIsTopList) drawerBinding.toolbar.inflateMenu(R.menu.drawer_gallery_list)
drawerBinding.toolbar.setOnMenuItemClickListener { item: MenuItem ->
val id = item.itemId
if (id == R.id.action_add) {
showAddQuickSearchDialog(
qsDrawerAdapter,
drawerBinding.recyclerViewDrawer,
drawerBinding.tip,
)
} else if (id == R.id.action_help) {
showQuickSearchTipDialog()
}
true
}
return drawerBinding.root
}
@@ -698,21 +687,13 @@ 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) {
showActionFab(true)
} else {
showSearchFab(true)
}
if (mState == State.NORMAL) selectActionFab(true)
}
private fun showActionFab() {
@@ -861,17 +842,13 @@ class GalleryListScene : SearchBarScene() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {
val holder = QsDrawerHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
holder.itemView.setOnClickListener {
if (mUrlBuilder.mode == MODE_WHATS_HOT) {
if (!mIsTopList) {
val q = mQuickSearchList[holder.bindingAdapterPosition]
// Navigate to galleryListScene to clear nav checked item
navAnimated(R.id.galleryListScene, ListUrlBuilder().apply { set(q) }.toStartArgs())
} else {
if (mIsTopList) {
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
mUrlBuilder.mJumpTo = null
} else {
val q = mQuickSearchList[holder.bindingAdapterPosition]
mUrlBuilder.set(q)
}
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
mUrlBuilder.mJumpTo = null
onUpdateUrlBuilder()
mAdapter?.refresh()
}
@@ -11,7 +11,6 @@ import androidx.annotation.CallSuper
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -83,7 +82,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
onApplySearch()
true
}
binding.searchBarList.consumeWindowInsets = false
binding.searchBarList.setMD3Content {
LazyColumn(contentPadding = WindowInsets.navigationBars.asPaddingValues()) {
items(mSuggestionList) {
@@ -142,7 +140,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
override fun onDestroyView() {
super.onDestroyView()
mSearchViewOnBackPressedCallback.remove()
binding.root.removeAllViews()
_binding = null
}
@@ -170,7 +167,8 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
fun showSearchFab(animation: Boolean, delay: Long = 0) {
if (animation) {
mSearchFab.visibility = View.VISIBLE
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
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 {
@@ -198,26 +196,10 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
}
}
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 {
fun selectSearchFab(animation: Boolean) {
_binding ?: return
mShowActionFab = false
return if (animation) {
val delay = if (animation) {
val fab = fabLayout.primaryFab!!
if (View.INVISIBLE == fab.visibility) {
0L
@@ -236,16 +218,27 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
fab.scaleY = 0.0f
0L
}
}
fun selectSearchFab(animation: Boolean) {
val delay = hideActionFab(animation)
showSearchFab(animation, delay)
}
fun selectActionFab(animation: Boolean) {
_binding ?: return
mShowActionFab = true
val delay = hideSearchFab(animation)
showActionFab(animation, delay)
if (animation) {
val fab = 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 {
fabLayout.setExpanded(expanded = false, animation = false)
val fab = fabLayout.primaryFab!!
fab.visibility = View.VISIBLE
fab.scaleX = 1.0f
fab.scaleY = 1.0f
}
}
fun setSearchBarHint(hint: String?) {
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -83,7 +84,7 @@ fun AboutScreen() {
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
Column(modifier = Modifier.padding(top = paddingValues.calculateTopPadding()).nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState())) {
Preference(
title = stringResource(id = R.string.settings_about_declaration),
summary = stringResource(id = R.string.settings_about_declaration_summary),
@@ -111,7 +112,7 @@ fun AboutScreen() {
title = stringResource(id = R.string.use_ci_update_channel),
value = Settings::useCIUpdateChannel,
)
SimpleMenuPreferenceInt(
SimpleMenuPreferenceInt2(
title = stringResource(id = R.string.auto_updates),
entry = R.array.update_frequency,
entryValueRes = R.array.update_frequency_values,
@@ -126,6 +127,7 @@ fun AboutScreen() {
launchSnackBar(ExceptionUtils.getReadableString(it))
}
}
Spacer(modifier = Modifier.size(paddingValues.calculateBottomPadding()))
}
}
}
@@ -7,7 +7,9 @@ import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
import android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -68,7 +70,7 @@ fun AdvancedScreen() {
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
Column(modifier = Modifier.padding(top = paddingValues.calculateTopPadding()).nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState())) {
SwitchPreference(
title = stringResource(id = R.string.settings_advanced_save_parse_error_body),
summary = stringResource(id = R.string.settings_advanced_save_parse_error_body_summary),
@@ -223,6 +225,7 @@ fun AdvancedScreen() {
}
}
}
Spacer(modifier = Modifier.size(paddingValues.calculateBottomPadding()))
}
}
}
@@ -5,7 +5,9 @@ import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -72,7 +74,7 @@ fun DownloadScreen() {
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
Column(modifier = Modifier.padding(top = paddingValues.calculateTopPadding()).nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState())) {
var downloadLocationState by ::downloadLocation.observed
val cannotGetDownloadLocation = stringResource(id = R.string.settings_download_cant_get_download_location)
val selectDownloadDirLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { treeUri ->
@@ -230,6 +232,7 @@ fun DownloadScreen() {
it.printStackTrace()
}
}
Spacer(modifier = Modifier.size(paddingValues.calculateBottomPadding()))
}
}
}
@@ -4,7 +4,9 @@ import android.content.DialogInterface
import android.os.Build
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -88,7 +90,7 @@ fun EhScreen() {
) { paddingValues ->
val touristMode = stringResource(id = R.string.settings_eh_identity_cookies_tourist)
val copiedToClipboard = stringResource(id = R.string.copied_to_clipboard)
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
Column(modifier = Modifier.padding(top = paddingValues.calculateTopPadding()).nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState())) {
Preference(
title = stringResource(id = R.string.account_name),
summary = Settings.displayName ?: touristMode,
@@ -222,10 +224,6 @@ 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,
@@ -341,6 +339,7 @@ fun EhScreen() {
)
}
}
Spacer(modifier = Modifier.size(paddingValues.calculateBottomPadding()))
}
}
}
@@ -38,7 +38,7 @@ fun MyTagsScreen() {
val state = rememberWebViewState(url = url)
WebView(
state = state,
modifier = Modifier.padding(paddingValues).fillMaxSize(),
modifier = Modifier.padding(top = paddingValues.calculateTopPadding()).fillMaxSize(),
onCreated = {
it.setDefaultSettings()
},
@@ -114,6 +114,20 @@ 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 {
@@ -62,7 +62,7 @@ fun UConfigScreen() {
val state = rememberWebViewState(url = url)
WebView(
state = state,
modifier = Modifier.padding(paddingValues).fillMaxSize(),
modifier = Modifier.padding(top = paddingValues.calculateTopPadding()).fillMaxSize(),
onCreated = {
it.setDefaultSettings()
},
@@ -3,6 +3,7 @@ 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
@@ -14,6 +15,7 @@ private const val QuarterCircle = PI / 2
@Composable
fun CircularLayout(
modifier: Modifier = Modifier,
radius: Dp,
placeFirstItemInCenter: Boolean = false,
content: @Composable () -> Unit,
) {
@@ -21,7 +23,7 @@ fun CircularLayout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val radiusPx = constraints.maxWidth * 0.4
val radiusPx = radius.toPx()
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
@@ -41,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
@@ -183,7 +183,6 @@ class DialogState {
}
AlertDialog(
onDismissRequest = { cont.cancel() },
properties = DialogProperties(usePlatformDefaultWidth = respectDefaultWidth),
content = {
Surface(
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
@@ -249,10 +248,12 @@ 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,
) {
@@ -260,7 +261,8 @@ class DialogState {
TextField(
value = note,
onValueChange = { note = it },
modifier = Modifier.fillMaxWidth(0.45F).aspectRatio(1F),
modifier = Modifier.width(128.dp),
textStyle = TextStyle(fontFamily = FontFamily.Monospace),
label = { Text(text = stringResource(id = hint)) },
trailingIcon = {
if (note.isNotEmpty()) {
@@ -276,6 +278,7 @@ class DialogState {
textAlign = TextAlign.End,
)
},
maxLines = 6,
shape = ShapeDefaults.ExtraSmall,
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
@@ -284,18 +287,16 @@ class DialogState {
)
items.forEachIndexed { index, (icon, text) ->
Column(
modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note) }
.fillMaxWidth(0.2F),
modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note) },
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
Text(
text = text,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
style = MaterialTheme.typography.bodySmall,
)
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)
}
}
}
}
@@ -34,7 +34,7 @@ fun WebViewScreen(url: String, onNavigateUp: () -> Unit) {
) { paddingValues ->
WebView(
state = state,
modifier = Modifier.padding(paddingValues).fillMaxSize(),
modifier = Modifier.padding(top = paddingValues.calculateTopPadding()).fillMaxSize(),
onCreated = { it.setDefaultSettings() },
)
DisposableEffect(Unit) {
@@ -60,12 +60,12 @@ object AppConfig {
return FileUtils.createTempFile(tempDir, null)
}
fun saveParseErrorBody(e: ParseException, body: String) {
fun saveParseErrorBody(e: ParseException) {
val dir = externalParseErrorDir ?: return
File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt").writer().use {
it.write(e.message)
it.appendLine()
it.append(body)
it.append(e.body)
}
}
@@ -1,8 +1,6 @@
package com.hippo.ehviewer.util
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
import com.google.android.gms.net.CronetProviderInstaller
@get:ChecksSdkIntAtLeast(Build.VERSION_CODES.S, extension = 7)
val isCronetSupported: Boolean
get() = isAtLeastSExtension7
get() = isAtLeastQ && CronetProviderInstaller.isInstalled()
@@ -1,8 +1,5 @@
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
+1 -2
View File
@@ -419,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 -->
@@ -476,7 +476,6 @@
<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>
+20 -20
View File
@@ -21,20 +21,20 @@
<item>@string/settings_eh_list_mode_thumb</item>
</string-array>
<integer-array name="list_mode_entry_values" translatable="false">
<string-array name="list_mode_entry_values" translatable="false">
<item>0</item>
<item>1</item>
</integer-array>
</string-array>
<string-array name="gallery_site_entries" translatable="false">
<item>@string/site_e</item>
<item>@string/site_ex</item>
</string-array>
<integer-array name="gallery_site_entry_values" translatable="false">
<string-array name="gallery_site_entry_values" translatable="false">
<item>0</item>
<item>1</item>
</integer-array>
</string-array>
<string-array name="launch_page_entries" translatable="false">
<item>@string/homepage</item>
@@ -43,22 +43,22 @@
<item>@string/toplist</item>
</string-array>
<integer-array name="launch_page_entry_values" translatable="false">
<string-array name="launch_page_entry_values" translatable="false">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</integer-array>
</string-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>
<integer-array name="detail_size_entry_values" translatable="false">
<string-array name="detail_size_entry_values" translatable="false">
<item>0</item>
<item>1</item>
</integer-array>
</string-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>
<integer-array name="thumb_resolution_entry_values" translatable="false">
<string-array name="thumb_resolution_entry_values" translatable="false">
<item>0</item>
<item>1</item>
<item>2</item>
</integer-array>
</string-array>
<string-array name="search_min_rating" translatable="false">
<item>@string/unlimited</item>
@@ -87,12 +87,12 @@
<item>7</item>
</string-array>
<integer-array name="multi_thread_download_entry_values" translatable="false">
<string-array name="multi_thread_download_entry_values" translatable="false">
<item>1</item>
<item>3</item>
<item>5</item>
<item>7</item>
</integer-array>
</string-array>
<string-array name="download_delay_entries" translatable="false">
<item>0</item>
@@ -107,7 +107,7 @@
<item>20000</item>
</string-array>
<integer-array name="download_delay_entry_values" translatable="false">
<string-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>
</integer-array>
</string-array>
<string-array name="preload_image_entries" translatable="false">
<item>3</item>
@@ -129,14 +129,14 @@
<item>17</item>
</string-array>
<integer-array name="preload_image_entry_values" translatable="false">
<string-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>
</integer-array>
</string-array>
<string-array name="read_cache_size_entries" translatable="false">
<item>320 MB</item>
@@ -146,13 +146,13 @@
<item>5120 MB</item>
</string-array>
<integer-array name="read_cache_size_entry_values" translatable="false">
<string-array name="read_cache_size_entry_values" translatable="false">
<item>320</item>
<item>640</item>
<item>1280</item>
<item>2560</item>
<item>5120</item>
</integer-array>
</string-array>
<string-array name="filter_entries">
<item>@string/filter_title</item>
@@ -208,11 +208,11 @@
<item>@string/dark_theme_on</item>
</string-array>
<integer-array name="night_mode_values">
<string-array name="night_mode_values">
<item>-1</item>
<item>1</item>
<item>2</item>
</integer-array>
</string-array>
<string-array name="download_state">
<item>@string/download_all</item>
+2 -3
View File
@@ -440,7 +440,7 @@
<string name="settings_about_declaration" translatable="false">EhViewer</string>
<string name="settings_about_declaration_summary">EhViewer is not affiliated with E-Hentai.org in any way</string>
<string name="settings_about_author">Author</string>
<string name="settings_about_author_summary" translatable="false">&lt;del&gt;Hippo &amp;lt;ehviewersu$gmail.com&amp;gt;&lt;/del&gt;&lt;br&gt;&lt;del&gt;NekoInverter&lt;/del&gt;&lt;br&gt;&lt;del&gt;飛鳥澪&lt;/del&gt;&lt;br&gt;Foolbar</string>
<string name="settings_about_author_summary" translatable="false">&lt;del&gt;Hippo &amp;lt;ehviewersu$gmail.com&amp;gt;&lt;/del&gt;&lt;br&gt;&lt;del&gt;NekoInverter&lt;/del&gt;&lt;br&gt;飛鳥澪</string>
<string name="settings_about_latest_release">Latest release</string>
<string name="settings_about_source">Source</string>
<string name="settings_about_version">Build version</string>
@@ -477,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">Favorite Note</string>
<string name="favorite_note">Note</string>
<string name="collections">Collections</string>
<string name="need_sign_in">Signing in is required</string>
<!-- Progress Scene -->
@@ -541,7 +541,6 @@
<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 = "0.1.2"
jni_fn = { path = "jni_fn" }
quick-xml = { default-features = false, version = "0.30" }
tl = "0.7"
log = "0.4"
+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
@@ -0,0 +1,2 @@
/target
Cargo.lock
@@ -19,4 +19,4 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
syn = { version = "1.0", features = ["full"] }
+25 -13
View File
@@ -3,7 +3,10 @@ use std::ops::Deref;
use proc_macro::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::{parse_macro_input, Expr, FnArg, ItemFn, LitStr, Pat, Path};
use syn::{
parse_macro_input, AttributeArgs, Expr, FnArg, ItemFn, Lit, Meta, MetaNameValue, NestedMeta,
Pat, Path,
};
const ONLY_FUNCTIONS_MSG: &str = "#[catch_panic] can only be applied to functions";
const FIRST_ARG_MSG: &str =
@@ -30,21 +33,30 @@ 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;
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);
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 default_value = match default_value {
Some(val) => val.to_token_stream(),
+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
+22
View File
@@ -0,0 +1,22 @@
[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
@@ -0,0 +1,398 @@
//! 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`)" }
}
)
);
}
}
+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
+11 -15
View File
@@ -30,21 +30,17 @@ 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);
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()
})
}
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()
})
}).collect::<Vec<Torrent>>())
}).unwrap().into_java(&env).forget().into_raw()
}
+7 -4
View File
@@ -1,20 +1,23 @@
plugins {
id("com.android.application") version "8.1.2" 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.diffplug.spotless") version "6.22.0" apply false
id("com.mikepenz.aboutlibraries.plugin") version "10.9.1" 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
}
allprojects {
apply(from = "$rootDir/ktlint.gradle.kts")
}
tasks.register("Delete", Delete::class) {
delete(rootProject.layout.buildDirectory)
}
buildscript {
dependencies {
classpath("com.android.tools:r8:8.3.6-dev")
classpath("com.android.tools:r8:8.2.20-dev")
}
}
+103
View File
@@ -0,0 +1,103 @@
<p align="right">
<strong>English</strong>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
简体中文
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
正體中文
</a>
</p>
<h1 align="center">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200">
<br>EhViewer<br>
</h1>
<p align="center">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
<img src="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases">
<img src="https://img.shields.io/github/v/release/Ehviewer-Overhauled/ehviewer?include_prereleases" alt="Release">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/issues">
<img src="https://img.shields.io/github/issues/Ehviewer-Overhauled/ehviewer" alt="Issues">
</a>
</p>
<div align="center">
<h3>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/en.md#description">
Description
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/en.md#download">
Download
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/en.md#screenshot">
Screenshot
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/en.md#thanks">
Thanks
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/en.md#license">
License
</a>
</h3>
</div>
# Description
EhViewer fork dedicated to lightweight and high-performance
with Material Design 3 and Dynamic Color Support
# Download
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
# Screenshot
![screenshot-01](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-01.png)
![screenshot-02](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-02.png)
# Thanks
Here is the libraries
- [Arrow](https://arrow-kt.io/)
- [AOSP & AndroidX](http://source.android.com/)
- [Kotlin & KotlinX](https://kotlinlang.org/)
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
- [MCA](https://github.com/material-components/material-components-android)
- [material-design-icons](https://github.com/google/material-design-icons)
- [Okhttp](https://square.github.io/okhttp/)
- [RikkaX](https://github.com/RikkaApps/RikkaX)
- [Libarchive](http://www.libarchive.org/)
- [Coil](https://coil-kt.github.io/coil/)
- [Jsoup](https://jsoup.org/)
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
# License
Copyright 2014-2019 Hippo Seven
Copyright 2020-2022 NekoInverter
Copyright 2022-2023 Tarsin Norbin
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
EhViewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with EhViewer. If not, see <https://www.gnu.org/licenses/>.
+19 -22
View File
@@ -1,57 +1,57 @@
<p align="right">
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/README.md">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/en.md">
English
</a>
<span> | </span>
<strong>简体中文</strong>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-tw.md">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
正體中文
</a>
</p>
<h1 align="center">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200">
<br>EhViewer<br>
</h1>
<p align="center">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
<img src="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/FooIbar/EhViewer/releases">
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases">
<img src="https://img.shields.io/github/v/release/Ehviewer-Overhauled/ehviewer?include_prereleases" alt="Release">
</a>
<a href="https://github.com/FooIbar/EhViewer/issues">
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/issues">
<img src="https://img.shields.io/github/issues/Ehviewer-Overhauled/ehviewer" alt="Issues">
</a>
</p>
<div align="center">
<h3>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-cn.md#描述">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md#描述">
描述
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-cn.md#下载">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md#下载">
下载
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-cn.md#截图">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md#截图">
截图
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-cn.md#感谢">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md#感谢">
感谢
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-cn.md#许可证">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md#许可证">
许可证
</a>
</h3>
@@ -65,9 +65,7 @@
# 下载
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
# 截图
@@ -97,7 +95,6 @@
Copyright 2014-2019 Hippo Seven
Copyright 2020-2022 NekoInverter
Copyright 2022-2023 Tarsin Norbin
Copyright 2023 Foolbar
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+19 -22
View File
@@ -1,9 +1,9 @@
<p align="right">
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/README.md">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/en.md">
English
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-cn.md">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
简体中文
</a>
<span> | </span>
@@ -11,47 +11,47 @@
</p>
<h1 align="center">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200">
<br>EhViewer<br>
</h1>
<p align="center">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
<img src="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/FooIbar/EhViewer/releases">
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases">
<img src="https://img.shields.io/github/v/release/Ehviewer-Overhauled/ehviewer?include_prereleases" alt="Release">
</a>
<a href="https://github.com/FooIbar/EhViewer/issues">
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/issues">
<img src="https://img.shields.io/github/issues/Ehviewer-Overhauled/ehviewer" alt="Issues">
</a>
</p>
<div align="center">
<h3>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-tw.md#說明">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md#說明">
說明
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-tw.md#下載">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md#下載">
下載
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-tw.md#截圖">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md#截圖">
截圖
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-tw.md#感謝">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md#感謝">
感謝
</a>
<span> | </span>
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-tw.md#許可證書">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md#許可證書">
許可證書
</a>
</h3>
@@ -65,9 +65,7 @@
# 下載
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
# 截圖
@@ -95,7 +93,6 @@
Copyright 2014-2019 Hippo Seven
Copyright 2020-2022 NekoInverter
Copyright 2022-2023 Tarsin Norbin
Copyright 2023 Foolbar
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Binary file not shown.
Vendored
+1 -2
View File
@@ -83,8 +83,7 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
+38
View File
@@ -0,0 +1,38 @@
val ktlint by configurations.creating
dependencies {
ktlint("com.pinterest:ktlint:0.50.0") {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
}
}
// ktlint(project(":custom-ktlint-ruleset")) // in case of custom ruleset
}
val ktlintCheck by tasks.registering(JavaExec::class) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Check Kotlin code style"
classpath = ktlint
mainClass.set("com.pinterest.ktlint.Main")
// see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information
args(
"**/*.kt",
"**/*.kts",
"!**/build/**",
)
}
val ktlintFormat by tasks.registering(JavaExec::class) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Check Kotlin code style and format"
classpath = ktlint
mainClass.set("com.pinterest.ktlint.Main")
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
// see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information
args(
"-F",
"**/*.kt",
"**/*.kts",
"!**/build/**",
)
}
+2 -1
View File
@@ -4,7 +4,8 @@
],
"automerge": true,
"automergeType": "branch",
"automergeType": "pr",
"automergeStrategy": "squash",
"extends": [
"config:base"
+1 -1
View File
@@ -19,5 +19,5 @@ dependencyResolutionManagement {
}
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "EhViewer"
rootProject.name = "Ehviewer"
include(":app")