Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75bec4314e | ||
|
|
b767c041b9 |
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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.10404224" --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
|
||||
|
||||
@@ -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.10404224" --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
|
||||
@@ -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.10404224" --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:
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
+29
-41
@@ -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.10404224"
|
||||
|
||||
splits {
|
||||
abi {
|
||||
@@ -65,7 +64,7 @@ android {
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 180043
|
||||
versionName = "1.8.9.7"
|
||||
versionName = "1.8.9.0-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", "\"\"")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +159,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.3"
|
||||
kotlinCompilerExtensionVersion = "1.5.1"
|
||||
}
|
||||
|
||||
namespace = "com.hippo.ehviewer"
|
||||
@@ -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-alpha06")
|
||||
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-alpha02")
|
||||
// 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.0"
|
||||
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-alpha03"
|
||||
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.31.6-rc"
|
||||
implementation("com.google.accompanist:accompanist-themeadapter-material3:$accompanist_version")
|
||||
implementation("com.google.accompanist:accompanist-webview:$accompanist_version")
|
||||
implementation("com.google.android.material:material:1.11.0-alpha01")
|
||||
|
||||
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,11 +246,14 @@ 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")
|
||||
|
||||
val serialization = "1.6.0"
|
||||
implementation("org.chromium.net:cronet-api:113.5672.61")
|
||||
implementation("com.google.android.gms:play-services-cronet:18.0.1")
|
||||
|
||||
val serialization = "1.5.1"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:$serialization")
|
||||
@@ -294,23 +295,10 @@ 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")
|
||||
// fix mergeDebugJniLibFolders UP-TO-DATE
|
||||
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
|
||||
inputs.dir(buildDir.resolve("rustJniLibs/android"))
|
||||
}
|
||||
}
|
||||
Vendored
-1
@@ -17,7 +17,6 @@
|
||||
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
|
||||
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
|
||||
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
|
||||
-keep class com.hippo.ehviewer.ui.scene.SearchBarScene { getBinding(); }
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
/nettle
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Submodule
+1
Submodule app/src/main/cpp/nettle/nettle added at 990abad16c.
@@ -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
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.navigation.NavOptions
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
|
||||
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
|
||||
val builder = NavOptions.Builder().setLaunchSingleTop(true)
|
||||
if (
|
||||
navController.currentDestination!!.parent!!.findNode(item.itemId)
|
||||
|
||||
@@ -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,9 +60,9 @@ object Settings : DataStorePreferences(null) {
|
||||
var removeImageFiles by boolPref("include_pic", true)
|
||||
var needSignIn by boolPref("need_sign_in", true)
|
||||
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
|
||||
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
||||
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
||||
var downloadOriginImage by boolPref("download_origin_image", false)
|
||||
var enableQuic by boolPref("enable_quic", true)
|
||||
var thumbSizeDp by intPref("thumb_size_", 120)
|
||||
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
|
||||
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
||||
@@ -95,6 +95,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
|
||||
|
||||
@@ -70,8 +70,8 @@ data class ListUrlBuilder(
|
||||
mPrev = index?.takeUnless { isNext }
|
||||
}
|
||||
|
||||
fun setJumpTo(jumpTo: Int) {
|
||||
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
|
||||
fun setJumpTo(jumpTo: String?) {
|
||||
mJumpTo = jumpTo
|
||||
}
|
||||
|
||||
var keyword: String?
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
|
||||
object GalleryListParser {
|
||||
suspend fun parse(body: String) = runCatching {
|
||||
@@ -18,21 +15,13 @@ object GalleryListParser {
|
||||
it.generateSLang()
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
if (body.contains("<p>You do not have any watched tags")) {
|
||||
throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
}
|
||||
if (body.contains("No hits found</p>")) {
|
||||
throw EhException(R.string.gallery_list_empty_hit)
|
||||
}
|
||||
throw ParseException("Can't parse gallery list", it)
|
||||
}
|
||||
}.getOrDefault(GalleryListResult())
|
||||
}
|
||||
|
||||
class GalleryListResult(
|
||||
val prev: String?,
|
||||
val next: String?,
|
||||
val galleryInfoList: ArrayList<BaseGalleryInfo>,
|
||||
val prev: String? = null,
|
||||
val next: String? = null,
|
||||
val galleryInfoList: ArrayList<BaseGalleryInfo> = arrayListOf(),
|
||||
)
|
||||
|
||||
private external fun parseGalleryInfoList(e: String): GalleryListResult
|
||||
+1
-1
@@ -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,26 @@ 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)
|
||||
setUserAgent(CHROME_USER_AGENT)
|
||||
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
|
||||
addQuicHint("e-hentai.org", 443, 443)
|
||||
addQuicHint("forums.e-hentai.org", 443, 443)
|
||||
addQuicHint("exhentai.org", 443, 443)
|
||||
}.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,19 +66,16 @@ 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("User-Agent", CHROME_USER_AGENT)
|
||||
addHeader("Accept", CHROME_ACCEPT)
|
||||
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
|
||||
referer?.let { addHeader("Referer", it) }
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.time.Instant
|
||||
),
|
||||
],
|
||||
)
|
||||
data class DownloadEntity(
|
||||
class DownloadEntity(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "GID")
|
||||
var gid: Long,
|
||||
@@ -44,23 +44,21 @@ data class DownloadEntity(
|
||||
|
||||
@ColumnInfo(name = "POSITION")
|
||||
override var position: Int = 0,
|
||||
|
||||
@Ignore
|
||||
override var speed: Long = 0,
|
||||
|
||||
@Ignore
|
||||
override var remaining: Long = 0,
|
||||
|
||||
@Ignore
|
||||
override var finished: Int = 0,
|
||||
|
||||
@Ignore
|
||||
override var downloaded: Int = 0,
|
||||
|
||||
@Ignore
|
||||
override var total: Int = 0,
|
||||
) : AbstractDownloadInfo {
|
||||
constructor() : this(0)
|
||||
@Ignore
|
||||
override var speed: Long = 0
|
||||
|
||||
@Ignore
|
||||
override var remaining: Long = 0
|
||||
|
||||
@Ignore
|
||||
override var finished = 0
|
||||
|
||||
@Ignore
|
||||
override var downloaded = 0
|
||||
|
||||
@Ignore
|
||||
override var total = 0
|
||||
}
|
||||
|
||||
data class DownloadInfo(
|
||||
|
||||
@@ -71,7 +71,7 @@ object DownloadManager : OnSpiderListener {
|
||||
// Store download info wait to start
|
||||
private val mWaitList = LinkedList<DownloadInfo>()
|
||||
private val mSpeedReminder = SpeedReminder()
|
||||
private var mDownloadInfoListener: DownloadInfoListener? = null
|
||||
private val mDownloadInfoListeners = mutableListOf<DownloadInfoListener>()
|
||||
private val mNotifyTaskPool = ConcurrentPool<NotifyTask?>(5)
|
||||
private var mDownloadListener: DownloadListener? = null
|
||||
private var mCurrentTask: DownloadInfo? = null
|
||||
@@ -125,6 +125,14 @@ object DownloadManager : OnSpiderListener {
|
||||
return info?.state ?: DownloadInfo.STATE_INVALID
|
||||
}
|
||||
|
||||
fun addDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
|
||||
mDownloadInfoListeners.add(downloadInfoListener)
|
||||
}
|
||||
|
||||
fun removeDownloadInfoListener(downloadInfoListener: DownloadInfoListener) {
|
||||
mDownloadInfoListeners.remove(downloadInfoListener)
|
||||
}
|
||||
|
||||
fun setDownloadListener(listener: DownloadListener?) {
|
||||
mDownloadListener = listener
|
||||
}
|
||||
@@ -158,7 +166,12 @@ object DownloadManager : OnSpiderListener {
|
||||
mDownloadListener!!.onStart(info)
|
||||
}
|
||||
// Notify state update
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
||||
if (list != null) {
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdate(info, list)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +192,12 @@ object DownloadManager : OnSpiderListener {
|
||||
// Update in DB
|
||||
EhDB.putDownloadInfo(info)
|
||||
// Notify state update
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
||||
if (list != null) {
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdate(info, list)
|
||||
}
|
||||
}
|
||||
// Make sure download is running
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -209,7 +227,9 @@ object DownloadManager : OnSpiderListener {
|
||||
EhDB.putDownloadInfo(info)
|
||||
|
||||
// Notify
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onAdd(info, list, list.size - 1)
|
||||
}
|
||||
// Make sure download is running
|
||||
ensureDownload()
|
||||
|
||||
@@ -239,7 +259,9 @@ object DownloadManager : OnSpiderListener {
|
||||
}
|
||||
if (update) {
|
||||
// Notify Listener
|
||||
mDownloadInfoListener?.onUpdateAll()
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdateAll()
|
||||
}
|
||||
// Ensure download
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -249,23 +271,33 @@ object DownloadManager : OnSpiderListener {
|
||||
|
||||
private val _stateFlow = callbackFlow {
|
||||
val listener = object : DownloadInfoListener {
|
||||
override fun onUpdate(info: DownloadInfo) {
|
||||
trySend(info.gid)
|
||||
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
|
||||
trySend(info)
|
||||
}
|
||||
|
||||
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
|
||||
trySend(info)
|
||||
}
|
||||
|
||||
override fun onUpdateAll() {
|
||||
trySend(0)
|
||||
// No-op
|
||||
}
|
||||
|
||||
override fun onReload() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
|
||||
trySend(info)
|
||||
}
|
||||
}
|
||||
mDownloadInfoListener = listener
|
||||
addDownloadInfoListener(listener)
|
||||
awaitClose {
|
||||
mDownloadInfoListener = null
|
||||
removeDownloadInfoListener(listener)
|
||||
}
|
||||
}.shareIn(callbackFlowScope, SharingStarted.Eagerly)
|
||||
|
||||
fun stateFlow(gid: Long) = _stateFlow.filter { it == gid }
|
||||
|
||||
fun stateFlow() = _stateFlow
|
||||
fun stateFlow(gid: Long) = _stateFlow.filter { it.gid == gid }
|
||||
|
||||
suspend fun startAllDownload() {
|
||||
var update = false
|
||||
@@ -283,7 +315,9 @@ object DownloadManager : OnSpiderListener {
|
||||
}
|
||||
if (update) {
|
||||
// Notify Listener
|
||||
mDownloadInfoListener?.onUpdateAll()
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdateAll()
|
||||
}
|
||||
// Ensure download
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -319,7 +353,9 @@ object DownloadManager : OnSpiderListener {
|
||||
|
||||
// Notify
|
||||
if (notify) {
|
||||
mDownloadInfoListener?.onUpdateAll()
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onReload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +386,9 @@ object DownloadManager : OnSpiderListener {
|
||||
|
||||
// Notify
|
||||
withUIContext {
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onAdd(info, list, list.size - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +396,12 @@ object DownloadManager : OnSpiderListener {
|
||||
val info = stopDownloadInternal(gid)
|
||||
if (info != null) {
|
||||
// Update listener
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
||||
if (list != null) {
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdate(info, list)
|
||||
}
|
||||
}
|
||||
// Ensure download
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -368,7 +411,12 @@ object DownloadManager : OnSpiderListener {
|
||||
val info = stopCurrentDownloadInternal()
|
||||
if (info != null) {
|
||||
// Update listener
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
||||
if (list != null) {
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdate(info, list)
|
||||
}
|
||||
}
|
||||
// Ensure download
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -378,7 +426,9 @@ object DownloadManager : OnSpiderListener {
|
||||
stopRangeDownloadInternal(gidList)
|
||||
|
||||
// Update listener
|
||||
mDownloadInfoListener?.onUpdateAll()
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdateAll()
|
||||
}
|
||||
|
||||
// Ensure download
|
||||
ensureDownload()
|
||||
@@ -397,7 +447,9 @@ object DownloadManager : OnSpiderListener {
|
||||
stopCurrentDownloadInternal()
|
||||
|
||||
// Notify mDownloadInfoListener
|
||||
mDownloadInfoListener?.onUpdateAll()
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdateAll()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteDownload(gid: Long, deleteFiles: Boolean = false) {
|
||||
@@ -418,7 +470,9 @@ object DownloadManager : OnSpiderListener {
|
||||
if (index >= 0) {
|
||||
list.remove(info)
|
||||
// Update listener
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onRemove(info, list, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,13 +495,15 @@ object DownloadManager : OnSpiderListener {
|
||||
allInfoList.removeAll(list.toSet())
|
||||
|
||||
// Update listener
|
||||
mDownloadInfoListener?.onUpdateAll()
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onReload()
|
||||
}
|
||||
|
||||
// Ensure download
|
||||
ensureDownload()
|
||||
}
|
||||
|
||||
private fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
|
||||
fun moveDownload(fromPosition: Int, toPosition: Int): List<DownloadInfo> {
|
||||
val info = allInfoList.removeAt(fromPosition)
|
||||
allInfoList.add(toPosition, info)
|
||||
val range = if (fromPosition < toPosition) fromPosition..toPosition else toPosition..fromPosition
|
||||
@@ -459,15 +515,12 @@ object DownloadManager : OnSpiderListener {
|
||||
return list
|
||||
}
|
||||
|
||||
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()
|
||||
fun moveDownload(label: String?, fromPosition: Int, toPosition: Int) =
|
||||
getInfoListForLabel(label)!!.let {
|
||||
val absFromPosition = allInfoList.indexOf(it[fromPosition])
|
||||
val absToPosition = allInfoList.indexOf(it[toPosition])
|
||||
moveDownload(absFromPosition, absToPosition)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resetAllReadingProgress() = coroutineScope {
|
||||
allInfoList.forEach { info ->
|
||||
@@ -605,7 +658,9 @@ object DownloadManager : OnSpiderListener {
|
||||
// Save to DB
|
||||
EhDB.putDownloadInfo(info)
|
||||
}
|
||||
mDownloadInfoListener?.onUpdateAll()
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onReload()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addLabel(label: String?) {
|
||||
@@ -755,15 +810,30 @@ object DownloadManager : OnSpiderListener {
|
||||
}
|
||||
|
||||
interface DownloadInfoListener {
|
||||
/**
|
||||
* Add the special info to the special position
|
||||
*/
|
||||
fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int)
|
||||
|
||||
/**
|
||||
* The special info is changed
|
||||
*/
|
||||
fun onUpdate(info: DownloadInfo)
|
||||
fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>)
|
||||
|
||||
/**
|
||||
* Maybe all data is changed, but size is the same
|
||||
*/
|
||||
fun onUpdateAll()
|
||||
|
||||
/**
|
||||
* Maybe all data is changed, maybe list is changed
|
||||
*/
|
||||
fun onReload()
|
||||
|
||||
/**
|
||||
* Remove the special info from the special position
|
||||
*/
|
||||
fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int)
|
||||
}
|
||||
|
||||
interface DownloadListener {
|
||||
@@ -870,7 +940,12 @@ object DownloadManager : OnSpiderListener {
|
||||
Log.e(TAG, "Current task is null, but it should not be")
|
||||
} else {
|
||||
info.total = mPages
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
||||
if (list != null) {
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdate(info, list)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -899,7 +974,12 @@ object DownloadManager : OnSpiderListener {
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener!!.onGetPage(info)
|
||||
}
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
||||
if (list != null) {
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdate(info, list)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,7 +992,12 @@ object DownloadManager : OnSpiderListener {
|
||||
info.finished = mFinished
|
||||
info.downloaded = mDownloaded
|
||||
info.total = mTotal
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
||||
if (list != null) {
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdate(info, list)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -951,7 +1036,12 @@ object DownloadManager : OnSpiderListener {
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener!!.onFinish(info)
|
||||
}
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
||||
if (list != null) {
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdate(info, list)
|
||||
}
|
||||
}
|
||||
// Start next download
|
||||
ensureDownload()
|
||||
}
|
||||
@@ -1037,7 +1127,12 @@ object DownloadManager : OnSpiderListener {
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener!!.onDownload(info)
|
||||
}
|
||||
mDownloadInfoListener?.onUpdate(info)
|
||||
val list: List<DownloadInfo>? = getInfoListForLabel(info.label)
|
||||
if (list != null) {
|
||||
for (l in mDownloadInfoListeners) {
|
||||
l.onUpdate(info, list)
|
||||
}
|
||||
}
|
||||
}
|
||||
mBytesRead = 0
|
||||
if (!mStop) {
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -20,8 +20,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
@@ -30,12 +28,7 @@ import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material.icons.filled.HeartBroken
|
||||
import androidx.compose.material.icons.filled.MenuBook
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.hippo.ehviewer.EhDB
|
||||
@@ -62,7 +55,6 @@ import com.hippo.ehviewer.util.requestPermission
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -320,6 +312,7 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
|
||||
1 -> withUIContext {
|
||||
if (downloaded) {
|
||||
confirmRemoveDownload(info)
|
||||
DownloadManager.deleteDownload(info.gid)
|
||||
} else {
|
||||
CommonOperations.startDownload(this@with, info, false)
|
||||
}
|
||||
@@ -349,28 +342,12 @@ suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Cont
|
||||
|
||||
private const val MAX_FAVNOTE_CHAR = 200
|
||||
|
||||
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo, onDismiss: () -> Unit = {}) {
|
||||
var checked by mutableStateOf(Settings.removeImageFiles)
|
||||
awaitPermissionOrCancel(
|
||||
confirmText = android.R.string.ok,
|
||||
dismissText = android.R.string.cancel,
|
||||
title = R.string.download_remove_dialog_title,
|
||||
onDismiss = onDismiss,
|
||||
text = {
|
||||
Column {
|
||||
Text(text = stringResource(id = R.string.download_remove_dialog_message, info.title.orEmpty()))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = checked, onCheckedChange = { checked = it })
|
||||
Text(text = stringResource(id = R.string.download_remove_dialog_check_text))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
Settings.removeImageFiles = checked
|
||||
withIOContext {
|
||||
DownloadManager.deleteDownload(info.gid, checked)
|
||||
}
|
||||
}
|
||||
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo) = awaitPermissionOrCancel(
|
||||
confirmText = android.R.string.ok,
|
||||
dismissText = android.R.string.cancel,
|
||||
title = R.string.download_remove_dialog_title,
|
||||
text = { Text(text = stringResource(id = R.string.download_remove_dialog_message, info.title.orEmpty())) },
|
||||
)
|
||||
|
||||
suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) {
|
||||
val defaultLabel = appCtx.getString(R.string.default_download_label_name)
|
||||
|
||||
@@ -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)) },
|
||||
|
||||
@@ -197,7 +197,7 @@ class MainActivity : EhActivity() {
|
||||
// Trick: Tweak NavigationUI to disable multiple backstack
|
||||
binding.navView.setNavigationItemSelectedListener {
|
||||
val navigationView = binding.navView
|
||||
val handled = navigationView.checkedItem?.itemId == it.itemId || onNavDestinationSelected2(it, navController)
|
||||
val handled = onNavDestinationSelected2(it, navController)
|
||||
if (handled) {
|
||||
val parent = navigationView.parent
|
||||
if (parent is Openable) {
|
||||
@@ -398,10 +398,6 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun clearNavCheckedItem() {
|
||||
binding.navView.setCheckedItem(R.id.nav_stub)
|
||||
}
|
||||
|
||||
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
|
||||
showTip(getString(id), length, useToast)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -26,10 +26,7 @@ import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.SoftwareKeyboardControllerCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
import com.google.android.material.sidesheet.SideSheetDialog
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
@@ -156,15 +153,10 @@ 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)
|
||||
setViewTreeViewModelStoreOwner(owner as ViewModelStoreOwner)
|
||||
setViewTreeSavedStateRegistryOwner(owner as SavedStateRegistryOwner)
|
||||
setViewTreeLifecycleOwner(viewLifecycleOwner)
|
||||
setViewTreeSavedStateRegistryOwner(this@BaseScene)
|
||||
}
|
||||
setContentView(it)
|
||||
}
|
||||
|
||||
@@ -46,9 +46,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -56,11 +54,11 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import arrow.core.partially1
|
||||
import coil.compose.AsyncImage
|
||||
@@ -81,11 +79,11 @@ import com.hippo.ehviewer.databinding.ItemDownloadBinding
|
||||
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
|
||||
import com.hippo.ehviewer.databinding.SceneDownloadBinding
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
|
||||
import com.hippo.ehviewer.download.DownloadService
|
||||
import com.hippo.ehviewer.download.DownloadService.Companion.clear
|
||||
import com.hippo.ehviewer.download.downloadDir
|
||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||
import com.hippo.ehviewer.ui.confirmRemoveDownload
|
||||
import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager
|
||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.CheckBoxDialogBuilder
|
||||
@@ -100,10 +98,8 @@ import com.hippo.ehviewer.ui.main.requestOf
|
||||
import com.hippo.ehviewer.ui.navToReader
|
||||
import com.hippo.ehviewer.ui.setMD3Content
|
||||
import com.hippo.ehviewer.ui.tools.CropDefaults
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.LongList
|
||||
import com.hippo.ehviewer.util.containsIgnoreCase
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
@@ -112,21 +108,20 @@ import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.pxToDp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import rikka.core.res.resolveColor
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
class DownloadsScene :
|
||||
SearchBarScene(),
|
||||
BaseToolbarScene(),
|
||||
DownloadInfoListener,
|
||||
OnClickFabListener {
|
||||
/*---------------
|
||||
Whole life cycle
|
||||
---------------*/
|
||||
private var mKeyword: String? = null
|
||||
private var mLabel: String? = null
|
||||
private val mList get() = mAdapter?.currentList
|
||||
private var mList: List<DownloadInfo>? = null
|
||||
|
||||
/*---------------
|
||||
View life cycle
|
||||
@@ -137,23 +132,18 @@ class DownloadsScene :
|
||||
private var mAdapter: DownloadAdapter? = null
|
||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||
private val tracker get() = mAdapter!!.tracker!!
|
||||
private var mGid = -1L
|
||||
private var mInitPosition = -1
|
||||
private var mLabelAdapter: DownloadLabelAdapter? = null
|
||||
private lateinit var mLabels: MutableList<String>
|
||||
private var mType = -1
|
||||
|
||||
private val dialogState = DialogState()
|
||||
|
||||
override val fabLayout get() = binding.fabLayout
|
||||
|
||||
private fun initLabels() {
|
||||
context ?: return
|
||||
val listLabel = downloadManager.labelList
|
||||
mLabels = ArrayList(listLabel.size + LABEL_OFFSET)
|
||||
// Add "All" and "Default" label names
|
||||
mLabels = arrayListOf(
|
||||
getString(R.string.download_all),
|
||||
getString(R.string.default_download_label_name),
|
||||
)
|
||||
mLabels.add(getString(R.string.download_all))
|
||||
mLabels.add(getString(R.string.default_download_label_name))
|
||||
listLabel.forEach {
|
||||
mLabels.add(it.label)
|
||||
}
|
||||
@@ -171,7 +161,18 @@ class DownloadsScene :
|
||||
val info = DownloadManager.getDownloadInfo(gid)
|
||||
if (null != info) {
|
||||
mLabel = info.label
|
||||
mGid = gid
|
||||
updateForLabel()
|
||||
updateView()
|
||||
|
||||
// Get position
|
||||
if (null != mList) {
|
||||
val position = mList!!.indexOf(info)
|
||||
if (position >= 0 && null != _binding) {
|
||||
binding.recyclerView.scrollToPosition(position)
|
||||
} else {
|
||||
mInitPosition = position
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -180,6 +181,7 @@ class DownloadsScene :
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
DownloadManager.addDownloadInfoListener(this)
|
||||
if (savedInstanceState == null) {
|
||||
onInit()
|
||||
} else {
|
||||
@@ -187,54 +189,50 @@ class DownloadsScene :
|
||||
}
|
||||
}
|
||||
|
||||
private fun filter(info: DownloadInfo) = info.run {
|
||||
(mType == -1 || state == mType) &&
|
||||
mKeyword?.let {
|
||||
title.containsIgnoreCase(it) || titleJpn.containsIgnoreCase(it) ||
|
||||
uploader.containsIgnoreCase(it)
|
||||
} ?: true
|
||||
}
|
||||
|
||||
private fun updateInfoList() {
|
||||
mAdapter?.run {
|
||||
val list = when (mLabel) {
|
||||
null -> DownloadManager.allInfoList
|
||||
getString(R.string.default_download_label_name) -> DownloadManager.defaultInfoList
|
||||
else -> DownloadManager.getLabelDownloadInfoList(mLabel)
|
||||
?: DownloadManager.allInfoList.also { mLabel = null }
|
||||
}.mapNotNull { it.takeIf(::filter)?.copy(downloadInfo = it.downloadInfo.copy()) }
|
||||
submitList(list) {
|
||||
if (mGid != -1L) {
|
||||
val position = list.indexOfFirst { it.gid == mGid }
|
||||
if (position != -1) {
|
||||
binding.recyclerView.scrollToPosition(position)
|
||||
}
|
||||
mGid = -1L
|
||||
}
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
mList = null
|
||||
DownloadManager.removeDownloadInfoListener(this)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun updateForLabel() {
|
||||
updateInfoList()
|
||||
val list = when (mLabel) {
|
||||
null -> DownloadManager.allInfoList
|
||||
getString(R.string.default_download_label_name) -> DownloadManager.defaultInfoList
|
||||
else -> DownloadManager.getLabelDownloadInfoList(mLabel)
|
||||
?: DownloadManager.allInfoList.also { mLabel = null }
|
||||
}
|
||||
mList = if (mType != -1) {
|
||||
list.filter { it.state == mType }
|
||||
} else {
|
||||
list
|
||||
}
|
||||
if (mAdapter != null) {
|
||||
mAdapter!!.notifyDataSetChanged()
|
||||
}
|
||||
Settings.recentDownloadLabel = mLabel
|
||||
}
|
||||
|
||||
private fun updateTitle() {
|
||||
val title = getString(R.string.scene_download_title, mLabel ?: getString(R.string.download_all))
|
||||
setSearchBarHint(title)
|
||||
setEditTextHint(getString(R.string.search_bar_hint, title))
|
||||
setTitle(
|
||||
getString(
|
||||
R.string.scene_download_title,
|
||||
if (mLabel != null) mLabel else getString(R.string.download_all),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onInit() {
|
||||
if (!handleArguments(arguments)) {
|
||||
mLabel = Settings.recentDownloadLabel
|
||||
updateForLabel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRestore(savedInstanceState: Bundle) {
|
||||
mLabel = savedInstanceState.getString(KEY_LABEL)
|
||||
updateForLabel()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -249,7 +247,6 @@ class DownloadsScene :
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = SceneDownloadBinding.inflate(inflater, container!!)
|
||||
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
|
||||
binding.run {
|
||||
setLiftOnScrollTargetView(recyclerView)
|
||||
mViewTransition = ViewTransition(content, tip)
|
||||
@@ -291,6 +288,11 @@ class DownloadsScene :
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.clipToPadding = false
|
||||
recyclerView.clipChildren = false
|
||||
// Cancel change animation
|
||||
val itemAnimator = recyclerView.itemAnimator
|
||||
if (itemAnimator is SimpleItemAnimator) {
|
||||
itemAnimator.supportsChangeAnimations = false
|
||||
}
|
||||
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
|
||||
val decoration = object : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
@@ -298,14 +300,20 @@ class DownloadsScene :
|
||||
}
|
||||
}
|
||||
recyclerView.addItemDecoration(decoration)
|
||||
if (mInitPosition >= 0) {
|
||||
recyclerView.scrollToPosition(mInitPosition)
|
||||
mInitPosition = -1
|
||||
}
|
||||
mItemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
if (actionState == ACTION_STATE_DRAG) {
|
||||
tracker.clearSelection()
|
||||
}
|
||||
}
|
||||
override fun isLongPressDragEnabled() = false
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
return if (tracker.isInCustomChoice) {
|
||||
0
|
||||
} else {
|
||||
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT)
|
||||
}
|
||||
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
|
||||
}
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
@@ -317,24 +325,18 @@ class DownloadsScene :
|
||||
if (fromPosition == toPosition) {
|
||||
return false
|
||||
}
|
||||
val fromItem = mList!![fromPosition]
|
||||
val toItem = mList!![toPosition]
|
||||
val list = DownloadManager.moveDownload(fromItem, toItem)
|
||||
updateInfoList()
|
||||
val list = when (mLabel) {
|
||||
null -> DownloadManager.moveDownload(fromPosition, toPosition)
|
||||
getString(R.string.default_download_label_name) -> DownloadManager.moveDownload(null, fromPosition, toPosition)
|
||||
else -> DownloadManager.moveDownload(mLabel, fromPosition, toPosition)
|
||||
}
|
||||
mAdapter!!.notifyItemMoved(fromPosition, toPosition)
|
||||
lifecycleScope.launchIO {
|
||||
EhDB.updateDownloadInfo(list)
|
||||
}
|
||||
return true
|
||||
}
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
val info = mList!![position]
|
||||
lifecycleScope.launchUI {
|
||||
dialogState.confirmRemoveDownload(info) {
|
||||
mAdapter!!.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
||||
})
|
||||
mItemTouchHelper!!.attachToRecyclerView(recyclerView)
|
||||
fastScroller.attachToRecyclerView(recyclerView)
|
||||
@@ -347,23 +349,15 @@ class DownloadsScene :
|
||||
fabLayout.setAutoCancel(false)
|
||||
fabLayout.setOnClickFabListener(this@DownloadsScene)
|
||||
addAboveSnackView(fabLayout)
|
||||
updateForLabel()
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
DownloadManager.stateFlow().collectLatest {
|
||||
updateInfoList()
|
||||
}
|
||||
}
|
||||
setOnApplySearch {
|
||||
mKeyword = it.takeUnless { it.isEmpty() }
|
||||
updateInfoList()
|
||||
updateView()
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSearchViewExpanded() {
|
||||
hideActionFab(true)
|
||||
super.onSearchViewExpanded()
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
updateTitle()
|
||||
setNavigationIcon(R.drawable.ic_baseline_menu_24)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -399,7 +393,8 @@ class DownloadsScene :
|
||||
mType + 1,
|
||||
) { dialog: DialogInterface, which: Int ->
|
||||
mType = which - 1
|
||||
updateInfoList()
|
||||
updateForLabel()
|
||||
updateView()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
@@ -452,7 +447,7 @@ class DownloadsScene :
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateView() {
|
||||
fun updateView() {
|
||||
if (mViewTransition != null) {
|
||||
if (mList.isNullOrEmpty()) {
|
||||
mViewTransition!!.showView(1)
|
||||
@@ -600,6 +595,61 @@ class DownloadsScene :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
|
||||
if (mList !== list) {
|
||||
return
|
||||
}
|
||||
lifecycleScope.launchUI {
|
||||
if (mAdapter != null) {
|
||||
mAdapter!!.notifyItemInserted(position)
|
||||
}
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {
|
||||
if (mLabel != null && mList !== list) {
|
||||
return
|
||||
}
|
||||
val index = mList!!.indexOf(info)
|
||||
lifecycleScope.launchUI {
|
||||
if (index >= 0 && mAdapter != null) {
|
||||
mAdapter!!.notifyItemChanged(index, PAYLOAD_STATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onUpdateAll() {
|
||||
lifecycleScope.launchUI {
|
||||
if (mAdapter != null) {
|
||||
mAdapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onReload() {
|
||||
lifecycleScope.launchUI {
|
||||
if (mAdapter != null) {
|
||||
mAdapter!!.notifyDataSetChanged()
|
||||
}
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
|
||||
if (mList !== list) {
|
||||
return
|
||||
}
|
||||
lifecycleScope.launchUI {
|
||||
if (mAdapter != null) {
|
||||
mAdapter!!.notifyItemRemoved(position)
|
||||
}
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadLabelHolder(val binding: ItemDrawerListBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
@@ -610,14 +660,17 @@ class DownloadsScene :
|
||||
DownloadLabelHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
|
||||
holder.itemView.setOnClickListener {
|
||||
val position = holder.bindingAdapterPosition
|
||||
val label = mLabels[position].takeUnless { position == 0 }
|
||||
if (mLabel != label) {
|
||||
mKeyword = null
|
||||
setSearchBarText(null)
|
||||
mLabel = label
|
||||
val label1: String? = if (position == 0) {
|
||||
null
|
||||
} else {
|
||||
mLabels[position]
|
||||
}
|
||||
if (mLabel != label1) {
|
||||
mLabel = label1
|
||||
updateForLabel()
|
||||
updateView()
|
||||
closeSideSheet()
|
||||
}
|
||||
closeSideSheet()
|
||||
}
|
||||
holder.binding.edit.setOnClickListener {
|
||||
val context = context
|
||||
@@ -655,7 +708,7 @@ class DownloadsScene :
|
||||
}
|
||||
holder.binding.run {
|
||||
if (list != null) {
|
||||
text.text = "$label [${list.size}]"
|
||||
text.text = label + " [" + list.size + "]"
|
||||
} else {
|
||||
text.text = label
|
||||
}
|
||||
@@ -810,7 +863,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,
|
||||
)
|
||||
@@ -900,23 +952,17 @@ class DownloadsScene :
|
||||
}
|
||||
}
|
||||
|
||||
private val diffCallback = object : DiffUtil.ItemCallback<DownloadInfo>() {
|
||||
override fun areItemsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
|
||||
override fun areContentsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem == newItem
|
||||
override fun getChangePayload(oldItem: DownloadInfo, newItem: DownloadInfo): Any? {
|
||||
return if (oldItem.downloadInfo != newItem.downloadInfo) {
|
||||
PAYLOAD_DOWNLOAD_INFO
|
||||
} else {
|
||||
super.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DownloadAdapter : ListAdapter<DownloadInfo, DownloadHolder>(diffCallback) {
|
||||
private inner class DownloadAdapter : RecyclerView.Adapter<DownloadHolder>() {
|
||||
private val mInflater: LayoutInflater = layoutInflater
|
||||
var tracker: GallerySelectionTracker<DownloadInfo>? = null
|
||||
|
||||
override fun getItemId(position: Int) = getItem(position).gid
|
||||
override fun getItemId(position: Int): Long {
|
||||
return if (mList == null || position < 0 || position >= mList!!.size) {
|
||||
0
|
||||
} else {
|
||||
mList!![position].gid
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
|
||||
val holder = DownloadHolder(ItemDownloadBinding.inflate(mInflater, parent, false))
|
||||
@@ -925,8 +971,10 @@ class DownloadsScene :
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
|
||||
val info = getItem(position)
|
||||
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
|
||||
mList?.let {
|
||||
val info = it[position]
|
||||
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
@@ -936,15 +984,18 @@ class DownloadsScene :
|
||||
) {
|
||||
payloads.forEach { payload ->
|
||||
when (payload) {
|
||||
PAYLOAD_DOWNLOAD_INFO -> {
|
||||
val info = getItem(position)
|
||||
holder.bindForState(info)
|
||||
PAYLOAD_STATE -> {
|
||||
mList?.let { holder.bindForState(it[position]) }
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onBindViewHolder(holder, position, payloads)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return if (mList == null) 0 else mList!!.size
|
||||
}
|
||||
}
|
||||
|
||||
private inner class RenameLabelDialogHelper(
|
||||
@@ -979,6 +1030,7 @@ class DownloadsScene :
|
||||
if (mLabel == mOriginalLabel) {
|
||||
mLabel = text
|
||||
updateForLabel()
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1067,6 +1119,7 @@ class DownloadsScene :
|
||||
withUIContext {
|
||||
mLabel = null
|
||||
updateForLabel()
|
||||
updateView()
|
||||
mLabelAdapter!!.notifyItemRemoved(position)
|
||||
}
|
||||
}
|
||||
@@ -1084,7 +1137,7 @@ class DownloadsScene :
|
||||
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
|
||||
private const val KEY_LABEL = "label"
|
||||
private const val LABEL_OFFSET = 2
|
||||
private const val PAYLOAD_DOWNLOAD_INFO = 0
|
||||
private const val PAYLOAD_STATE = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,6 @@ import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.datepicker.CalendarConstraints
|
||||
import com.google.android.material.datepicker.CalendarConstraints.DateValidator
|
||||
import com.google.android.material.datepicker.CompositeDateValidator
|
||||
@@ -80,18 +78,17 @@ import com.hippo.ehviewer.ui.legacy.HandlerDrawable
|
||||
import com.hippo.ehviewer.ui.legacy.ViewTransition
|
||||
import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper
|
||||
import com.hippo.ehviewer.ui.setMD3Content
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.SimpleHandler
|
||||
import com.hippo.ehviewer.util.getValue
|
||||
import com.hippo.ehviewer.util.lazyMut
|
||||
import com.hippo.ehviewer.util.setValue
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import rikka.core.res.resolveColor
|
||||
import java.time.Instant
|
||||
@@ -163,10 +160,6 @@ class FavoritesScene : SearchBarScene() {
|
||||
}
|
||||
}
|
||||
|
||||
private val dialogState = DialogState()
|
||||
|
||||
override val fabLayout get() = binding.fabLayout
|
||||
|
||||
private fun onItemClick(position: Int) {
|
||||
// Skip if in search mode
|
||||
if (!tracker.isInCustomChoice) {
|
||||
@@ -198,11 +191,10 @@ class FavoritesScene : SearchBarScene() {
|
||||
}
|
||||
if (keyword.isNullOrEmpty()) {
|
||||
setSearchBarHint(getString(R.string.favorites_title, favCatName))
|
||||
setSearchBarText(null)
|
||||
} else {
|
||||
setSearchBarHint(getString(R.string.favorites_title_2, favCatName, keyword))
|
||||
}
|
||||
setEditTextHint(getString(R.string.search_bar_hint, favCatName))
|
||||
setEditTextHint(getString(R.string.favorites_search_bar_hint, favCatName))
|
||||
Settings.recentFavCat = urlBuilder.favCat
|
||||
}
|
||||
|
||||
@@ -230,7 +222,6 @@ class FavoritesScene : SearchBarScene() {
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = SceneFavoritesBinding.inflate(inflater, container!!)
|
||||
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
|
||||
setOnApplySearch {
|
||||
if (!tracker.isInCustomChoice) {
|
||||
switchFav(urlBuilder.favCat, it)
|
||||
@@ -333,11 +324,11 @@ 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 {
|
||||
adapter.loadStateFlow.collectLatest {
|
||||
adapter.addLoadStateListener {
|
||||
lifecycleScope.launchUI {
|
||||
when (val state = it.refresh) {
|
||||
is LoadState.Loading -> {
|
||||
showSearchBar()
|
||||
@@ -390,40 +381,7 @@ class FavoritesScene : SearchBarScene() {
|
||||
}
|
||||
switchFav(Settings.recentFavCat)
|
||||
}
|
||||
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
return if (tracker.isInCustomChoice) {
|
||||
0
|
||||
} else {
|
||||
makeMovementFlags(0, ItemTouchHelper.LEFT)
|
||||
}
|
||||
}
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder,
|
||||
) = false
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
val info = mAdapter!!.peek(position)!!
|
||||
lifecycleScope.launchIO {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.delete,
|
||||
dismissText = android.R.string.cancel,
|
||||
title = R.string.delete_favorites_dialog_title,
|
||||
onDismiss = { mAdapter!!.notifyItemChanged(position) },
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.delete_favorites_dialog_message, 1))
|
||||
}
|
||||
if (urlBuilder.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
|
||||
EhDB.removeLocalFavorites(info)
|
||||
} else {
|
||||
EhEngine.modifyFavorites(info.gid, info.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
|
||||
allowEmptySearch = false
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -481,16 +439,6 @@ class FavoritesScene : SearchBarScene() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSearchViewExpanded() {
|
||||
hideActionFab(true)
|
||||
super.onSearchViewExpanded()
|
||||
}
|
||||
|
||||
override fun onSearchViewHidden() {
|
||||
super.onSearchViewHidden()
|
||||
showActionFab(true)
|
||||
}
|
||||
|
||||
private fun showGoToDialog() {
|
||||
context ?: return
|
||||
val local = LocalDateTime.of(2007, 3, 21, 0, 0)
|
||||
@@ -580,3 +528,5 @@ class FavoritesScene : SearchBarScene() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val ANIMATE_TIME = 300L
|
||||
@@ -68,6 +68,7 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
@@ -105,7 +106,6 @@ import arrow.core.partially1
|
||||
import coil.imageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
|
||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -123,7 +123,6 @@ import com.hippo.ehviewer.client.data.GalleryTagGroup
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||
import com.hippo.ehviewer.client.getImageKey
|
||||
import com.hippo.ehviewer.client.parser.ArchiveParser
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.client.parser.TorrentResult
|
||||
@@ -160,7 +159,6 @@ import com.hippo.ehviewer.ui.setMD3Content
|
||||
import com.hippo.ehviewer.ui.tools.CrystalCard
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconButton
|
||||
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconToggleButton
|
||||
import com.hippo.ehviewer.ui.tools.GalleryDetailRating
|
||||
import com.hippo.ehviewer.util.AppHelper
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
@@ -343,24 +341,6 @@ class GalleryDetailScene : BaseScene() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun actionClearCache() {
|
||||
val gd = composeBindingGD ?: return
|
||||
lifecycleScope.launchIO {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.clear_all,
|
||||
dismissText = android.R.string.cancel,
|
||||
title = R.string.clear_image_cache,
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.clear_image_cache_confirm))
|
||||
}
|
||||
(0..<gd.pages).forEach {
|
||||
val key = getImageKey(gd.gid, it)
|
||||
imageCache.remove(key)
|
||||
}
|
||||
showTip(R.string.image_cache_cleared, LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun actionOpenInOtherApp() {
|
||||
val url = galleryDetailUrl ?: return
|
||||
val activity: Activity = mainActivity ?: return
|
||||
@@ -486,13 +466,6 @@ class GalleryDetailScene : BaseScene() {
|
||||
actionRefresh()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.clear_image_cache)) },
|
||||
onClick = {
|
||||
dropdown = false
|
||||
actionClearCache()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(id = R.string.open_in_other_app)) },
|
||||
onClick = {
|
||||
@@ -739,7 +712,7 @@ class GalleryDetailScene : BaseScene() {
|
||||
stringResource(id = R.string.not_favorited)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
FilledTertiaryIconToggleButton(
|
||||
FilledTonalIconToggleButton(
|
||||
checked = favored,
|
||||
onCheckedChange = { modifyFavourite() },
|
||||
) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.ui.scene
|
||||
|
||||
import android.animation.Animator
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
@@ -27,7 +28,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 +40,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
|
||||
@@ -67,12 +66,10 @@ import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_IMAGE_SEARCH
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_NORMAL
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_SUBSCRIPTION
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TAG
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_UPLOADER
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT
|
||||
import com.hippo.ehviewer.client.exception.CloudflareBypassException
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
@@ -101,11 +98,13 @@ import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.updater.AppUpdater
|
||||
import com.hippo.ehviewer.util.AnimationUtils
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.SimpleAnimatorListener
|
||||
import com.hippo.ehviewer.util.getParcelableCompat
|
||||
import com.hippo.ehviewer.util.getValue
|
||||
import com.hippo.ehviewer.util.lazyMut
|
||||
import com.hippo.ehviewer.util.setValue
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -131,10 +130,10 @@ class VMStorage1 : ViewModel() {
|
||||
if (urlBuilder.mode == MODE_TOPLIST) {
|
||||
// TODO: Since we know total pages, let pager support jump
|
||||
val key = (params.key ?: urlBuilder.mJumpTo ?: "0").toInt()
|
||||
val prev = (key - 1).takeIf { it > 0 }
|
||||
val next = (key + 1).takeIf { it < TOPLIST_PAGES }
|
||||
val prev = if (key != 0) key - 1 else null
|
||||
val next = if (key != 199) key + 1 else null
|
||||
runSuspendCatching {
|
||||
urlBuilder.setJumpTo(key)
|
||||
urlBuilder.setJumpTo(key.toString())
|
||||
EhEngine.getGalleryList(urlBuilder.build())
|
||||
}.onFailure {
|
||||
return@withIOContext LoadResult.Error(it)
|
||||
@@ -157,7 +156,7 @@ class VMStorage1 : ViewModel() {
|
||||
}
|
||||
}
|
||||
val r = runSuspendCatching {
|
||||
if (MODE_IMAGE_SEARCH == urlBuilder.mode) {
|
||||
if (ListUrlBuilder.MODE_IMAGE_SEARCH == urlBuilder.mode) {
|
||||
EhEngine.imageSearch(
|
||||
File(urlBuilder.imagePath!!),
|
||||
urlBuilder.isUseSimilarityScan,
|
||||
@@ -193,15 +192,23 @@ class GalleryListScene : SearchBarScene() {
|
||||
}
|
||||
private var _binding: SceneGalleryListBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
mSearchFab.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
private val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
binding.fabLayout.primaryFab?.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
private var fabAnimator: ViewPropertyAnimator? = null
|
||||
private var mViewTransition: ViewTransition? = null
|
||||
private var mAdapter: GalleryAdapter? = null
|
||||
lateinit var mQuickSearchList: MutableList<QuickSearch>
|
||||
private var mHideActionFabSlop = 0
|
||||
private var mShowActionFab = true
|
||||
private var mState = State.NORMAL
|
||||
|
||||
override val fabLayout get() = binding.fabLayout
|
||||
|
||||
override fun getMenuResId(): Int {
|
||||
return R.menu.scene_gallery_list_searchbar_menu
|
||||
}
|
||||
@@ -216,6 +223,7 @@ class GalleryListScene : SearchBarScene() {
|
||||
ACTION_LIST_URL_BUILDER -> args?.getParcelableCompat<ListUrlBuilder>(KEY_LIST_URL_BUILDER)?.copy() ?: ListUrlBuilder()
|
||||
else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!")
|
||||
}
|
||||
args?.getString(KEY_GOTO)?.let { mUrlBuilder.mNext = it }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -258,7 +266,10 @@ class GalleryListScene : SearchBarScene() {
|
||||
val category = mUrlBuilder.category
|
||||
val mode = mUrlBuilder.mode
|
||||
val isPopular = mode == MODE_WHATS_HOT
|
||||
mIsTopList = mode == MODE_TOPLIST
|
||||
val isTopList = mode == MODE_TOPLIST
|
||||
if (isTopList != mIsTopList) {
|
||||
mIsTopList = isTopList
|
||||
}
|
||||
|
||||
// Update fab visibility
|
||||
binding.fabLayout.setSecondaryFabVisibilityAt(0, !isPopular)
|
||||
@@ -284,14 +295,6 @@ class GalleryListScene : SearchBarScene() {
|
||||
title = resources.getString(R.string.search)
|
||||
}
|
||||
setSearchBarHint(title)
|
||||
|
||||
when (mode) {
|
||||
MODE_NORMAL, MODE_SUBSCRIPTION -> if (category != EhUtils.NONE || !keyword.isNullOrEmpty()) {
|
||||
mainActivity?.clearNavCheckedItem()
|
||||
}
|
||||
MODE_TAG, MODE_UPLOADER, MODE_IMAGE_SEARCH -> mainActivity?.clearNavCheckedItem()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private val dialogState = DialogState()
|
||||
@@ -333,8 +336,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,15 +365,12 @@ 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 {
|
||||
adapter.loadStateFlow.collectLatest {
|
||||
val noWatch = getString(R.string.gallery_list_empty_hit_subscription)
|
||||
adapter.addLoadStateListener {
|
||||
lifecycleScope.launchUI {
|
||||
when (val state = it.refresh) {
|
||||
is LoadState.Loading -> {
|
||||
showSearchBar()
|
||||
@@ -399,7 +397,11 @@ class GalleryListScene : SearchBarScene() {
|
||||
delay(500)
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
if (mAdapter?.itemCount == 0) {
|
||||
binding.tip.text = empty
|
||||
if (mUrlBuilder.mode == MODE_SUBSCRIPTION) {
|
||||
binding.tip.text = noWatch
|
||||
} else {
|
||||
binding.tip.text = empty
|
||||
}
|
||||
transition.showView(2)
|
||||
} else {
|
||||
transition.showView(0)
|
||||
@@ -450,14 +452,12 @@ class GalleryListScene : SearchBarScene() {
|
||||
override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {
|
||||
when (position) {
|
||||
0 -> showGoToDialog()
|
||||
1 -> { // First
|
||||
1 -> {
|
||||
mUrlBuilder.setIndex(null, true)
|
||||
mUrlBuilder.mJumpTo = null
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
2 -> { // Last
|
||||
2 -> {
|
||||
if (mIsTopList) {
|
||||
mUrlBuilder.mJumpTo = "${TOPLIST_PAGES - 1}"
|
||||
mAdapter?.refresh()
|
||||
} else {
|
||||
mUrlBuilder.setIndex("1", false)
|
||||
@@ -481,6 +481,7 @@ class GalleryListScene : SearchBarScene() {
|
||||
}
|
||||
binding.fabLayout.primaryFab!!.setImageDrawable(actionFabDrawable)
|
||||
addAboveSnackView(binding.fabLayout)
|
||||
mSearchFab.setOnClickListener { onApplySearch() }
|
||||
|
||||
// Update list url builder
|
||||
onUpdateUrlBuilder()
|
||||
@@ -517,15 +518,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) {
|
||||
if (ListUrlBuilder.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 +560,7 @@ class GalleryListScene : SearchBarScene() {
|
||||
}
|
||||
return@launchIO
|
||||
}
|
||||
if (checked[0]) {
|
||||
if (checked[0] && next != null) {
|
||||
text += "@$next"
|
||||
Settings.qSSaveProgress = true
|
||||
} else {
|
||||
@@ -623,22 +623,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
|
||||
}
|
||||
@@ -646,9 +644,9 @@ class GalleryListScene : SearchBarScene() {
|
||||
private fun showGoToDialog() {
|
||||
mAdapter ?: return
|
||||
if (mIsTopList) {
|
||||
val page = mUrlBuilder.mJumpTo?.toIntOrNull() ?: 0
|
||||
val pages = 200
|
||||
val title = getString(R.string.go_to)
|
||||
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
|
||||
val hint = getString(R.string.go_to_hint, 1, 200)
|
||||
lifecycleScope.launch {
|
||||
val text = dialogState.awaitInputText(
|
||||
title = title,
|
||||
@@ -661,13 +659,13 @@ class GalleryListScene : SearchBarScene() {
|
||||
}.onFailure {
|
||||
return@awaitInputText getString(R.string.error_invalid_number)
|
||||
}.getOrThrow()
|
||||
if (goTo !in 0..<TOPLIST_PAGES) {
|
||||
if (goTo < 0 || goTo >= pages) {
|
||||
getString(R.string.error_out_of_range)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.trim().toInt() - 1
|
||||
mUrlBuilder.setJumpTo(text)
|
||||
mUrlBuilder.setJumpTo(text.toString())
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
} else {
|
||||
@@ -698,21 +696,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() {
|
||||
@@ -748,6 +738,70 @@ class GalleryListScene : SearchBarScene() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectSearchFab(animation: Boolean) {
|
||||
_binding ?: return
|
||||
mShowActionFab = false
|
||||
if (animation) {
|
||||
val fab: View? = binding.fabLayout.primaryFab
|
||||
val delay: Long
|
||||
if (View.INVISIBLE == fab!!.visibility) {
|
||||
delay = 0L
|
||||
} else {
|
||||
delay = ANIMATE_TIME
|
||||
binding.fabLayout.setExpanded(expanded = false, animation = true)
|
||||
fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)
|
||||
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
||||
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
||||
}
|
||||
mSearchFab.visibility = View.VISIBLE
|
||||
mSearchFab.rotation = -45.0f
|
||||
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
|
||||
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
||||
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
||||
} else {
|
||||
binding.fabLayout.setExpanded(expanded = false, animation = false)
|
||||
val fab: View? = binding.fabLayout.primaryFab
|
||||
fab!!.visibility = View.INVISIBLE
|
||||
fab.scaleX = 0.0f
|
||||
fab.scaleY = 0.0f
|
||||
mSearchFab.visibility = View.VISIBLE
|
||||
mSearchFab.scaleX = 1.0f
|
||||
mSearchFab.scaleY = 1.0f
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectActionFab(animation: Boolean) {
|
||||
_binding ?: return
|
||||
mShowActionFab = true
|
||||
if (animation) {
|
||||
val delay: Long
|
||||
if (View.INVISIBLE == mSearchFab.visibility) {
|
||||
delay = 0L
|
||||
} else {
|
||||
delay = ANIMATE_TIME
|
||||
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
|
||||
.setListener(mSearchFabAnimatorListener)
|
||||
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
||||
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
||||
}
|
||||
val fab: View? = binding.fabLayout.primaryFab
|
||||
fab!!.visibility = View.VISIBLE
|
||||
fab.rotation = -45.0f
|
||||
fab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
|
||||
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
||||
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
||||
} else {
|
||||
binding.fabLayout.setExpanded(expanded = false, animation = false)
|
||||
val fab: View? = binding.fabLayout.primaryFab
|
||||
fab!!.visibility = View.VISIBLE
|
||||
fab.scaleX = 1.0f
|
||||
fab.scaleY = 1.0f
|
||||
mSearchFab.visibility = View.INVISIBLE
|
||||
mSearchFab.scaleX = 0.0f
|
||||
mSearchFab.scaleY = 0.0f
|
||||
}
|
||||
}
|
||||
|
||||
private fun setState(state: State) {
|
||||
setState(state, true)
|
||||
}
|
||||
@@ -809,35 +863,24 @@ class GalleryListScene : SearchBarScene() {
|
||||
private fun onApplySearch(query: String?) {
|
||||
_binding ?: return
|
||||
lifecycleScope.launchIO {
|
||||
val builder = ListUrlBuilder()
|
||||
val oldMode = mUrlBuilder.mode
|
||||
if (mState == State.SEARCH || mState == State.SEARCH_SHOW_LIST) {
|
||||
try {
|
||||
binding.searchLayout.formatListUrlBuilder(builder, query)
|
||||
binding.searchLayout.formatListUrlBuilder(mUrlBuilder, query)
|
||||
} catch (e: EhException) {
|
||||
showTip(e.message, LENGTH_LONG)
|
||||
return@launchIO
|
||||
}
|
||||
} else {
|
||||
val oldMode = mUrlBuilder.mode
|
||||
// If it's MODE_SUBSCRIPTION, keep it
|
||||
val newMode = if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else MODE_NORMAL
|
||||
builder.mode = newMode
|
||||
builder.keyword = query
|
||||
mUrlBuilder.reset()
|
||||
mUrlBuilder.mode = newMode
|
||||
mUrlBuilder.keyword = query
|
||||
}
|
||||
withUIContext {
|
||||
when (oldMode) {
|
||||
MODE_TOPLIST, MODE_WHATS_HOT -> {
|
||||
// Wait for search view to hide
|
||||
delay(300)
|
||||
navAnimated(R.id.galleryListScene, builder.toStartArgs())
|
||||
}
|
||||
|
||||
else -> {
|
||||
mUrlBuilder = builder
|
||||
onUpdateUrlBuilder()
|
||||
mAdapter?.refresh()
|
||||
}
|
||||
}
|
||||
onUpdateUrlBuilder()
|
||||
mAdapter?.refresh()
|
||||
setState(State.NORMAL)
|
||||
}
|
||||
}
|
||||
@@ -856,27 +899,26 @@ class GalleryListScene : SearchBarScene() {
|
||||
|
||||
private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) :
|
||||
RecyclerView.Adapter<QsDrawerHolder>() {
|
||||
private val keywords = intArrayOf(11, 12, 13, 15)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {
|
||||
val holder = QsDrawerHolder(ItemDrawerListBinding.inflate(mInflater, parent, false))
|
||||
holder.itemView.setOnClickListener {
|
||||
if (mUrlBuilder.mode == MODE_WHATS_HOT) {
|
||||
if (!mIsTopList) {
|
||||
holder.itemView.setOnClickListener {
|
||||
val q = mQuickSearchList[holder.bindingAdapterPosition]
|
||||
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.set(q)
|
||||
onUpdateUrlBuilder()
|
||||
mAdapter?.refresh()
|
||||
setState(State.NORMAL)
|
||||
closeSideSheet()
|
||||
}
|
||||
} else {
|
||||
val keywords = intArrayOf(11, 12, 13, 15)
|
||||
holder.itemView.setOnClickListener {
|
||||
mUrlBuilder.keyword = keywords[holder.bindingAdapterPosition].toString()
|
||||
onUpdateUrlBuilder()
|
||||
mAdapter?.refresh()
|
||||
setState(State.NORMAL)
|
||||
closeSideSheet()
|
||||
}
|
||||
setState(State.NORMAL)
|
||||
closeSideSheet()
|
||||
}
|
||||
return holder
|
||||
}
|
||||
@@ -1006,6 +1048,7 @@ class GalleryListScene : SearchBarScene() {
|
||||
|
||||
companion object {
|
||||
const val KEY_ACTION = "action"
|
||||
const val KEY_GOTO = "goto"
|
||||
const val ACTION_HOMEPAGE = "action_homepage"
|
||||
const val ACTION_SUBSCRIPTION = "action_subscription"
|
||||
const val ACTION_WHATS_HOT = "action_whats_hot"
|
||||
@@ -1013,6 +1056,7 @@ class GalleryListScene : SearchBarScene() {
|
||||
const val ACTION_LIST_URL_BUILDER = "action_list_url_builder"
|
||||
const val KEY_LIST_URL_BUILDER = "list_url_builder"
|
||||
const val KEY_STATE = "state"
|
||||
private const val ANIMATE_TIME = 300L
|
||||
fun ListUrlBuilder.toStartArgs() = bundleOf(
|
||||
KEY_ACTION to ACTION_LIST_URL_BUILDER,
|
||||
KEY_LIST_URL_BUILDER to this,
|
||||
@@ -1020,8 +1064,6 @@ class GalleryListScene : SearchBarScene() {
|
||||
}
|
||||
}
|
||||
|
||||
private const val TOPLIST_PAGES = 200
|
||||
|
||||
@Parcelize
|
||||
enum class State : Parcelable {
|
||||
NORMAL, SIMPLE_SEARCH, SEARCH, SEARCH_SHOW_LIST
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.hippo.ehviewer.ui.scene
|
||||
|
||||
import android.animation.Animator
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
@@ -11,7 +10,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
|
||||
@@ -42,10 +40,7 @@ import com.hippo.ehviewer.dao.Search
|
||||
import com.hippo.ehviewer.dao.SearchDao
|
||||
import com.hippo.ehviewer.databinding.SceneSearchbarBinding
|
||||
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
|
||||
import com.hippo.ehviewer.ui.legacy.FabLayout
|
||||
import com.hippo.ehviewer.ui.setMD3Content
|
||||
import com.hippo.ehviewer.util.AnimationUtils
|
||||
import com.hippo.ehviewer.util.SimpleAnimatorListener
|
||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||
import com.jamal.composeprefs3.ui.ifTrueThen
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
@@ -62,18 +57,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
private val mSearchDatabase = searchDatabase.searchDao()
|
||||
private var onApplySearch: (String) -> Unit = {}
|
||||
protected val mSearchFab get() = binding.searchFab
|
||||
protected abstract val fabLayout: FabLayout
|
||||
protected var mShowActionFab = true
|
||||
protected val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
fabLayout.primaryFab?.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
mSearchFab.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = SceneSearchbarBinding.inflate(inflater, container, false)
|
||||
@@ -83,7 +66,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
onApplySearch()
|
||||
true
|
||||
}
|
||||
binding.searchBarList.consumeWindowInsets = false
|
||||
binding.searchBarList.setMD3Content {
|
||||
LazyColumn(contentPadding = WindowInsets.navigationBars.asPaddingValues()) {
|
||||
items(mSuggestionList) {
|
||||
@@ -117,7 +99,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
onSearchViewHidden()
|
||||
}
|
||||
}
|
||||
mSearchFab.setOnClickListener { onApplySearch() }
|
||||
onCreateViewWithToolbar(inflater, binding.root, savedInstanceState)
|
||||
// This has to be placed after onCreateViewWithToolbar() since
|
||||
// callbacks are invoked in the reverse order in which they are added
|
||||
@@ -142,7 +123,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
mSearchViewOnBackPressedCallback.remove()
|
||||
binding.root.removeAllViews()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
@@ -167,87 +147,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
privLockModeStart = null
|
||||
}
|
||||
|
||||
fun showSearchFab(animation: Boolean, delay: Long = 0) {
|
||||
if (animation) {
|
||||
mSearchFab.visibility = View.VISIBLE
|
||||
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
|
||||
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
||||
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
||||
} else {
|
||||
mSearchFab.visibility = View.VISIBLE
|
||||
mSearchFab.scaleX = 1.0f
|
||||
mSearchFab.scaleY = 1.0f
|
||||
}
|
||||
}
|
||||
|
||||
fun hideSearchFab(animation: Boolean): Long {
|
||||
return if (View.INVISIBLE == mSearchFab.visibility) {
|
||||
0L
|
||||
} else {
|
||||
if (animation) {
|
||||
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
|
||||
.setListener(mSearchFabAnimatorListener)
|
||||
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
||||
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
||||
} else {
|
||||
mSearchFab.visibility = View.INVISIBLE
|
||||
mSearchFab.scaleX = 0.0f
|
||||
mSearchFab.scaleY = 0.0f
|
||||
}
|
||||
ANIMATE_TIME
|
||||
}
|
||||
}
|
||||
|
||||
fun showActionFab(animation: Boolean, delay: Long = ANIMATE_TIME) {
|
||||
mShowActionFab = true
|
||||
if (animation) {
|
||||
val fab = fabLayout.primaryFab!!
|
||||
fab.visibility = View.VISIBLE
|
||||
fab.animate().scaleX(1.0f).scaleY(1.0f).setListener(null)
|
||||
.setDuration(ANIMATE_TIME).setStartDelay(delay)
|
||||
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
|
||||
} else {
|
||||
fabLayout.setExpanded(expanded = false, animation = false)
|
||||
val fab = fabLayout.primaryFab!!
|
||||
fab.visibility = View.VISIBLE
|
||||
fab.scaleX = 1.0f
|
||||
fab.scaleY = 1.0f
|
||||
}
|
||||
}
|
||||
|
||||
fun hideActionFab(animation: Boolean): Long {
|
||||
mShowActionFab = false
|
||||
return if (animation) {
|
||||
val fab = fabLayout.primaryFab!!
|
||||
if (View.INVISIBLE == fab.visibility) {
|
||||
0L
|
||||
} else {
|
||||
fabLayout.setExpanded(expanded = false, animation = true)
|
||||
fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)
|
||||
.setDuration(ANIMATE_TIME).setStartDelay(0L)
|
||||
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
|
||||
ANIMATE_TIME
|
||||
}
|
||||
} else {
|
||||
fabLayout.setExpanded(expanded = false, animation = false)
|
||||
val fab = fabLayout.primaryFab!!
|
||||
fab.visibility = View.INVISIBLE
|
||||
fab.scaleX = 0.0f
|
||||
fab.scaleY = 0.0f
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun selectSearchFab(animation: Boolean) {
|
||||
val delay = hideActionFab(animation)
|
||||
showSearchFab(animation, delay)
|
||||
}
|
||||
|
||||
fun selectActionFab(animation: Boolean) {
|
||||
val delay = hideSearchFab(animation)
|
||||
showActionFab(animation, delay)
|
||||
}
|
||||
|
||||
fun setSearchBarHint(hint: String?) {
|
||||
binding.toolbar.hint = hint
|
||||
}
|
||||
@@ -430,5 +329,3 @@ fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val ANIMATE_TIME = 300L
|
||||
@@ -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),
|
||||
@@ -124,6 +126,11 @@ fun AdvancedScreen() {
|
||||
entryValueRes = R.array.app_language_entry_values,
|
||||
value = Settings::language,
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.settings_advanced_enable_quic),
|
||||
summary = stringResource(id = R.string.settings_advanced_enable_quic_summary),
|
||||
value = Settings::enableQuic,
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.preload_thumb_aggressively),
|
||||
value = Settings::preloadThumbAggressively,
|
||||
@@ -223,6 +230,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,
|
||||
@@ -191,27 +193,6 @@ fun EhScreen() {
|
||||
summary = stringResource(id = R.string.settings_my_tags_summary),
|
||||
) { navController.navigate(MYTAGS_SCREEN) }
|
||||
}
|
||||
var defaultFavSlot by Settings::defaultFavSlot.observed
|
||||
val disabled = stringResource(id = R.string.disabled_nav)
|
||||
val localFav = stringResource(id = R.string.local_favorites)
|
||||
val summary = when (defaultFavSlot) {
|
||||
-1 -> localFav
|
||||
in 0..9 -> Settings.favCat[defaultFavSlot]
|
||||
else -> stringResource(id = R.string.default_favorites_warning)
|
||||
}
|
||||
val items = if (signin) {
|
||||
arrayOf(disabled, localFav, *Settings.favCat)
|
||||
} else {
|
||||
arrayOf(disabled, localFav)
|
||||
}
|
||||
Preference(
|
||||
title = stringResource(id = R.string.default_favorites_collection),
|
||||
summary = summary,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
defaultFavSlot = dialogState.showSelectItem(*items, title = R.string.default_favorites_collection) - 2
|
||||
}
|
||||
}
|
||||
SimpleMenuPreferenceInt(
|
||||
title = stringResource(id = rikka.core.R.string.dark_theme),
|
||||
entry = R.array.night_mode_entries,
|
||||
@@ -222,10 +203,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,
|
||||
@@ -267,6 +244,27 @@ fun EhScreen() {
|
||||
entryValueRes = R.array.thumb_resolution_entry_values,
|
||||
value = thumbResolution,
|
||||
)
|
||||
var defaultFavSlot by Settings::defaultFavSlot.observed
|
||||
val disabled = stringResource(id = R.string.disabled_nav)
|
||||
val localFav = stringResource(id = R.string.local_favorites)
|
||||
val summary = when (defaultFavSlot) {
|
||||
-1 -> localFav
|
||||
in 0..9 -> Settings.favCat[defaultFavSlot]
|
||||
else -> stringResource(id = R.string.default_favorites_warning)
|
||||
}
|
||||
val items = if (signin) {
|
||||
arrayOf(disabled, localFav, *Settings.favCat)
|
||||
} else {
|
||||
arrayOf(disabled, localFav)
|
||||
}
|
||||
Preference(
|
||||
title = stringResource(id = R.string.default_favorites_collection),
|
||||
summary = summary,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
defaultFavSlot = dialogState.showSelectItem(*items, title = R.string.default_favorites_collection) - 2
|
||||
}
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.settings_eh_force_eh_thumb),
|
||||
summary = stringResource(id = R.string.settings_eh_force_eh_thumb_summary),
|
||||
@@ -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
|
||||
|
||||
@@ -3,10 +3,12 @@ package com.hippo.ehviewer.ui.tools
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
@@ -41,11 +43,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
|
||||
@@ -146,25 +148,18 @@ class DialogState {
|
||||
@StringRes confirmText: Int? = null,
|
||||
@StringRes dismissText: Int? = null,
|
||||
@StringRes title: Int? = null,
|
||||
onDismiss: () -> Unit = {},
|
||||
text: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
return dialog { cont ->
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
onDismiss()
|
||||
cont.cancel()
|
||||
},
|
||||
onDismissRequest = { cont.cancel() },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { cont.resume(Unit) }) {
|
||||
Text(text = stringResource(id = confirmText ?: android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = dismissText.ifNotNullThen {
|
||||
TextButton(onClick = {
|
||||
onDismiss()
|
||||
cont.cancel()
|
||||
}) {
|
||||
TextButton(onClick = { cont.cancel() }) {
|
||||
Text(text = stringResource(id = dismissText!!))
|
||||
}
|
||||
},
|
||||
@@ -183,7 +178,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) },
|
||||
@@ -201,22 +195,12 @@ class DialogState {
|
||||
vararg items: String,
|
||||
@StringRes title: Int,
|
||||
): Int = showNoButton {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
LazyColumn {
|
||||
itemsIndexed(items) { index, text ->
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
Text(text = stringResource(id = title), style = MaterialTheme.typography.headlineSmall)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
|
||||
items.forEachIndexed { index, text ->
|
||||
Text(text = text, modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth().padding(vertical = 8.dp), style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,10 +233,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 +246,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 +263,7 @@ class DialogState {
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
},
|
||||
maxLines = 6,
|
||||
shape = ShapeDefaults.ExtraSmall,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
@@ -284,18 +272,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,7 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import com.google.android.gms.net.CronetProviderInstaller
|
||||
import com.hippo.ehviewer.Settings
|
||||
|
||||
@get:ChecksSdkIntAtLeast(Build.VERSION_CODES.S, extension = 7)
|
||||
val isCronetSupported: Boolean
|
||||
get() = isAtLeastSExtension7
|
||||
get() = Settings.enableQuic && 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
|
||||
@@ -21,5 +21,3 @@ fun String.toLongOrDefault(defaultValue: Long): Long {
|
||||
fun String.toFloatOrDefault(defaultValue: Float): Float {
|
||||
return toFloatOrNull() ?: defaultValue
|
||||
}
|
||||
|
||||
fun String?.containsIgnoreCase(other: String) = this?.contains(other, true) ?: false
|
||||
+1
-3
@@ -57,9 +57,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun onTouchEvent(e: MotionEvent): Boolean {
|
||||
if (scrollState == SCROLL_STATE_IDLE) {
|
||||
detector.onTouchEvent(e)
|
||||
}
|
||||
detector.onTouchEvent(e)
|
||||
return super.onTouchEvent(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,26 +17,29 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_filter"
|
||||
android:icon="@drawable/v_filter_dark_x24"
|
||||
android:title="@string/download_filter"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_open_side_sheet"
|
||||
android:icon="@drawable/ic_outline_label_24"
|
||||
android:title="@string/download_labels"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_filter"
|
||||
android:title="@string/download_filter"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_start_all"
|
||||
android:icon="@drawable/v_play_x24"
|
||||
android:title="@string/download_start_all"
|
||||
app:showAsAction="never" />
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_stop_all"
|
||||
android:icon="@drawable/v_pause_x24"
|
||||
android:title="@string/download_stop_all"
|
||||
app:showAsAction="never" />
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_reset_reading_progress"
|
||||
|
||||
@@ -294,7 +294,7 @@
|
||||
<string name="settings_advanced_dump_logcat_to">Protokolldatei wurde auf %s gespeichert.</string>
|
||||
<string name="cloud_favorites">Cloud-Favoriten</string>
|
||||
<string name="local_favorites">Lokale Favoriten</string>
|
||||
<string name="search_bar_hint">%s durchsuchen</string>
|
||||
<string name="favorites_search_bar_hint">%s durchsuchen</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">Aus Favoriten löschen</string>
|
||||
|
||||
@@ -354,7 +354,7 @@
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">Favoritos en la nube</string>
|
||||
<string name="local_favorites">Favoritos locales</string>
|
||||
<string name="search_bar_hint">Buscar %s</string>
|
||||
<string name="favorites_search_bar_hint">Buscar %s</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">Eliminar de favoritos</string>
|
||||
|
||||
@@ -393,7 +393,7 @@
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">Favoris de cloud</string>
|
||||
<string name="local_favorites">Favoris locales</string>
|
||||
<string name="search_bar_hint">Chercher dans %s</string>
|
||||
<string name="favorites_search_bar_hint">Chercher dans %s</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">Effacer de favoris</string>
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">Cloudのお気に入り</string>
|
||||
<string name="local_favorites">ローカルのお気に入り</string>
|
||||
<string name="search_bar_hint">%sを検索</string>
|
||||
<string name="favorites_search_bar_hint">%sを検索</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">お気に入りから削除</string>
|
||||
|
||||
@@ -364,7 +364,7 @@
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">클라우드 즐겨찾기</string>
|
||||
<string name="local_favorites">로컬 즐겨찾기</string>
|
||||
<string name="search_bar_hint">%s 검색</string>
|
||||
<string name="favorites_search_bar_hint">%s 검색</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">즐겨찾기에서 삭제</string>
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">รายการโปรดบนเว็บ</string>
|
||||
<string name="local_favorites">รายการโปรดบนเครื่อง</string>
|
||||
<string name="search_bar_hint">ค้นหา %s</string>
|
||||
<string name="favorites_search_bar_hint">ค้นหา %s</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">ลบออกจากรายการโปรด</string>
|
||||
|
||||
@@ -149,9 +149,6 @@
|
||||
<string name="refresh">刷新</string>
|
||||
<string name="refresh_original">刷新(原图)</string>
|
||||
<string name="open_in_other_app">在其他应用中打开</string>
|
||||
<string name="clear_image_cache">清除图片缓存</string>
|
||||
<string name="clear_image_cache_confirm">清除该画廊的所有图片缓存?</string>
|
||||
<string name="image_cache_cleared">已清除图片缓存</string>
|
||||
<string name="rate_successfully">评分成功</string>
|
||||
<string name="rate_failed">评分失败</string>
|
||||
<string name="no_torrents">没有种子</string>
|
||||
@@ -379,6 +376,8 @@
|
||||
<string name="settings_advanced_dump_logcat_to">已保存日志至 %s</string>
|
||||
<string name="settings_advanced_read_cache_size">阅读缓存大小</string>
|
||||
<string name="settings_advanced_app_language_title">App 界面语言</string>
|
||||
<string name="settings_advanced_enable_quic">[实验性] QUIC 支持</string>
|
||||
<string name="settings_advanced_enable_quic_summary">启用 QUIC 协议支持(需要重启应用)</string>
|
||||
<string name="settings_advanced_export_data">导出数据</string>
|
||||
<string name="settings_advanced_export_data_summary">保存数据至外置存储器,例如下载列表,快速搜索列表</string>
|
||||
<string name="settings_advanced_export_data_to">已导出数据至 %s</string>
|
||||
@@ -410,7 +409,7 @@
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">云端收藏</string>
|
||||
<string name="local_favorites">本地收藏</string>
|
||||
<string name="search_bar_hint">搜索 %s</string>
|
||||
<string name="favorites_search_bar_hint">搜索 %s</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">删除收藏</string>
|
||||
@@ -419,7 +418,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 +475,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>
|
||||
|
||||
@@ -419,7 +419,7 @@
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">雲端收藏</string>
|
||||
<string name="local_favorites">本地收藏</string>
|
||||
<string name="search_bar_hint">搜索 %s</string>
|
||||
<string name="favorites_search_bar_hint">搜索 %s</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">刪除收藏</string>
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">雲端收藏</string>
|
||||
<string name="local_favorites">本機收藏</string>
|
||||
<string name="search_bar_hint">搜尋 %s</string>
|
||||
<string name="favorites_search_bar_hint">搜尋 %s</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">移除收藏</string>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -191,9 +191,6 @@
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="refresh_original">Refresh (Original image)</string>
|
||||
<string name="open_in_other_app">Open with other app</string>
|
||||
<string name="clear_image_cache">Clear image cache</string>
|
||||
<string name="clear_image_cache_confirm">Clear all cached images for this gallery?</string>
|
||||
<string name="image_cache_cleared">Image cache cleared</string>
|
||||
<string name="rate_successfully">Rate successfully</string>
|
||||
<string name="rate_failed">Rate failed</string>
|
||||
<string name="no_torrents">No torrents</string>
|
||||
@@ -423,6 +420,8 @@
|
||||
<string name="settings_advanced_dump_logcat_to">Logcat dumped to %s</string>
|
||||
<string name="settings_advanced_read_cache_size">Read cache size</string>
|
||||
<string name="settings_advanced_app_language_title">App language</string>
|
||||
<string name="settings_advanced_enable_quic">[Experimental] QUIC support</string>
|
||||
<string name="settings_advanced_enable_quic_summary">Enable QUIC protocol support (App restart required)</string>
|
||||
<string name="settings_advanced_export_data">Export data</string>
|
||||
<string name="settings_advanced_export_data_summary">Save data like download list, quick search list, to external storage</string>
|
||||
<string name="settings_advanced_export_data_to">Exported data to %s</string>
|
||||
@@ -440,7 +439,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"><del>Hippo &lt;ehviewersu$gmail.com&gt;</del><br><del>NekoInverter</del><br><del>飛鳥澪</del><br>Foolbar</string>
|
||||
<string name="settings_about_author_summary" translatable="false"><del>Hippo &lt;ehviewersu$gmail.com&gt;</del><br><del>NekoInverter</del><br>飛鳥澪</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>
|
||||
@@ -468,7 +467,7 @@
|
||||
<!-- Favorites -->
|
||||
<string name="cloud_favorites">Cloud Favorites</string>
|
||||
<string name="local_favorites">Local Favorites</string>
|
||||
<string name="search_bar_hint">Search %s</string>
|
||||
<string name="favorites_search_bar_hint">Search %s</string>
|
||||
<string name="favorites_title">%s</string>
|
||||
<string name="favorites_title_2">%1$s - %2$s</string>
|
||||
<string name="delete_favorites_dialog_title">Delete from favorites</string>
|
||||
@@ -477,7 +476,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 +540,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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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(),
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
@@ -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"
|
||||
@@ -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`)" }
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
@@ -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()
|
||||
}
|
||||
+11
-8
@@ -1,20 +1,23 @@
|
||||
plugins {
|
||||
id("com.android.application") version "8.1.2" 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.android.application") version "8.1.0" apply false
|
||||
kotlin("android") version "1.9.0" apply false
|
||||
kotlin("plugin.serialization") version "1.9.0" apply false
|
||||
id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
|
||||
id("com.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)
|
||||
delete(rootProject.buildDir)
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("com.android.tools:r8:8.3.6-dev")
|
||||
classpath("com.android.tools:r8:8.2.20-dev")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||

|
||||

|
||||
|
||||
# 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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user