Compare commits

..
6 Commits
Author SHA1 Message Date
FooIbar e2345cc756 Release 1.8.9.2
Releases / Build (push) Has been cancelled
2023-08-15 17:48:26 +08:00
FooIbar 8d0b8534b2 Merge branch '1.8.x.x' into 1.8.9.x 2023-08-15 17:48:11 +08:00
FooIbar 63f9c8de79 Release 1.8.9.1
Releases / Build (push) Has been cancelled
2023-08-14 23:17:22 +08:00
FooIbar f792f2c968 Merge branch '1.8.x.x' into 1.8.9.x 2023-08-14 23:16:13 +08:00
FooIbar 4e916731b3 Release 1.8.9.0
Releases / Build (push) Has been cancelled
2023-08-11 14:29:52 +08:00
FooIbar 6c24407fb7 Update releases.yml 2023-08-11 14:29:52 +08:00
550 changed files with 18290 additions and 15796 deletions

No files matched your search

-4
View File
@@ -1,4 +0,0 @@
[*.{kt,kts}]
ij_kotlin_imports_layout = *
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
+8 -14
View File
@@ -6,10 +6,10 @@ body:
- type: markdown
attributes:
value: |
感谢您愿意为 EhViewer 做出贡献!
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
提交问题报告前,还请首先完成文末的自查步骤
Thanks for your contribution on EhViewer Project!
Thanks for your contribution on Ehviewer-Overhauled Project!
Please finish verify steps which list in the end first before create bug report
- type: textarea
@@ -64,7 +64,7 @@ body:
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
placeholder: 06-15 17:44:53.704 23382 23382 E ...
validations:
required: true
required: false
- type: textarea
id: more
@@ -79,15 +79,6 @@ body:
validations:
required: false
- type: input
id: site
attributes:
label: 浏览站点 / Browsing site
description: E-Hentai / ExHentai
placeholder: E-Hentai
validations:
required: true
- type: input
id: version
attributes:
@@ -144,11 +135,14 @@ body:
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- label: 您已搜索过 [Issue Tracker](https://github.com/FooIbar/EhViewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/FooIbar/EhViewer/issues), No duplicate or related open issue has been found
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
required: true
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
required: true
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
required: true
- label: 您确保已使用最新 [CI 版本](https://github.com/FooIbar/EhViewer/actions)测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest [CI version](https://github.com/FooIbar/EhViewer/actions)
- label: 您确保已使用最新 CI 版本测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest CI version
required: true
+5 -1
View File
@@ -1 +1,5 @@
blank_issues_enabled: false
blank_issues_enabled: false
contact_links:
- name: 所有其他问题 / All other questions
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
about: 转到 Discussions / Turn to discussions
@@ -0,0 +1,94 @@
name: 功能请求 / Feature request
description: 提出一个功能建议 / Suggest an idea
labels:
- "enhancement"
body:
- type: markdown
attributes:
value: |
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
提交功能建议前,还请首先完成文末的自查步骤
Thanks for your contribution on Ehviewer-Overhauled Project!
Please finish verify steps which list in the end first before suggest an idea
- type: textarea
id: request
attributes:
label: 需求 / Requirement
description: |
在此处描述您的需求。这通常会是一个您想要的功能
Ddescribe what you need here.
placeholder: |
我需要 XXX 功能 ...
I want ABC feature ...
validations:
required: true
- type: textarea
id: impl
attributes:
label: 建议实现 / Suggested implements
description: |
在此处表述您建议的实现方式。如有可能,UI 类功能请求还请尽量附上图示
Describe your suggested implements here, It's recommend to add a photo if you are making a UI feature request.
placeholder: |
建议在 XX 处添加 XX ...
I recommend add ABC feature to DEF...
图片(如果有)/ Photos (if exists)
validations:
required: true
- type: textarea
id: more
attributes:
label: 备注 / Addition details
description: |
在此处写下其他您想说的内容
Describe addition details here
placeholder: |
其他有用的信息与附件
Additional details and attachments
validations:
required: false
- type: input
id: version
attributes:
label: EhViewer 版本号 / EhViewer version code
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
placeholder: 1.6.9.13
validations:
required: true
- type: input
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 位于设置 - 关于中的版本号后 / Next to the version code in Settings - About
placeholder: 26f05a0
validations:
required: true
- type: checkboxes
id: check
attributes:
label: 自查步骤 / Verify steps
description: |
请确认您已经遵守所有必选项
Please ensure you have obtained all needed options
options:
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
required: true
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
required: true
- label: 您确保这个 Issue 只提及一个功能。如果您有多个功能请求,烦请发起多个 Issue / Ensure there is only one feature request in this issue. Please make mutiply issue for mutiply feature request
required: true
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
required: true
+29 -164
View File
@@ -4,9 +4,8 @@ on:
push:
branches:
- '*'
- 'renovate/**'
pull_request:
workflow_dispatch:
tags-ignore:
- "*"
jobs:
linux:
@@ -14,24 +13,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 17
uses: actions/setup-java@v4
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: 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,175 +38,42 @@ 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
arguments: assembleRelease
- 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-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cmake-
- name: Build OSS
run: ./gradlew assembleDefaultOssRelease
- name: Upload OSS ARM64
- name: Update ARM64
uses: actions/upload-artifact@v3
with:
name: default-oss-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
- name: Upload OSS x86_64
- name: Update x86_64
uses: actions/upload-artifact@v3
with:
name: default-oss-x86_64-${{ github.sha }}
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk
name: x86_64-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86_64-release.apk
- name: Upload OSS ARM32
- name: Update ARM32
uses: actions/upload-artifact@v3
with:
name: default-oss-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk
name: armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
- name: Upload OSS x86
- name: Update x86
uses: actions/upload-artifact@v3
with:
name: default-oss-x86-${{ github.sha }}
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk
name: x86-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86-release.apk
- name: Upload OSS mapping
- name: Update mapping
uses: actions/upload-artifact@v3
with:
name: default-oss-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultOssRelease/mapping.txt
- name: Upload OSS native debug symbols
uses: actions/upload-artifact@v3
with:
name: default-oss-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip
- name: Build GMS
run: ./gradlew assembleDefaultGmsRelease
- name: Upload GMS ARM64
uses: actions/upload-artifact@v3
with:
name: default-gms-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk
- name: Upload GMS x86_64
uses: actions/upload-artifact@v3
with:
name: default-gms-x86_64-${{ github.sha }}
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk
- name: Upload GMS ARM32
uses: actions/upload-artifact@v3
with:
name: default-gms-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk
- name: Upload GMS x86
uses: actions/upload-artifact@v3
with:
name: default-gms-x86-${{ github.sha }}
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk
- name: Upload GMS mapping
uses: actions/upload-artifact@v3
with:
name: default-gms-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultGmsRelease/mapping.txt
- name: Upload GMS native debug symbols
uses: actions/upload-artifact@v3
with:
name: default-gms-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip
- name: Build Marshmallow OSS
run: ./gradlew assembleMarshmallowOssRelease
- name: Upload Marshmallow OSS ARM64
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk
- name: Upload Marshmallow OSS x86_64
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-x86_64-${{ github.sha }}
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk
- name: Upload Marshmallow OSS ARM32
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk
- name: Upload Marshmallow OSS x86
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-x86-${{ github.sha }}
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk
- name: Upload Marshmallow OSS mapping
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-mapping-${{ github.sha }}
path: app/build/outputs/mapping/marshmallowOssRelease/mapping.txt
- name: Upload Marshmallow OSS native debug symbols
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip
- name: Build Marshmallow GMS
run: ./gradlew assembleMarshmallowGmsRelease
- name: Upload Marshmallow GMS ARM64
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-arm64-v8a-release.apk
- name: Upload Marshmallow GMS x86_64
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-x86_64-${{ github.sha }}
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86_64-release.apk
- name: Upload Marshmallow GMS ARM32
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-armeabi-v7a-release.apk
- name: Upload Marshmallow GMS x86
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-x86-${{ github.sha }}
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86-release.apk
- name: Upload Marshmallow GMS mapping
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-mapping-${{ github.sha }}
path: app/build/outputs/mapping/marshmallowGmsRelease/mapping.txt
- name: Upload Marshmallow GMS native debug symbols
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release/mapping.txt
retention-days: 5
-22
View File
@@ -1,22 +0,0 @@
name: 'Lock Threads'
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
concurrency:
group: lock-threads
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: '1'
include-any-issue-labels: 'duplicate, wontfix'
process-only: 'issues'
+66
View File
@@ -0,0 +1,66 @@
name: Pull Request Check
on: [ pull_request ]
jobs:
linux:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.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
+17 -78
View File
@@ -11,33 +11,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 17
uses: actions/setup-java@v4
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
- 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-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cmake-
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
@@ -45,71 +35,20 @@ jobs:
- name: Rename Apks
run: |
mv app/build/outputs/apk/defaultOss/release/app-default-oss-universal-release.apk EhViewer-${{ github.ref_name }}-default-oss-universal.apk
mv app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86.apk
mv app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
mv app/build/outputs/apk/defaultGms/release/app-default-gms-universal-release.apk EhViewer-${{ github.ref_name }}-default-gms-universal.apk
mv app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86.apk
mv app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
mv app/build/outputs/mapping/marshmallowOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
mv app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
mv app/build/outputs/mapping/marshmallowGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
mv app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
cp app/build/outputs/apk/release/app-universal-release.apk app/build/outputs/apk/release/EhViewer-universal.apk
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64.apk
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
- name: Releases
uses: softprops/action-gh-release@v1
with:
body: Bump Version
files: |
EhViewer-${{ github.ref_name }}-default-oss-universal.apk
EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
EhViewer-${{ github.ref_name }}-default-oss-x86.apk
EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
EhViewer-${{ github.ref_name }}-default-gms-universal.apk
EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
EhViewer-${{ github.ref_name }}-default-gms-x86.apk
EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
app/build/outputs/apk/release/EhViewer-universal.apk
app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
app/build/outputs/apk/release/EhViewer-x86_64.apk
app/build/outputs/apk/release/EhViewer-x86.apk
app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
app/build/outputs/mapping/release/mapping.txt
+1
View File
@@ -6,4 +6,5 @@
*.iml
.idea
/captures
release
google-services.json
+3
View File
@@ -0,0 +1,3 @@
[submodule "app/src/main/cpp/nettle/nettle"]
path = app/src/main/cpp/nettle/nettle
url = https://github.com/Ehviewer-Overhauled/nettle
-40
View File
@@ -1,40 +0,0 @@
# 变更日志
## 1.9.0
### 重大变更
* 除阅读器外全部迁移至 Compose
### 新功能
* 设置-高级 中增加实验性滑动手势敏感度设置
* 支持通过滑动手势打开右侧抽屉
* 支持预见式返回动画,相关设置位于 设置-高级
### 改进
* \[阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
* 优化原图下载重试策略
* 优化小屏设备上的搜索类别布局
* 优化过渡动画
* 优化历史记录删除动画
* 设置-EH 中的缩略图大小设置改为缩略图列数
### Bug 修复
* 修复点击通知栏的下载通知闪退 [#175](https://github.com/FooIbar/EhViewer/issues/175)
* 修复收藏状态显示错误 [#189](https://github.com/FooIbar/EhViewer/issues/189)
* 修复双击导航按钮后无法打开导航抽屉
* 修复某些情况下闪退 [#142](https://github.com/FooIbar/EhViewer/issues/142) [#208](https://github.com/FooIbar/EhViewer/issues/208)
* 修复收藏界面点击重试崩溃 [#193](https://github.com/FooIbar/EhViewer/issues/193)
* 修复本地收藏滚动位置丢失 [#192](https://github.com/FooIbar/EhViewer/issues/192)
* 修复手速过快页面切换过快使加载圈圈不自动消失 [#209](https://github.com/FooIbar/EhViewer/issues/209)
* 修复打开并关闭右侧抽屉后,状态栏一片白色 [#101](https://github.com/FooIbar/EhViewer/issues/101)
* 修复使用深色主题时,切换界面会闪过亮色界面 [#121](https://github.com/FooIbar/EhViewer/issues/121)
* \[Marshmallow] 修复小内存设备上图片解码失败 [#229](https://github.com/FooIbar/EhViewer/issues/229) [#372](https://github.com/FooIbar/EhViewer/issues/372)
* 修复已舍弃画廊上传者解析
### 已知问题
* 画廊信息对话框无法通过返回关闭 [#413](https://github.com/FooIbar/EhViewer/issues/413)
+29 -39
View File
@@ -1,57 +1,60 @@
<p align="right">
<strong>English</strong>
<span> | </span>
<a href="/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="/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="/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/downloads/Ehviewer-Overhauled/EhViewer/total.svg" alt="Downloads">
</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/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="#description">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#description">
Description
</a>
<span> | </span>
<a href="#download">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#download">
Download
</a>
<span> | </span>
<a href="#screenshot">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#screenshot">
Screenshot
</a>
<span> | </span>
<a href="#thanks">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#thanks">
Thanks
</a>
<span> | </span>
<a href="#license">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#license">
License
</a>
</h3>
@@ -66,20 +69,7 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
# Download
| Flavor | Feature |
|-------------|-----------------------------------|
| Default | Android 9+, full support |
| Marshmallow | Android 6.0-8.1, limited support* |
| OSS | FOSS dependencies** |
| GMS | Proprietary GMS component |
\* Archives and GIFs are not supported
** Fall back to OkHttp on devices that don't support [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine)
<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
@@ -93,13 +83,14 @@ Here is the libraries
- [Arrow](https://arrow-kt.io/)
- [AOSP & AndroidX](http://source.android.com/)
- [Kotlin & KotlinX](https://kotlinlang.org/)
- [MDC-Android](https://github.com/material-components/material-components-android)
- [Material Icons](https://github.com/google/material-design-icons)
- [OkHttp](https://square.github.io/okhttp/)
- [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/)
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
- [libarchive](http://www.libarchive.org/)
- [jsoup](https://jsoup.org/)
- [Jsoup](https://jsoup.org/)
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
@@ -108,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.
+107 -132
View File
@@ -8,21 +8,20 @@ val isRelease: Boolean
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.spotless)
alias(libs.plugins.aboutlibrariesPlugin)
alias(libs.plugins.rustAndroidPlugin)
alias(libs.plugins.composeCompilerReportGenerator)
id("com.android.application")
kotlin("android")
id("kotlin-parcelize")
kotlin("plugin.serialization")
id("com.google.devtools.ksp")
id("com.mikepenz.aboutlibraries.plugin")
id("org.mozilla.rust-android-gradle.rust-android")
id("dev.shreyaspatil.compose-compiler-report-generator")
}
android {
compileSdk = 34
buildToolsVersion = "34.0.0"
ndkVersion = "26.1.10909125"
ndkVersion = "26.0.10404224"
splits {
abi {
@@ -32,7 +31,7 @@ android {
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
isUniversalApk = true
} else {
include("arm64-v8a", "x86_64")
include("arm64-v8a")
}
}
}
@@ -64,8 +63,8 @@ android {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
targetSdk = 34
versionCode = 180046
versionName = "1.9.0"
versionCode = 180043
versionName = "1.8.9.2"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -83,39 +82,8 @@ android {
),
)
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
ndk {
debugSymbolLevel = "FULL"
}
}
flavorDimensions += listOf("api", "oss")
productFlavors {
create("default") {
dimension = "api"
}
create("marshmallow") {
dimension = "api"
minSdk = 23
applicationIdSuffix = ".m"
versionNameSuffix = "-M"
externalNativeBuild {
cmake {
targets += "noop"
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
create("oss") {
dimension = "oss"
}
create("gms") {
dimension = "oss"
versionNameSuffix = "-gms"
}
}
externalNativeBuild {
@@ -124,12 +92,19 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = listOf(
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
"-Xjvm-default=all",
"-Xlambdas=indy",
"-XXLanguage:+BreakContinueInInlineLambdas",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
@@ -145,15 +120,13 @@ android {
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-opt-in=splitties.experimental.ExperimentalSplittiesApi",
"-opt-in=splitties.preferences.DataStorePreferencesPreview",
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:intrinsicRemember=true",
)
}
lint {
disable += setOf("MissingTranslation", "MissingQuantity")
fatal += setOf("NewApi", "InlineApi")
abortOnError = true
checkReleaseBuilds = false
disable.add("MissingTranslation")
}
packaging {
@@ -173,11 +146,9 @@ android {
isShrinkResources = true
proguardFiles("proguard-rules.pro")
signingConfig = signConfig
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
}
debug {
applicationIdSuffix = ".debug"
buildConfigField("String", "BUILD_TIME", "\"\"")
}
}
@@ -188,7 +159,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
kotlinCompilerExtensionVersion = "1.5.1"
}
namespace = "com.hippo.ehviewer"
@@ -196,97 +167,104 @@ android {
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.biometric)
implementation(libs.androidx.browser)
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-beta01")
// https://developer.android.com/jetpack/androidx/releases/compose-material3
api(platform(libs.compose.bom))
implementation(libs.bundles.compose)
// api(platform("androidx.compose:compose-bom:2023.05.00"))
api(platform("dev.chrisbanes.compose:compose-bom:2023.07.00-alpha02"))
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material3:material3-window-size-class")
implementation("androidx.compose.ui:ui-util")
implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.compiler)
implementation("androidx.core:core-ktx:1.12.0-rc01")
implementation(libs.androidx.core)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.constraintlayout.compose)
implementation(libs.androidx.coordinatorlayout)
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha12")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha02")
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
// https://developer.android.com/jetpack/androidx/releases/navigation
implementation(libs.bundles.androidx.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
implementation(libs.bundles.androidx.paging)
implementation(libs.androidx.recyclerview)
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
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.paging)
val room_version = "2.6.0-alpha03"
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-paging:$room_version")
implementation(libs.androidx.work.runtime)
implementation(libs.photoview) // Dead Dependency
implementation(libs.directionalviewpager) // Dead Dependency
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("com.drakeet.drawer:drawer:1.0.3")
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
implementation(libs.bundles.accompanist)
implementation(libs.material)
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-alpha02")
implementation(libs.bundles.splitties)
val splitties = "3.0.0"
implementation("com.louiscad.splitties:splitties-appctx:$splitties")
implementation("com.louiscad.splitties:splitties-systemservices:$splitties")
implementation("com.louiscad.splitties:splitties-preferences:$splitties")
implementation("com.louiscad.splitties:splitties-arch-room:$splitties")
// https://square.github.io/okhttp/changelogs/changelog/
implementation(platform(libs.okhttp.bom))
implementation(libs.bundles.okhttp)
implementation(platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11"))
implementation("com.squareup.okhttp3:okhttp-android")
implementation("com.squareup.okhttp3:okhttp-coroutines")
implementation(libs.okio.jvm)
implementation("com.squareup.okio:okio-jvm:3.5.0")
implementation(libs.diff)
implementation("com.mikepenz:aboutlibraries-core:10.8.3")
implementation(libs.aboutlibraries.core)
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(libs.insetter) // Dead Dependency
implementation(libs.reorderable)
implementation(platform(libs.arrow.stack))
implementation(libs.arrow.fx.coroutines)
implementation(platform("io.arrow-kt:arrow-stack:1.2.0"))
implementation("io.arrow-kt:arrow-fx-coroutines")
// https://coil-kt.github.io/coil/changelog/
implementation(platform(libs.coil.bom))
implementation(libs.bundles.coil)
implementation(platform("io.coil-kt:coil-bom:2.4.0"))
implementation("io.coil-kt:coil-compose")
implementation("io.coil-kt:coil-gif")
implementation(libs.bundles.ktor)
val ktor = "2.3.3"
implementation("io.ktor:ktor-io-jvm:$ktor")
implementation("io.ktor:ktor-utils-jvm:$ktor")
implementation(libs.bundles.kotlinx.serialization)
implementation("org.chromium.net:cronet-embedded:113.5672.61")
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
implementation(libs.jsoup)
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")
debugImplementation(libs.chucker)
releaseImplementation(libs.chucker.nop)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jsoup:jsoup:1.16.1")
coreLibraryDesugaring(libs.desugar)
val chunker = "4.0.0"
debugImplementation("com.github.chuckerteam.chucker:library:$chunker")
releaseImplementation("com.github.chuckerteam.chucker:library-no-op:$chunker")
"gmsImplementation"(libs.bundles.cronet)
}
configurations.all {
resolutionStrategy {
// Workaround IME won't show again once hidden.
// Google issue-tracker link?
force("androidx.compose.ui:ui-text-android:1.6.0-alpha08")
exclude("androidx.navigation", "navigation-compose")
}
}
kotlin {
jvmToolchain(17)
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
}
ksp {
@@ -297,32 +275,29 @@ ksp {
aboutLibraries {
duplicationMode = MERGE
duplicationRule = GROUP
exclusionPatterns = listOf(
"androidx\\..*".toPattern(),
".*annotations".toPattern(),
"com\\.google\\.code\\..*".toPattern(),
"com\\.google\\.guava:listenablefuture".toPattern(),
"com\\.google\\.protobuf:protobuf-javalite".toPattern(),
"org\\.checkerframework:checker-qual".toPattern(),
"org\\.chromium\\.net:cronet-embedded".toPattern(),
"org\\.slf4j:slf4j-api".toPattern(),
)
}
cargo {
module = "src/main/rust"
libname = "ehviewer_rust"
targets = if (isRelease) listOf("arm", "x86", "arm64", "x86_64") else listOf("arm64", "x86_64")
targets = if (isRelease) listOf("arm", "x86", "arm64", "x86_64") else listOf("arm64")
if (isRelease) profile = "release"
}
val ktlintVersion = libs.versions.ktlint.get()
spotless {
kotlin {
// https://github.com/diffplug/spotless/issues/111
target("src/**/*.kt")
ktlint(ktlintVersion)
}
kotlinGradle {
ktlint(ktlintVersion)
}
}
tasks.configureEach {
if (name.startsWith("merge") && name.endsWith("JniLibFolders")) {
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"))
}
}
+22 -13
View File
@@ -1,20 +1,29 @@
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# https://issuetracker.google.com/issues/307323842
-keepclassmembers class androidx.compose.ui.platform.WindowInfoImpl {
setWindowFocused(boolean);
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
-allowaccessmodification
# https://issuetracker.google.com/issues/190382641
-keepclassmembers class kotlin.SafePublicationLazyImpl {
java.lang.Object _value;
}
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
-keep class com.hippo.ehviewer.client.parser.Torrent { *; }
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
-repackageclasses
-allowaccessmodification
-overloadaggressively
@@ -1,13 +0,0 @@
package com.hippo.ehviewer.util
import com.google.android.gms.net.CronetProviderInstaller
import com.hippo.ehviewer.Settings
import org.chromium.net.CronetProvider
import splitties.init.appCtx
val isCronetSupported: Boolean
get() = Settings.enableCronet && CronetProvider.getAllProviders(appCtx).any { it.isEnabled }.also {
if (!it) {
CronetProviderInstaller.installProvider(appCtx)
}
}
+2 -6
View File
@@ -23,20 +23,16 @@
android:allowBackup="true"
android:appCategory="image"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_scheme"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="@bool/large_heap"
android:localeConfig="@xml/locale_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="tiramisu">
<profileable android:shell="true" />
<activity
android:name=".ui.ConfigureActivity"
android:exported="false"
@@ -48,7 +44,6 @@
android:configChanges="screenSize"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/AppTheme.Splash"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -139,7 +134,8 @@
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity android:name=".ui.WebViewActivity" />
<activity
android:name=".ui.WebViewActivity" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
@@ -0,0 +1,34 @@
From 2bda36800a5424c44378bbc0cc8bb5d26a0f34fa Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 22 Jul 2023 08:30:20 +0800
Subject: [PATCH] Don't build other binarys
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 55cd358..52b57aa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -962,7 +962,7 @@ check_symbol_exists(getopt_long getopt.h HAVE_GETOPT_LONG)
# xzdec
#############################################################################
-if(HAVE_GETOPT_LONG AND HAVE_DECODERS)
+if(0)
add_executable(xzdec
src/common/sysdefs.h
src/common/tuklib_common.h
@@ -1008,7 +1008,7 @@ endif()
# xz
#############################################################################
-if(NOT MSVC AND HAVE_GETOPT_LONG)
+if(0)
add_executable(xz
src/common/mythread.h
src/common/sysdefs.h
--
2.41.0
+29
View File
@@ -0,0 +1,29 @@
From 57da49f9b0ed8538573f6cfe289f835ee0078eec Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 22 Jul 2023 17:50:08 +0800
Subject: [PATCH] Hide symbols
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 646484b7..67a71272 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2060,10 +2060,10 @@ SET(OLD_CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS}")
SET(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -fvisibility=hidden -Werror")
CHECK_C_SOURCE_COMPILES("void __attribute__((visibility(\"default\"))) foo(void);
int main() { return 0; }" HAVE_VISIBILITY_ATTR)
-IF (HAVE_VISIBILITY_ATTR)
+IF (0)
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
ADD_DEFINITIONS(-D__LIBARCHIVE_ENABLE_VISIBILITY)
-ENDIF(HAVE_VISIBILITY_ATTR)
+ENDIF(0)
SET(CMAKE_REQUIRED_FLAGS "${OLD_CMAKE_REQUIRED_FLAGS}")
# Generate "config.h" from "build/cmake/config.h.in"
--
2.41.0
@@ -0,0 +1,25 @@
From c9298fd6dc0a8b04dd4b2247118b9cb895c46e58 Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 23 Jul 2022 16:05:36 +0800
Subject: [PATCH 2/2] Insert link libs
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 713e3bc5..7247640d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -415,7 +415,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
# # endif
ENDIF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
-SET(ADDITIONAL_LIBS "")
+SET(ADDITIONAL_LIBS ${LIBARCHIVE_CUSTOM_LIBS})
#
# Find ZLIB
#
--
2.39.2
+6 -13
View File
@@ -23,21 +23,15 @@ set(FETCHCONTENT_BASE_DIR ${deps})
set(BUILD_TESTING 0)
FetchContent_Declare(liblzma
GIT_REPOSITORY https://github.com/FooIbar/xz.git
GIT_TAG v5.4.5
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)
@@ -78,5 +73,3 @@ include_directories(${libarchive_SOURCE_DIR}/libarchive)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED archive.c gifutils.c natsort/strnatcmp.c)
target_link_libraries(${PROJECT_NAME} archive_static log)
add_custom_target("noop")
+8 -8
View File
@@ -279,7 +279,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
}
JNIEXPORT jint JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
jlong size) {
EH_UNUSED(env);
EH_UNUSED(thiz);
@@ -359,7 +359,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
#pragma ide diagnostic ignored "UnreachableCode"
JNIEXPORT jobject JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
@@ -389,7 +389,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass th
#pragma clang diagnostic pop
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_closeArchive(JNIEnv *env, jclass thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
for (int i = 0; i < CTX_POOL_SIZE; i++)
@@ -408,14 +408,14 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_needPassword(JNIEnv *env, jclass thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
return need_encrypt;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_providePassword(JNIEnv *env, jclass thiz,
jstring str) {
EH_UNUSED(thiz);
struct archive_entry *entry;
@@ -446,7 +446,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
}
JNIEXPORT jstring JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_getFilename(JNIEnv *env, jclass thiz,
jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
@@ -462,7 +462,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz,
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToFd(JNIEnv *env, jclass thiz,
jint index, jint fd) {
EH_UNUSED(env);
EH_UNUSED(thiz);
@@ -477,7 +477,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
jobject buffer) {
EH_UNUSED(thiz);
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
+7 -23
View File
@@ -1,7 +1,6 @@
#include <jni.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <android/log.h>
#include <sys/stat.h>
#include <sys/mman.h>
@@ -12,7 +11,6 @@
#define GIF_HEADER_87A "GIF87a"
#define GIF_HEADER_89A "GIF89a"
#define GIF_HEADER_LENGTH 6
static int FRAME_DELAY_START_MARKER = 0x0021F904;
@@ -23,8 +21,7 @@ typedef signed char byte;
#define DEFAULT_FRAME_DELAY 10
static inline bool isGif(void *addr) {
return !memcmp(addr, GIF_HEADER_87A, GIF_HEADER_LENGTH) ||
!memcmp(addr, GIF_HEADER_89A, GIF_HEADER_LENGTH);
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
}
static void doRewrite(byte *addr, size_t size) {
@@ -45,33 +42,20 @@ static void doRewrite(byte *addr, size_t size) {
}
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_isGif(JNIEnv *env, jclass clazz, jint fd) {
byte buffer[GIF_HEADER_LENGTH];
return read(fd, buffer, GIF_HEADER_LENGTH) == GIF_HEADER_LENGTH &&
isGif(buffer);
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
Java_com_hippo_ehviewer_image_ImageKt_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
doRewrite(addr, size);
}
JNIEXPORT jobject JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_mmap(JNIEnv *env, jclass clazz, jint fd) {
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_image_ImageKt_rewriteGifSource2(JNIEnv *env, jclass clazz, jint fd) {
struct stat64 st;
fstat64(fd, &st);
size_t size = st.st_size;
byte *addr = mmap64(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) return NULL;
return (*env)->NewDirectByteBuffer(env, addr, size);
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_munmap(JNIEnv *env, jclass clazz, jobject buffer) {
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
byte *addr = mmap64(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) return;
doRewrite(addr, size);
munmap(addr, size);
}
-1
View File
@@ -1 +0,0 @@
/nettle
+2 -1
View File
@@ -32,6 +32,7 @@ set(LIBNETTLE_SOURCES
nettle/aes256-meta.c
nettle/nist-keywrap.c
nettle/arcfour.c
nettle/arcfour-crypt.c
nettle/arctwo.c
nettle/arctwo-meta.c
nettle/blowfish.c
@@ -176,7 +177,7 @@ set(LIBNETTLE_SOURCES
nettle/sha1-compress.c
nettle/sha1-meta.c
nettle/sha256.c
nettle/sha256-compress-n.c
nettle/sha256-compress.c
nettle/sha224-meta.c
nettle/sha256-meta.c
nettle/sha512.c
+20 -25
View File
@@ -4,19 +4,13 @@
/* Define if building universal (internal helper macro) */
/* #undef AC_APPLE_UNIVERSAL_BUILD */
/* Define to one of `_getb67', `GETB67', `getb67' for Cray-2 and Cray-YMP
systems. This function is required for `alloca.c' support on those systems.
*/
/* #undef CRAY_STACKSEG_END */
/* Define to 1 if using `alloca.c'. */
/* Define to 1 if using 'alloca.c'. */
/* #undef C_ALLOCA */
/* Define to 1 if you have `alloca', as a function or macro. */
/* Define to 1 if you have 'alloca', as a function or macro. */
#define HAVE_ALLOCA 1
/* Define to 1 if you have <alloca.h> and it should be used (not on Ultrix).
*/
/* Define to 1 if <alloca.h> works. */
#define HAVE_ALLOCA_H 1
/* Define if __builtin_bswap64 is available */
@@ -52,9 +46,6 @@
/* Define to 1 if you have the <malloc.h> header file. */
#define HAVE_MALLOC_H 1
/* Define to 1 if you have the <memory.h> header file. */
#define HAVE_MEMORY_H 1
/* Define to 1 each of the following for which a native (ie. CPU specific)
implementation of the corresponding routine exists. */
/* #undef HAVE_NATIVE_memxor3 */
@@ -80,7 +71,7 @@
/* #undef HAVE_NATIVE_cbc_aes256_encrypt */
/* #undef HAVE_NATIVE_chacha_core */
/* #undef HAVE_NATIVE_chacha_2core */
#define HAVE_NATIVE_chacha_3core 1
/* #undef HAVE_NATIVE_chacha_3core */
/* #undef HAVE_NATIVE_chacha_4core */
/* #undef HAVE_NATIVE_fat_chacha_2core */
/* #undef HAVE_NATIVE_fat_chacha_3core */
@@ -97,18 +88,13 @@
/* #undef HAVE_NATIVE_ecc_secp384r1_redc */
/* #undef HAVE_NATIVE_ecc_secp521r1_modp */
/* #undef HAVE_NATIVE_ecc_secp521r1_redc */
/* #undef HAVE_NATIVE_poly1305_set_key */
/* #undef HAVE_NATIVE_poly1305_block */
/* #undef HAVE_NATIVE_poly1305_digest */
/* #undef HAVE_NATIVE_poly1305_blocks */
/* #undef HAVE_NATIVE_fat_poly1305_blocks */
/* #undef HAVE_NATIVE_ghash_set_key */
/* #undef HAVE_NATIVE_ghash_update */
/* #undef HAVE_NATIVE_salsa20_core */
#define HAVE_NATIVE_salsa20_2core 1
/* #undef HAVE_NATIVE_salsa20_2core */
/* #undef HAVE_NATIVE_fat_salsa20_2core */
/* #undef HAVE_NATIVE_sha1_compress */
/* #undef HAVE_NATIVE_sha256_compress_n */
/* #undef HAVE_NATIVE_sha256_compress */
/* #undef HAVE_NATIVE_sha512_compress */
/* #undef HAVE_NATIVE_sha3_permute */
/* #undef HAVE_NATIVE_umac_nh */
@@ -121,11 +107,14 @@
/* #undef HAVE_OPENSSL_EVP_H */
/* Define to 1 if you have the `secure_getenv' function. */
#define HAVE_SECURE_GETENV 1
/* #undef HAVE_SECURE_GETENV */
/* Define to 1 if you have the <stdint.h> header file. */
#define HAVE_STDINT_H 1
/* Define to 1 if you have the <stdio.h> header file. */
#define HAVE_STDIO_H 1
/* Define to 1 if you have the <stdlib.h> header file. */
#define HAVE_STDLIB_H 1
@@ -141,6 +130,9 @@
/* Define to 1 if you have the <sys/stat.h> header file. */
#define HAVE_SYS_STAT_H 1
/* Define to 1 if you have the <sys/time.h> header file. */
#define HAVE_SYS_TIME_H 1
/* Define to 1 if you have the <sys/types.h> header file. */
#define HAVE_SYS_TYPES_H 1
@@ -157,7 +149,7 @@
#define PACKAGE_NAME "nettle"
/* Define to the full name and version of this package. */
#define PACKAGE_STRING "nettle 3.9.1"
#define PACKAGE_STRING "nettle 3.8.1"
/* Define to the one symbol short name of this package. */
#define PACKAGE_TARNAME "nettle"
@@ -166,7 +158,7 @@
#define PACKAGE_URL ""
/* Define to the version of this package. */
#define PACKAGE_VERSION "3.9.1"
#define PACKAGE_VERSION "3.8.1"
/* The size of `long', as computed by sizeof. */
#define SIZEOF_LONG __SIZEOF_LONG__
@@ -182,10 +174,13 @@
STACK_DIRECTION = 0 => direction of growth unknown */
/* #undef STACK_DIRECTION */
/* Define to 1 if you have the ANSI C header files. */
/* Define to 1 if all of the C90 standard headers exist (not just the ones
required in a freestanding environment). This macro is provided for
backward compatibility; new code need not use it. */
#define STDC_HEADERS 1
/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. */
/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. This
macro is obsolete. */
#define TIME_WITH_SYS_TIME 1
/* Defined if public key features are enabled */
+1 -1
View File
@@ -41,7 +41,7 @@ extern "C" {
/* Individual version numbers in decimal */
#define NETTLE_VERSION_MAJOR 3
#define NETTLE_VERSION_MINOR 9
#define NETTLE_VERSION_MINOR 8
#define NETTLE_USE_MINI_GMP 0
@@ -0,0 +1,41 @@
package androidx.navigation.ui
import android.annotation.SuppressLint
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.navigation.ActivityNavigator
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavOptions
@SuppressLint("PrivateResource")
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
is ActivityNavigator.Destination
) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(androidx.fragment.R.animator.fragment_open_enter)
.setExitAnim(androidx.fragment.R.animator.fragment_open_exit)
.setPopEnterAnim(androidx.fragment.R.animator.fragment_close_enter)
.setPopExitAnim(androidx.fragment.R.animator.fragment_close_exit)
}
val options = builder.build()
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.itemId, null, options)
// Return true only if the destination we've navigated to matches the MenuItem
navController.currentDestination?.matchDestination(item.itemId) == true
} catch (e: IllegalArgumentException) {
false
}
}
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
@@ -0,0 +1,25 @@
package androidx.swiperefreshlayout.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.ShapeDrawable
import android.util.AttributeSet
import androidx.core.graphics.ColorUtils
import rikka.core.res.resolveColor
@SuppressLint("PrivateResource")
class ThemedSwipeRefreshLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : SwipeRefreshLayout(context, attrs) {
init {
val surfaceColor =
context.theme.resolveColor(com.google.android.material.R.attr.colorSurface)
val overlayColor =
context.getColor(com.google.android.material.R.color.m3_popupmenu_overlay_color)
val backgroundColor = ColorUtils.compositeColors(overlayColor, surfaceColor)
(mCircleView.background as ShapeDrawable).paint.color = backgroundColor
setColorSchemeColors(context.theme.resolveColor(com.google.android.material.R.attr.colorAccent))
}
}
@@ -0,0 +1,111 @@
package com.davemorrissey.labs.subscaleview
import android.graphics.Bitmap
import android.graphics.Rect
import com.davemorrissey.labs.subscaleview.provider.InputProvider
/**
* Helper class used to set the source and additional attributes from a variety of sources. Supports
* use of a bitmap, asset, resource, external file or any other URI.
*
*
* When you are using a preview image, you must set the dimensions of the full size image on the
* ImageSource object for the full size image using the [.dimensions] method.
*/
class ImageSource {
val bitmap: Bitmap?
val provider: InputProvider?
private var sWidth = 0
private var sHeight = 0
var sRegion = Rect(0, 0, 0, 0)
private set
var isCached = false
private set
private constructor(bitmap: Bitmap, cached: Boolean) {
this.bitmap = bitmap
provider = null
sWidth = bitmap.width
sHeight = bitmap.height
isCached = cached
}
private constructor(provider: InputProvider) {
bitmap = null
this.provider = provider
}
/**
* Use a region of the source image. Region must be set independently for the full size image and the preview if
* you are using one.
*
* @param sRegion the region of the source image to be displayed.
* @return this instance for chaining.
*/
fun region(sRegion: Rect): ImageSource {
this.sRegion = sRegion
setInvariants()
return this
}
/**
* Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI
* and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare
* the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset.
*
* @param sWidth width of the source image.
* @param sHeight height of the source image.
* @return this instance for chaining.
*/
fun dimensions(sWidth: Int, sHeight: Int): ImageSource {
if (bitmap == null) {
this.sWidth = sWidth
this.sHeight = sHeight
}
setInvariants()
return this
}
private fun setInvariants() {
if (!sRegion.isEmpty) {
sWidth = sRegion.width()
sHeight = sRegion.height()
}
}
companion object {
/**
* Create an instance from an input provider.
*
* @param provider input stream provider.
* @return an [ImageSource] instance.
*/
fun provider(provider: InputProvider): ImageSource {
return ImageSource(provider)
}
/**
* Provide a loaded bitmap for display.
*
* @param bitmap bitmap to be displayed.
* @return an [ImageSource] instance.
*/
fun bitmap(bitmap: Bitmap): ImageSource {
return ImageSource(bitmap, false)
}
/**
* Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no
* longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso
* or Volley.
*
* @param bitmap bitmap to be displayed.
* @return an [ImageSource] instance.
*/
fun cachedBitmap(bitmap: Bitmap): ImageSource {
return ImageSource(bitmap, true)
}
}
}
@@ -0,0 +1,15 @@
package com.davemorrissey.labs.subscaleview
import android.graphics.PointF
import java.io.Serializable
/**
* Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.
*/
class ImageViewState(val scale: Float, center: PointF) : Serializable {
private val centerX: Float = center.x
private val centerY: Float = center.y
val center: PointF
get() = PointF(centerX, centerY)
}
@@ -0,0 +1,50 @@
package com.davemorrissey.labs.subscaleview.decoder
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import com.davemorrissey.labs.subscaleview.provider.InputProvider
/**
* Interface for image decoding classes.
*/
interface Decoder {
/**
* Initialise the decoder. When possible, perform initial setup work once in this method. The
* dimensions of the image must be returned.
*
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param provider Provider of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
fun init(context: Context, provider: InputProvider): Point
/**
* Decode a region of the image with the given sample size. This method is called off the UI
* thread so it can safely load the image on the current thread. It is called from
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
* implementations must be thread safe. Adding `synchronized` to the method signature
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
* called concurrently.
*
* @param sRect Source image rectangle to decode.
* @param sampleSize Sample size.
* @return The decoded region. It is safe to return null if decoding fails.
*/
fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap
/**
* Status check. Should return false before initialisation and after recycle.
*
* @return true if the decoder is ready to be used.
*/
val isReady: Boolean
/**
* This method will be called when the decoder is no longer required. It should clean up any
* resources still in use.
*/
fun recycle()
}
@@ -0,0 +1,58 @@
package com.davemorrissey.labs.subscaleview.decoder
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.provider.InputProvider
class ImageDecoder(private val cropBorders: Boolean, bitmapConfig: Bitmap.Config? = null) : Decoder {
init {
val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig
}
/**
* Initialise the decoder. When possible, perform initial setup work once in this method. The
* dimensions of the image must be returned.
*
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param provider Provider of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
override fun init(context: Context, provider: InputProvider): Point {
TODO()
}
/**
* Decode a region of the image with the given sample size. This method is called off the UI
* thread so it can safely load the image on the current thread. It is called from
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
* implementations must be thread safe. Adding `synchronized` to the method signature
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
* called concurrently.
*
* @param sRect Source image rectangle to decode.
* @param sampleSize Sample size.
* @return The decoded region. It is safe to return null if decoding fails.
*/
override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap {
TODO()
}
/**
* Status check. Should return false before initialisation and after recycle.
*
* @return true if the decoder is ready to be used.
*/
override val isReady: Boolean = TODO()
/**
* This method will be called when the decoder is no longer required. It should clean up any
* resources still in use.
*/
override fun recycle() {
TODO()
}
}
@@ -0,0 +1,3 @@
package com.davemorrissey.labs.subscaleview.provider
interface InputProvider
@@ -20,7 +20,6 @@ import android.app.Application
import android.content.ComponentCallbacks2
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.LruCache
import androidx.compose.runtime.snapshots.Snapshot
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.coroutineScope
import coil.ImageLoaderFactory
@@ -48,15 +47,11 @@ import com.hippo.ehviewer.util.Crash
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.isAtLeastS
import com.hippo.ehviewer.util.isCronetSupported
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.launch
import moe.tarsin.kt.unreachable
import okhttp3.AsyncDns
@@ -70,15 +65,6 @@ private val lifecycleScope = lifecycle.coroutineScope
class EhApplication : Application(), ImageLoaderFactory {
override fun onCreate() {
// Initialize Settings on first access
lifecycleScope.launchIO {
val mode = Settings.theme
if (!isAtLeastS) {
withUIContext {
AppCompatDelegate.setDefaultNightMode(mode)
}
}
}
lifecycle.addObserver(lockObserver)
val handler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
@@ -91,17 +77,21 @@ class EhApplication : Application(), ImageLoaderFactory {
handler?.uncaughtException(t, e)
}
super.onCreate()
if (isAtLeastP) {
System.loadLibrary("ehviewer")
}
System.loadLibrary("ehviewer")
System.loadLibrary("ehviewer_rust")
ReadableTime.initialize(this)
lifecycleScope.launchIO {
val theme = Settings.theme
withUIContext {
AppCompatDelegate.setDefaultNightMode(theme)
}
}
lifecycleScope.launchIO {
launchIO {
migrateCookies()
}
launchIO {
EhTagDatabase
EhTagDatabase.update()
}
launchIO {
EhDB
@@ -119,11 +109,6 @@ class EhApplication : Application(), ImageLoaderFactory {
}
}
cleanObsoleteCache(this)
if (BuildConfig.DEBUG) {
Snapshot.registerApplyObserver { anies, _ ->
logcat { anies.toString() }
}
}
}
private suspend fun cleanupDownload() {
@@ -165,9 +150,7 @@ class EhApplication : Application(), ImageLoaderFactory {
} else {
callFactory(baseOkHttpClient)
}
if (isAtLeastP) {
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
}
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
add(MergeInterceptor)
}
diskCache(imageCache)
@@ -185,7 +168,6 @@ class EhApplication : Application(), ImageLoaderFactory {
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
}
chunker { alwaysReadResponseBody(false) }
addInterceptor(UncaughtExceptionInterceptor())
addInterceptor(CloudflareInterceptor(appCtx))
}
}
@@ -130,7 +130,7 @@ object EhDB {
deleteGalleryInfo(galleryInfo)
}
suspend fun removeLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
suspend fun removeLocalFavorites(galleryInfoList: List<BaseGalleryInfo>) {
galleryInfoList.forEach {
removeLocalFavorites(it)
}
@@ -146,7 +146,7 @@ object EhDB {
db.localFavoritesDao().upsert(LocalFavoriteInfo(galleryInfo.gid))
}
suspend fun putLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
suspend fun putLocalFavorites(galleryInfoList: List<BaseGalleryInfo>) {
galleryInfoList.forEach {
putLocalFavorites(it)
}
@@ -195,7 +195,7 @@ object EhDB {
db.historyDao().upsert(HistoryInfo(galleryInfo.gid))
}
suspend fun updateFavoriteSlot(gid: Long, slot: Int) {
suspend fun modifyHistoryInfoFavslotNonRefresh(gid: Long, slot: Int) {
val dao = db.galleryDao()
dao.load(gid)?.let {
it.favoriteSlot = slot
@@ -292,7 +292,7 @@ object EhDB {
}
runCatching {
val downloadInfoList = oldDB.downloadsDao().joinList().asReversed()
DownloadManager.addDownload(downloadInfoList)
DownloadManager.addDownload(downloadInfoList, false)
}
runCatching {
val historyInfoList = oldDB.historyDao().list()
@@ -2,95 +2,18 @@
package com.hippo.ehviewer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import com.hippo.ehviewer.client.CHROME_USER_AGENT
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import java.util.Locale
import kotlin.reflect.KProperty
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.map
import splitties.preferences.BoolPref
import splitties.preferences.DataStorePreferences
import splitties.preferences.FloatPref
import splitties.preferences.IntPref
import splitties.preferences.LongPref
import splitties.preferences.PrefDelegate
import splitties.preferences.StringOrNullPref
import splitties.preferences.StringPref
import splitties.preferences.StringSetOrNullPref
import splitties.preferences.StringSetPref
import splitties.preferences.edit
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
fun <T> PrefDelegate<T>.getValue(): T = when (this) {
is BoolPref -> value
is IntPref -> value
is FloatPref -> value
is LongPref -> value
is StringPref -> value
is StringOrNullPref -> value
is StringSetPref -> value
is StringSetOrNullPref -> value
} as T
@Suppress("UNCHECKED_CAST")
fun <T> PrefDelegate<T>.setValue(newValue: T) = when (this) {
is BoolPref -> value = newValue as Boolean
is IntPref -> value = newValue as Int
is FloatPref -> value = newValue as Float
is LongPref -> value = newValue as Long
is StringPref -> value = newValue as String
is StringOrNullPref -> value = newValue as? String
is StringSetPref -> value = newValue as Set<String?>
is StringSetOrNullPref -> value = newValue as? Set<String?>
}
@Stable
@Composable
inline fun <R, T> PrefDelegate<T>.collectAsState(crossinline transform: @DisallowComposableCalls (T) -> R): State<R> {
val flow = remember { valueFlow().map { transform(it) } }
val init = getValue()
return flow.collectAsState(transform(init))
}
@Stable
@Composable
fun <T> PrefDelegate<T>.collectAsState(): State<T> {
val flow = remember { valueFlow() }
val init = getValue()
return flow.collectAsState(init)
}
@Stable
@Composable
fun <T> PrefDelegate<T>.asMutableState(): MutableState<T> {
val flow = remember { valueFlow() }
val init = getValue()
val readOnly = flow.collectAsState(init)
return remember {
object : MutableState<T> {
override var value: T
get() = readOnly.value
set(value) {
setValue(value)
}
override fun component1() = TODO()
override fun component2() = TODO()
}
}
}
import java.util.Locale
import kotlin.reflect.KProperty
object Settings : DataStorePreferences(null) {
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
@@ -101,37 +24,30 @@ object Settings : DataStorePreferences(null) {
var favCount by intArrayPref("fav_count", 10).emitTo(_favFlow)
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
val listMode = intPref("list_mode_2", 0)
val detailSize = intPref("detail_size_2", 0)
val thumbColumns = intPref("thumb_columns", 3)
val listThumbSize = intPref("list_tile_size", 40)
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
val predictiveNavAnim = boolPref("predictive_nav_anim", true)
val needSignInFlow: Flow<Boolean>
var downloadScheme by stringOrNullPref("image_scheme", null)
var downloadAuthority by stringOrNullPref("image_authority", null)
var downloadPath by stringOrNullPref("image_path", 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_3", 1000)
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 downloadTimeout by intPref("download_timeout", 60)
var theme by intPref("theme_2", -1).observed { updateWhenThemeChanges() }
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 commentThreshold by intPref("comment_threshold", -101)
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() }
var hideHvEvents by boolPref("hide_hv_events", false)
var showJpnTitle by boolPref("show_jpn_title", false)
var showGalleryPages by boolPref("show_gallery_pages", true)
var showGalleryPages by boolPref("show_gallery_pages", false)
var showTagTranslations by boolPref(KEY_SHOW_TAG_TRANSLATIONS, false).observed { updateWhenTagTranslationChanges() }
var meteredNetworkWarning by boolPref("cellular_network_warning", false)
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
var enabledSecurity by boolPref("enable_secure", false)
var useCIUpdateChannel by boolPref("ci_update_channel", false)
@@ -142,22 +58,21 @@ object Settings : DataStorePreferences(null) {
var saveCrashLog by boolPref("save_crash_log", true)
var security by boolPref("require_unlock", false)
var removeImageFiles by boolPref("include_pic", true)
var needSignIn by boolPref("need_sign_in", true).also { needSignInFlow = it.valueFlow() }
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 enableCronet by boolPref("enable_cronet", true)
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)
var securityDelay by intPref("require_unlock_delay", 0)
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
var listThumbSize by intPref("list_tile_size", 40)
var searchCategory by intPref("search_pref", EhUtils.ALL_CATEGORY)
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
var updateIntervalDays by intPref("update_interval_days", 0)
var recentToplist by stringPref("recent_toplist", "11")
var userAgent by stringPref("user_agent", CHROME_USER_AGENT)
var recentDownloadLabel by stringOrNullPref("recent_download_label", null)
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
var displayName by stringOrNullPref("display_name", null)
@@ -165,10 +80,6 @@ object Settings : DataStorePreferences(null) {
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
var lastDawnDay by longPref("last_dawn_day", 0)
var lastUpdateDay by longPref("last_update_day", 0)
var favDialogTheta by floatPref("fav_select_dialog_delta", 0F)
// TODO: Remove this after swipe gestures are correctly handled in compose
var touchSlopFactor by intPref("touch_slop", 3)
init {
if ("CN" == Locale.getDefault().country) {
@@ -184,10 +95,15 @@ 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() }
@Suppress("ktlint:standard:property-naming")
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): IntArray = _value.map { it.value }.toIntArray()
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: IntArray) {
@@ -198,8 +114,6 @@ object Settings : DataStorePreferences(null) {
private fun stringArrayPref(key: String, count: Int, defMetaValue: String) = object : Delegate<Array<String>> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
@Suppress("ktlint:standard:property-naming")
private var _value = (0 until count).map { stringPref("${key}_$it", "$defMetaValue $it") }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): Array<String> = _value.map { it.value }.toTypedArray()
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Array<String>) {
@@ -1,6 +1,5 @@
package com.hippo.ehviewer
import android.app.UiModeManager
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
import androidx.core.os.LocaleListCompat
@@ -9,7 +8,6 @@ import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.util.isAtLeastS
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -19,7 +17,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import splitties.init.appCtx
import splitties.preferences.PrefDelegate
import splitties.systemservices.uiModeManager
private val collectScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun <T, R : PrefDelegate<T>> R.observed(func: (Unit) -> Unit) = apply { collectScope.launch { changesFlow().collect(func) } }
@@ -47,14 +44,6 @@ fun updateWhenKeepMediaStatusChanges() {
fun updateWhenThemeChanges() {
collectScope.launch {
delay(100) // Avoid recompose being cancelled
if (isAtLeastS) {
val mode = when (Settings.theme) {
AppCompatDelegate.MODE_NIGHT_NO -> UiModeManager.MODE_NIGHT_NO
AppCompatDelegate.MODE_NIGHT_YES -> UiModeManager.MODE_NIGHT_YES
else -> UiModeManager.MODE_NIGHT_AUTO
}
uiModeManager.setApplicationNightMode(mode)
}
AppCompatDelegate.setDefaultNightMode(Settings.theme)
}
}
@@ -62,7 +51,7 @@ fun updateWhenThemeChanges() {
fun updateWhenAmoledModeChanges() {
collectScope.launch {
delay(100) // Avoid recompose being cancelled
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY) // Trigger onNightModeChanged
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY)
AppCompatDelegate.setDefaultNightMode(Settings.theme)
}
}
@@ -16,11 +16,6 @@
package com.hippo.ehviewer.client
import android.webkit.CookieManager
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastJoinToString
import androidx.compose.ui.util.fastMapNotNull
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
@@ -29,15 +24,13 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
object EhCookieStore : CookieJar {
private val manager = CookieManager.getInstance()
fun signOut() = manager.removeAllCookies(null)
fun contains(url: HttpUrl, name: String) = get(url).fastAny { it.name == name }
fun contains(url: HttpUrl, name: String) = get(url).any { it.name == name }
fun get(url: HttpUrl): List<Cookie> {
val cookies = manager.getCookie(url.toString())
return if (!cookies.isNullOrEmpty()) {
cookies.split(";").fastMapNotNull { setCookie ->
Cookie.parse(url, setCookie)?.takeUnless { it.name == KEY_UTMP_NAME }
}
return if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(url, it) }.filterNot { it.name == KEY_UTMP_NAME }
} else {
emptyList()
}
@@ -64,12 +57,9 @@ object EhCookieStore : CookieJar {
}.build()
fun copyNecessaryCookies() {
val cookies = get(EhUrl.HOST_E.toHttpUrl())
cookies.fastForEach {
if (it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH) {
manager.setCookie(EhUrl.HOST_EX, it.toString())
}
}
val cookie = get(EhUrl.HOST_E.toHttpUrl()).filter { it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH || it.name == KEY_IGNEOUS }
cookie.forEach { manager.setCookie(EhUrl.HOST_EX, it.toString()) }
flush()
}
fun deleteCookie(url: HttpUrl, name: String) {
@@ -77,29 +67,35 @@ object EhCookieStore : CookieJar {
}
fun addCookie(cookie: Cookie) {
manager.setCookie(if (EhUrl.DOMAIN_E == cookie.domain) EhUrl.HOST_E else EhUrl.HOST_EX, cookie.toString())
if (EhUrl.DOMAIN_E in cookie.domain) manager.setCookie(EhUrl.HOST_E, cookie.toString()) else manager.setCookie(EhUrl.DOMAIN_EX, cookie.toString())
}
fun flush() = manager.flush()
fun getCookieHeader(url: HttpUrl): String {
return loadForRequest(url).fastJoinToString("; ") {
"${it.name}=${it.value}"
val cookies = loadForRequest(url)
val cookieHeader = StringBuilder()
var i = 0
val size = cookies.size
while (i < size) {
if (i > 0) {
cookieHeader.append("; ")
}
val cookie = cookies[i]
cookieHeader.append(cookie.name).append('=').append(cookie.value)
i++
}
return cookieHeader.toString()
}
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.fastForEach {
if (it.name != KEY_UTMP_NAME) {
manager.setCookie(url.toString(), it.toString())
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.filterNot { it.name == KEY_UTMP_NAME }.forEach { manager.setCookie(url.toString(), it.toString()) }.also { flush() }
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val checkTips = EhUrl.DOMAIN_E in url.host
return get(url).run {
if (checkTips) {
fastFilter { it.name != KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
filterNot { it.name == KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
} else {
this
}
@@ -21,7 +21,6 @@ import arrow.fx.coroutines.parZip
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.InsufficientFundsException
@@ -35,6 +34,7 @@ import com.hippo.ehviewer.client.parser.GalleryApiParser
import com.hippo.ehviewer.client.parser.GalleryDetailParser
import com.hippo.ehviewer.client.parser.GalleryListParser
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
import com.hippo.ehviewer.client.parser.GalleryPageApiParser
import com.hippo.ehviewer.client.parser.GalleryPageParser
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
import com.hippo.ehviewer.client.parser.HomeParser
@@ -48,50 +48,57 @@ import com.hippo.ehviewer.client.parser.VoteTagParser
import com.hippo.ehviewer.cronet.awaitBodyFully
import com.hippo.ehviewer.cronet.cronetRequest
import com.hippo.ehviewer.cronet.execute
import com.hippo.ehviewer.cronet.withRequestBody
import com.hippo.ehviewer.dailycheck.showEventNotification
import com.hippo.ehviewer.dailycheck.today
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.StatusCodeException
import com.hippo.ehviewer.util.isCronetSupported
import io.ktor.utils.io.pool.DirectByteBufferPool
import io.ktor.utils.io.pool.useInstance
import java.io.File
import java.nio.ByteBuffer
import kotlin.math.ceil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonArray
import kotlinx.serialization.json.put
import moe.tarsin.coroutines.removeAllSuspend
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.Headers
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import org.jsoup.Jsoup
import splitties.init.appCtx
import java.io.File
import java.nio.ByteBuffer
import kotlin.math.ceil
private const val TAG = "EhEngine"
private const val MAX_REQUEST_SIZE = 25
private const val SAD_PANDA_DISPOSITION = "inline; filename=\"sadpanda.jpg\""
private const val SAD_PANDA_TYPE = "image/gif"
private const val SAD_PANDA_LENGTH = "9615"
private const val KOKOMADE_URL = "https://exhentai.org/img/kokomade.jpg"
private const val U_CONFIG_TEXT = "Selected Profile"
private val MEDIA_TYPE_JPEG: MediaType = "image/jpeg".toMediaType()
private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
private fun rethrowExactly(code: Int, headers: Headers, body: String, e: Throwable): Nothing {
// Don't translate coroutine cancellation
if (e is CancellationException) throw e
// Check sad panda
if (SAD_PANDA_DISPOSITION == headers["Content-Disposition"] && SAD_PANDA_TYPE == headers["Content-Type"] && SAD_PANDA_LENGTH == headers["Content-Length"]) {
throw EhException("Sad Panda")
}
// Check sad panda(without panda)
if (body.isEmpty()) {
if (EhUtils.isExHentai) {
throw EhException("Sad Panda\n(without panda)")
} else {
throw EhException("IP banned")
}
if ("text/html; charset=UTF-8" == headers["Content-Type"] && "0" == headers["Content-Length"] && EhUtils.isExHentai) {
throw EhException("Sad Panda\n(without panda)")
}
// Check kokomade
if (body.contains(KOKOMADE_URL)) {
throw EhException("今回はここまで ${appCtx.getString(R.string.kokomade_tip)}".trimIndent())
}
// Check Gallery Not Available
@@ -107,12 +114,14 @@ private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
throw StatusCodeException(code)
}
if (e is ParseException || e is SerializationException) {
if ("<" !in body) {
if (e is ParseException) {
if (body.isEmpty()) {
throw EhException(appCtx.getString(R.string.error_empty_html))
} 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))
}
}
@@ -120,53 +129,33 @@ private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
throw e
}
val httpContentPool = DirectByteBufferPool(8, 0x80000)
val httpContentPool = DirectByteBufferPool(16, 16384)
suspend fun <T> fetchCompat(
url: String,
referer: String? = null,
origin: String? = null,
reqBody: RequestBody? = null,
parser: suspend (ByteBuffer) -> T,
): T {
Log.d(TAG, url)
fun commonChecks(body: ByteBuffer) {
// Check sad panda(without panda)
if (!body.hasRemaining()) {
if (EhUtils.isExHentai) {
throw EhException("Sad Panda\n(without panda)")
} else {
throw EhException("IP banned")
}
}
}
suspend inline fun <T> fetchCompat(url: String, referer: String? = null, crossinline parser: (ByteBuffer) -> T): T {
return if (isCronetSupported) {
cronetRequest(url, referer, origin) {
reqBody?.let { withRequestBody(it) }
}.execute {
@Suppress("NewApi")
val code = it.httpStatusCode
if (code >= 400) {
throw StatusCodeException(code)
}
httpContentPool.useInstance { buffer ->
awaitBodyFully(buffer)
cronetRequest(url, referer).execute {
val buffer = httpContentPool.borrow()
try {
awaitBodyFully {
buffer.put(it)
}
buffer.flip()
commonChecks(buffer)
parser(buffer)
} finally {
httpContentPool.recycle(buffer)
}
}
} else {
ehRequest(url, referer, origin) {
reqBody?.let { post(it) }
}.execute {
httpContentPool.useInstance { buffer ->
ehRequest(url, referer).execute {
val buffer = httpContentPool.borrow()
try {
while (true) {
if (body.source().read(buffer) == -1) break
}
buffer.flip()
commonChecks(buffer)
parser(buffer)
} finally {
httpContentPool.recycle(buffer)
}
}
}
@@ -180,7 +169,7 @@ private suspend inline fun <T> Request.executeAndParsingWith(block: suspend Stri
try {
block(body)
} catch (e: Exception) {
rethrowExactly(code, body, e)
rethrowExactly(code, headers, body, e)
}
}
}
@@ -199,7 +188,7 @@ object EhEngine {
suspend fun getTorrentList(url: String, gid: Long, token: String?): TorrentResult {
val referer = EhUrl.getGalleryDetailUrl(gid, token)
return fetchCompat(url, referer, parser = TorrentParser::parse)
return fetchCompat(url, referer, TorrentParser::parse)
}
suspend fun getArchiveList(url: String, gid: Long, token: String?) = ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
@@ -243,9 +232,9 @@ object EhEngine {
return ehRequest(url, referer).executeAndParsingWith(GalleryPageParser::parse)
}
suspend fun getGalleryList(url: String) = fetchCompat(url, EhUrl.referer, parser = GalleryListParser::parse)
suspend fun getGalleryList(url: String) = ehRequest(url, EhUrl.referer)
.executeAndParsingWith(GalleryListParser::parse)
.apply { fillGalleryList(galleryInfoList, url, true) }
.takeUnless { it.galleryInfoList.isEmpty() } ?: GalleryListParser.emptyResult
suspend fun getGalleryDetail(url: String) = ehRequest(url, EhUrl.referer).executeAndParsingWith {
EventPaneParser.parse(this)?.let {
@@ -259,8 +248,9 @@ object EhEngine {
GalleryDetailParser.parsePreviewList(this) to GalleryDetailParser.parsePreviewPages(this)
}
suspend fun getFavorites(url: String) = fetchCompat(url, EhUrl.referer, parser = FavoritesParser::parse)
.apply { fillGalleryList(galleryInfoList, url) }
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer)
.executeAndParsingWith(FavoritesParser::parse)
.apply { fillGalleryList(galleryInfoList, url, false) }
suspend fun signIn(username: String, password: String): String {
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
@@ -278,13 +268,13 @@ object EhEngine {
}.executeAndParsingWith(SignInParser::parse)
}
suspend fun commentGallery(url: String, comment: String, id: Long = -1) = ehRequest(url, url, EhUrl.origin) {
suspend fun commentGallery(url: String, comment: String, id: String?) = ehRequest(url, url, EhUrl.origin) {
formBody {
if (id == -1L) {
if (id == null) {
add("commenttext_new", comment)
} else {
add("commenttext_edit", comment)
add("edit_comment", id.toString())
add("edit_comment", id)
}
}
}.executeAndParsingWith {
@@ -296,21 +286,31 @@ object EhEngine {
GalleryDetailParser.parseComments(document)
}
suspend fun modifyFavorites(gid: Long, token: String?, dstCat: Int = -1, note: String = "") {
/**
* @param dstCat -1 for delete, 0 - 9 for cloud favorite, others throw Exception
* @param note max 250 characters
*/
suspend fun modifyFavorites(
gid: Long,
token: String?,
dstCat: Int = -1,
note: String = "",
) {
val catStr: String = when (dstCat) {
-1 -> "favdel"
in 0..9 -> dstCat.toString()
else -> throw EhException("Invalid dstCat: $dstCat")
}
val url = EhUrl.getAddFavorites(gid, token)
val body = formBody {
add("favcat", catStr)
add("favnote", note)
// apply=Add+to+Favorites is not necessary, just use apply=Apply+Changes all the time
add("apply", "Apply Changes")
add("update", "1")
}
fetchCompat(url, url, EhUrl.origin, body) { }
return ehRequest(url, url, EhUrl.origin) {
formBody {
add("favcat", catStr)
add("favnote", note)
// apply=Add+to+Favorites is not necessary, just use apply=Apply+Changes all the time
add("apply", "Apply Changes")
add("update", "1")
}
}.executeAndParsingWith { }
}
suspend fun downloadArchive(
@@ -363,24 +363,18 @@ object EhEngine {
}
}.executeAndParsingWith(HomeParser::parseResetLimits)
suspend fun modifyFavorites(gidArray: LongArray, srcCat: Int, dstCat: Int): FavoritesParser.Result {
val url = ehUrl {
addPathSegments(EhUrl.FAV_PATH)
if (FavListUrlBuilder.isValidFavCat(srcCat)) {
addQueryParameter("favcat", srcCat.toString())
}
}.toString()
suspend fun modifyFavorites(url: String, gidArray: LongArray, dstCat: Int): FavoritesParser.Result {
val catStr: String = when (dstCat) {
-1 -> "delete"
in 0..9 -> "fav$dstCat"
else -> throw EhException("Invalid dstCat: $dstCat")
}
val body = formBody {
add("ddact", catStr)
gidArray.forEach { add("modifygids[]", it.toString()) }
}
return fetchCompat(url, url, EhUrl.origin, body, FavoritesParser::parse)
.apply { fillGalleryList(galleryInfoList, url) }
return ehRequest(url, url, EhUrl.origin) {
formBody {
add("ddact", catStr)
gidArray.forEach { add("modifygids[]", it.toString()) }
}
}.executeAndParsingWith(FavoritesParser::parse).apply { fillGalleryList(galleryInfoList, url, false) }
}
suspend fun getGalleryPageApi(
@@ -389,7 +383,7 @@ object EhEngine {
pToken: String,
showKey: String?,
previousPToken: String?,
): GalleryPageParser.Result {
): GalleryPageApiParser.Result {
val referer = if (index > 0 && previousPToken != null) EhUrl.getPageUrl(gid, index - 1, previousPToken) else null
return ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
jsonBody {
@@ -399,7 +393,7 @@ object EhEngine {
put("imgkey", pToken)
put("showkey", showKey)
}
}.executeAndParsingWith { GalleryPageParser.parse(filterNot { it == '\\' }) }
}.executeAndParsingWith(GalleryPageApiParser::parse)
}
suspend fun rateGallery(
@@ -419,23 +413,22 @@ object EhEngine {
}
}.executeAndParsingWith(String::parseAs)
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String) =
galleryInfoList.chunked(MAX_REQUEST_SIZE).parMap(concurrency = Settings.multiThreadDownload) {
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
jsonBody {
put("method", "gdata")
array("gidlist") {
it.forEach {
addJsonArray {
add(it.gid)
add(it.token)
}
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String) = galleryInfoList.chunked(MAX_REQUEST_SIZE).parMap {
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
jsonBody {
put("method", "gdata")
array("gidlist") {
it.forEach {
addJsonArray {
add(it.gid)
add(it.token)
}
}
put("namespace", 1)
}
}.executeAndParsingWith { GalleryApiParser.parse(this, it) }
}
put("namespace", 1)
}
}.executeAndParsingWith { GalleryApiParser.parse(this, it) }
}
suspend fun voteComment(
apiUid: Long,
@@ -491,21 +484,17 @@ object EhEngine {
/**
* @param image Must be jpeg
*/
suspend fun imageSearch(image: File, uss: Boolean, osc: Boolean) = fetchCompat(
EhUrl.imageSearchUrl,
EhUrl.referer,
EhUrl.origin,
suspend fun imageSearch(image: File, uss: Boolean, osc: Boolean) = ehRequest(EhUrl.imageSearchUrl, EhUrl.referer, EhUrl.origin) {
multipartBody {
setType(MultipartBody.FORM)
addFormDataPart("sfile", "a.jpg", image.asRequestBody(MEDIA_TYPE_JPEG))
if (uss) addFormDataPart("fs_similar", "on")
if (osc) addFormDataPart("fs_covers", "on")
addFormDataPart("f_sfile", "File Search")
},
parser = GalleryListParser::parse,
).apply { fillGalleryList(galleryInfoList, EhUrl.imageSearchUrl) }
}
}.executeAndParsingWith(GalleryListParser::parse).apply { fillGalleryList(galleryInfoList, EhUrl.imageSearchUrl, true) }
private suspend fun fillGalleryList(list: MutableList<BaseGalleryInfo>, url: String, filter: Boolean = false) {
private suspend fun fillGalleryList(list: MutableList<BaseGalleryInfo>, url: String, filter: Boolean) {
// Filter title and uploader
if (filter) list.removeAllSuspend { EhFilter.filterTitle(it) || EhFilter.filterUploader(it) }
@@ -513,7 +502,7 @@ object EhEngine {
var hasPages = false
var hasRated = false
for (gi in list) {
if (!gi.simpleTags.isNullOrEmpty()) {
if (gi.simpleTags != null) {
hasTags = true
}
if (gi.pages != 0) {
@@ -530,7 +519,7 @@ object EhEngine {
if (filter) list.removeAllSuspend { EhFilter.filterUploader(it) || EhFilter.filterTag(it) || EhFilter.filterTagNamespace(it) }
}
suspend fun addFavorites(galleryList: List<Pair<Long, String>>, dstCat: Int) {
galleryList.parMap(concurrency = Settings.multiThreadDownload) { (gid, token) -> modifyFavorites(gid, token, dstCat) }
suspend fun modifyFavoritesRange(galleryList: List<Pair<Long, String>>, dstCat: Int) {
galleryList.forEach { (gid, token) -> modifyFavorites(gid, token, dstCat) }
}
}
@@ -17,17 +17,17 @@ object EhFilter : CoroutineScope {
val filters = async { EhDB.getAllFilter() as MutableList }
private suspend inline fun anyActive(mode: FilterMode, predicate: (Filter) -> Boolean) = filters.await().any { it.mode == mode && it.enable && predicate(it) }
private fun <R> Filter.launchOps(
callback: (suspend (R) -> Unit)? = null,
callback: ((R) -> Unit)? = null,
ops: suspend Filter.() -> R,
) = launch { ops().let { callback?.invoke(it) } }
fun Filter.remember(callback: (suspend (Boolean) -> Unit)? = null) = launchOps(callback) {
fun Filter.remember(callback: ((Boolean) -> Unit)? = null) = launchOps(callback) {
EhDB.addFilter(this).also { if (it) filters.await().add(this) }
}
fun Filter.trigger(callback: (suspend (Unit) -> Unit)? = null) = launchOps(callback) {
fun Filter.trigger(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
enable = !enable
EhDB.updateFilter(this)
}
fun Filter.forget(callback: (suspend (Unit) -> Unit)? = null) = launchOps(callback) {
fun Filter.forget(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
EhDB.deleteFilter(this)
filters.await().remove(this)
}
@@ -19,11 +19,6 @@ import android.util.Log
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.Settings
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -43,19 +38,22 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.executeAsync
import okio.BufferedSource
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
inline fun ehRequest(url: String, referer: String? = null, origin: String? = null, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply {
addHeader("User-Agent", Settings.userAgent)
addHeader("User-Agent", CHROME_USER_AGENT)
addHeader("Accept", CHROME_ACCEPT)
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
referer?.let { addHeader("Referer", it) }
origin?.let { addHeader("Origin", it) }
}.apply(builder).build()
inline fun formBody(builder: FormBody.Builder.() -> Unit) = FormBody.Builder().apply(builder).build()
inline fun Request.Builder.formBody(builder: FormBody.Builder.() -> Unit) = post(FormBody.Builder().apply(builder).build())
inline fun multipartBody(builder: MultipartBody.Builder.() -> Unit) = MultipartBody.Builder().apply(builder).build()
inline fun Request.Builder.multipartBody(builder: MultipartBody.Builder.() -> Unit) = post(MultipartBody.Builder().apply(builder).build())
val MEDIA_TYPE_JSON: MediaType = "application/json; charset=utf-8".toMediaType()
@@ -24,8 +24,6 @@ import com.hippo.ehviewer.R
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.copyToFile
import java.io.File
import java.nio.charset.StandardCharsets
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -44,6 +42,8 @@ import okio.blackholeSink
import okio.buffer
import okio.source
import splitties.init.appCtx
import java.io.File
import java.nio.charset.StandardCharsets
object EhTagDatabase : CoroutineScope {
private const val NAMESPACE_PREFIX = "n"
@@ -55,6 +55,7 @@ object EhTagDatabase : CoroutineScope {
)
private lateinit var tagGroups: Map<String, Map<String, String>>
private val updateLock = Mutex()
private val dbLock = Mutex()
override val coroutineContext = Dispatchers.IO + Job()
var initialized by mutableStateOf(false)
@@ -201,42 +202,42 @@ object EhTagDatabase : CoroutineScope {
}
}
private fun issueUpdateInMemoryData(file: File? = null) {
val dataFile = file ?: getMetadata(appCtx)?.let { metadata ->
val dataName = metadata[2]
val dir = AppConfig.getFilesDir("tag-translations")
File(dir, dataName).takeIf { it.exists() }
} ?: return
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
initialized = true
}
init {
launch {
updateLock.withLock {
private suspend fun issueUpdateInMemoryData() {
dbLock.withLock {
getMetadata(appCtx)?.let { urls ->
val dataName = urls[2]
val dir = AppConfig.getFilesDir("tag-translations")
val dataFile = File(dir, dataName).takeIf { it.exists() } ?: return
runSuspendCatching {
issueUpdateInMemoryData()
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
initialized = true
}.onFailure {
it.printStackTrace()
}
updateInternal()
}
}
}
init {
launch {
issueUpdateInMemoryData()
}
}
private suspend fun updateInternal() {
getMetadata(appCtx)?.let { metadata ->
val sha1Name = metadata[0]
val sha1Url = metadata[1]
val dataName = metadata[2]
val dataUrl = metadata[3]
val urls = getMetadata(appCtx)
urls?.let {
val sha1Name = urls[0]
val sha1Url = urls[1]
val dataName = urls[2]
val dataUrl = urls[3]
val dir = AppConfig.getFilesDir("tag-translations")
checkNotNull(dir)
val sha1File = File(dir, sha1Name)
val dataFile = File(dir, dataName)
runSuspendCatching {
runCatching {
// Check current sha1 and current data
val sha1 = getFileContent(sha1File)
if (!checkData(sha1, dataFile)) {
@@ -253,7 +254,7 @@ object EhTagDatabase : CoroutineScope {
if (tempSha1 == sha1) {
// The data is the same
FileUtils.delete(tempSha1File)
return
return@runCatching
}
// Save new data
@@ -264,7 +265,7 @@ object EhTagDatabase : CoroutineScope {
if (!checkData(tempSha1, tempDataFile)) {
FileUtils.delete(tempSha1File)
FileUtils.delete(tempDataFile)
return
return@runCatching
}
// Replace current sha1 and current data with new sha1 and new data
@@ -274,7 +275,7 @@ object EhTagDatabase : CoroutineScope {
tempDataFile.renameTo(dataFile)
// Read new EhTagDatabase
issueUpdateInMemoryData(dataFile)
issueUpdateInMemoryData()
}.onFailure {
it.printStackTrace()
}
@@ -24,13 +24,13 @@ object EhUrl {
const val DOMAIN_EX = "exhentai.org"
const val DOMAIN_E = "e-hentai.org"
const val HOST_EX = "https://$DOMAIN_EX/"
const val API_EX = "https://s.exhentai.org/api.php"
const val API_EX = HOST_EX + "api.php"
const val FAV_PATH = "favorites.php"
const val WATCHED_PATH = "watched"
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
const val URL_MY_TAGS_EX = HOST_EX + "mytags"
const val HOST_E = "https://$DOMAIN_E/"
const val API_E = "https://api.e-hentai.org/api.php"
const val API_E = HOST_E + "api.php"
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
const val URL_MY_TAGS_E = HOST_E + "mytags"
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
@@ -64,14 +64,10 @@ object EhUrl {
}
val apiUrl: String
get() = if (Settings.forceEhThumb) {
API_E
} else {
when (Settings.gallerySite) {
SITE_E -> API_E
SITE_EX -> API_EX
else -> API_E
}
get() = when (Settings.gallerySite) {
SITE_E -> API_E
SITE_EX -> API_EX
else -> API_E
}
val referer: String
@@ -16,13 +16,12 @@
package com.hippo.ehviewer.client
import android.annotation.SuppressLint
import androidx.compose.material3.MaterialTheme
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import arrow.core.memoize
import androidx.compose.ui.platform.LocalContext
import com.google.android.material.R
import com.google.android.material.color.MaterialColors
import com.google.android.material.color.utilities.Hct
import com.google.android.material.color.utilities.MathUtils
import com.hippo.ehviewer.Settings
@@ -103,19 +102,17 @@ object EhUtils {
}
@SuppressLint("RestrictedApi")
val harmonizeWithRole = { primaryContainer: Int, src: Int ->
fun harmonizeWithRole(context: Context, src: Int): Int {
val primary = MaterialColors.getColor(context, R.attr.colorPrimaryContainer, "EhUtils")
val fromHct = Hct.fromInt(src)
val toHct = Hct.fromInt(primaryContainer)
val toHct = Hct.fromInt(primary)
val differenceDegrees = MathUtils.differenceDegrees(fromHct.hue, toHct.hue)
val rotationDegrees = minOf(differenceDegrees * 0.5, 15.0)
val outputHue = MathUtils.sanitizeDegreesDouble(fromHct.hue + rotationDegrees * MathUtils.rotationDirection(fromHct.hue, toHct.hue))
Hct.from(outputHue, toHct.chroma, toHct.tone).toInt()
}.memoize()
return Hct.from(outputHue, toHct.chroma, toHct.tone).toInt()
}
@Stable
@ReadOnlyComposable
@Composable
fun getCategoryColor(category: Int): Color {
private fun getCategoryColor(context: Context, category: Int): Int {
val primary = when (category) {
DOUJINSHI -> BG_COLOR_DOUJINSHI
MANGA -> BG_COLOR_MANGA
@@ -129,16 +126,11 @@ object EhUtils {
MISC -> BG_COLOR_MISC
else -> BG_COLOR_UNKNOWN
}.toInt()
val color = if (Settings.harmonizeCategoryColor) {
val primaryContainer = MaterialTheme.colorScheme.primaryContainer.toArgb()
harmonizeWithRole(primaryContainer, primary)
} else {
primary
}
return Color(color)
return harmonizeWithRole(context, primary)
}
val categoryTextColor = Color(0xffe6e0e9)
@Composable
fun getCategoryColor(category: Int) = Color(getCategoryColor(LocalContext.current, category))
fun signOut() {
EhCookieStore.signOut()
@@ -148,6 +140,10 @@ object EhUtils {
Settings.needSignIn = true
}
fun needSignedIn(): Boolean {
return Settings.needSignIn
}
fun getSuitableTitle(gi: GalleryInfo): String {
return if (Settings.showJpnTitle) {
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
@@ -16,7 +16,6 @@
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
@@ -26,7 +25,7 @@ import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "GALLERIES")
data class BaseGalleryInfo @Keep constructor(
open class BaseGalleryInfo(
@PrimaryKey
@ColumnInfo(name = "GID")
override var gid: Long = 0,
@@ -22,16 +22,16 @@ import com.hippo.ehviewer.client.ehUrl
import kotlinx.parcelize.Parcelize
@Parcelize
data class FavListUrlBuilder(
val favCat: Int = FAV_CAT_ALL,
val keyword: String? = null,
class FavListUrlBuilder(
var mPrev: String? = null,
var mNext: String? = null,
var jumpTo: String? = null,
var prev: String? = null,
var next: String? = null,
var keyword: String? = null,
var favCat: Int = FAV_CAT_ALL,
) : Parcelable {
fun setIndex(index: String?, isNext: Boolean) {
next = index.takeIf { isNext }
prev = index.takeUnless { isNext }
mNext = index.takeIf { isNext }
mPrev = index.takeUnless { isNext }
}
fun build() = ehUrl {
@@ -42,8 +42,8 @@ data class FavListUrlBuilder(
addQueryParameter("favcat", "all")
}
addQueryParameterIfNotBlank("f_search", keyword)
addQueryParameterIfNotBlank("prev", prev)
addQueryParameterIfNotBlank("next", next)
addQueryParameterIfNotBlank("prev", mPrev)
addQueryParameterIfNotBlank("next", mNext)
addQueryParameterIfNotBlank("seek", jumpTo)
}.toString()
@@ -19,18 +19,20 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class GalleryComment(
val id: Long,
val score: Int,
val editable: Boolean,
val voteUpAble: Boolean,
val voteUpEd: Boolean,
val voteDownAble: Boolean,
val voteDownEd: Boolean,
val uploader: Boolean,
val voteState: String?,
val time: Long,
val user: String?,
val comment: String,
val lastEdited: Long,
class GalleryApiInfo(
var gid: Long = 0,
var token: String? = null,
var archiverKey: String? = null,
var title: String? = null,
var titleJpn: String? = null,
var category: Int = 0,
var thumb: String? = null,
var uploader: String? = null,
var posted: Long = 0,
var filecount: Int = 0,
var filesize: Long = 0,
var expunged: Boolean = false,
var rating: Float = 0f,
var torrentcount: Int = 0,
var tags: Array<String>?,
) : Parcelable
@@ -0,0 +1,37 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryComment(
// 0 for uploader comment. can't vote
var id: Long = 0,
var score: Int = 0,
var editable: Boolean = false,
var voteUpAble: Boolean = false,
var voteUpEd: Boolean = false,
var voteDownAble: Boolean = false,
var voteDownEd: Boolean = false,
var uploader: Boolean = false,
var voteState: String? = null,
var time: Long = 0,
var user: String? = null,
var comment: String? = null,
var lastEdited: Long = 0,
) : Parcelable
@@ -19,7 +19,7 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class GalleryCommentList(
val comments: List<GalleryComment>,
class GalleryCommentList(
var comments: Array<GalleryComment>,
val hasMore: Boolean,
) : Parcelable
@@ -37,7 +37,7 @@ private val LANGUAGES = arrayOf(
)
@Parcelize
data class GalleryDetail(
class GalleryDetail(
val galleryInfo: BaseGalleryInfo = BaseGalleryInfo(),
var apiUid: Long = -1L,
var apiKey: String? = null,
@@ -51,7 +51,7 @@ data class GalleryDetail(
var size: String? = null,
var favoriteCount: Int = 0,
var ratingCount: Int = 0,
val tags: List<GalleryTagGroup>,
val tags: Array<GalleryTagGroup>,
var comments: GalleryCommentList,
val previewPages: Int,
val previewList: List<GalleryPreview>,
@@ -16,7 +16,6 @@
package com.hippo.ehviewer.client.data
import java.util.regex.Pattern
import moe.tarsin.kt.unreachable
interface GalleryInfo {
var gid: Long
@@ -44,29 +43,67 @@ interface GalleryInfo {
?: title?.let { generateSLangFromTitle(it) }
}
private fun generateSLangFromTags(simpleTags: List<String>): String? {
for (tag in simpleTags) {
for (i in S_LANGS.indices) {
if (S_LANG_TAGS[i] == tag) {
return S_LANGS[i]
}
}
}
return null
}
private fun generateSLangFromTitle(title: String): String? {
for (i in S_LANGS.indices) {
if (S_LANG_PATTERNS[i].matcher(title).find()) {
return S_LANGS[i]
}
}
return null
}
companion object {
/**
* ISO 639-1
*/
const val S_LANG_JA = "JA"
const val S_LANG_EN = "EN"
const val S_LANG_ZH = "ZH"
const val S_LANG_NL = "NL"
const val S_LANG_FR = "FR"
const val S_LANG_DE = "DE"
const val S_LANG_HU = "HU"
const val S_LANG_IT = "IT"
const val S_LANG_KO = "KO"
const val S_LANG_PL = "PL"
const val S_LANG_PT = "PT"
const val S_LANG_RU = "RU"
const val S_LANG_ES = "ES"
const val S_LANG_TH = "TH"
const val S_LANG_VI = "VI"
val S_LANGS = arrayOf(
"EN",
"ZH",
"ES",
"KO",
"RU",
"FR",
"PT",
"TH",
"DE",
"IT",
"VI",
"PL",
"HU",
"NL",
S_LANG_EN,
S_LANG_ZH,
S_LANG_ES,
S_LANG_KO,
S_LANG_RU,
S_LANG_FR,
S_LANG_PT,
S_LANG_TH,
S_LANG_DE,
S_LANG_IT,
S_LANG_VI,
S_LANG_PL,
S_LANG_HU,
S_LANG_NL,
)
private val S_LANG_PATTERNS = arrayOf(
val S_LANG_PATTERNS = arrayOf(
Pattern.compile(
"[(\\[]eng(?:lish)?[)\\]]|英訳",
Pattern.CASE_INSENSITIVE,
),
// [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
), // [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
Pattern.compile(
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
Pattern.CASE_INSENSITIVE,
@@ -93,7 +130,7 @@ interface GalleryInfo {
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
)
private val S_LANG_TAGS = arrayOf(
val S_LANG_TAGS = arrayOf(
"language:english",
"language:chinese",
"language:spanish",
@@ -110,39 +147,7 @@ interface GalleryInfo {
"language:dutch",
)
private fun generateSLangFromTags(simpleTags: List<String>): String? {
for (tag in simpleTags) {
for (i in S_LANGS.indices) {
if (S_LANG_TAGS[i] == tag) {
return S_LANGS[i]
}
}
}
return null
}
private fun generateSLangFromTitle(title: String): String? {
for (i in S_LANGS.indices) {
if (S_LANG_PATTERNS[i].matcher(title).find()) {
return S_LANGS[i]
}
}
return null
}
const val NOT_FAVORITED = -2
const val LOCAL_FAVORITED = -1
}
}
fun GalleryInfo.findBaseInfo(): BaseGalleryInfo {
return when (this) {
is BaseGalleryInfo -> this
is GalleryDetail -> galleryInfo
else -> unreachable()
}
}
fun GalleryInfo.asGalleryDetail(): GalleryDetail? {
return this as? GalleryDetail
}
@@ -19,7 +19,7 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class GalleryTagGroup(
class GalleryTagGroup(
val groupName: String,
private val tagList: MutableList<String> = mutableListOf(),
) : Parcelable, MutableList<String> by tagList
private val mTagList: MutableList<String> = mutableListOf(),
) : Parcelable, MutableList<String> by mTagList
@@ -25,9 +25,9 @@ import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.ui.main.AdvanceTable
import com.hippo.ehviewer.util.encodeUTF8
import com.hippo.ehviewer.util.toIntOrDefault
import kotlinx.parcelize.Parcelize
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import kotlinx.parcelize.Parcelize
@Parcelize
data class ListUrlBuilder(
@@ -35,7 +35,6 @@ data class ListUrlBuilder(
var mode: Int = MODE_NORMAL,
private var mPrev: String? = null,
var mNext: String? = null,
// Reset to null after initial loading
var mJumpTo: String? = null,
var category: Int = EhUtils.NONE,
private var mKeyword: String? = null,
@@ -49,6 +48,23 @@ data class ListUrlBuilder(
var isOnlySearchCovers: Boolean = false,
) : Parcelable {
fun reset() {
mode = MODE_NORMAL
mPrev = null
mNext = null
mJumpTo = null
this.category = EhUtils.NONE
mKeyword = null
advanceSearch = -1
minRating = -1
pageFrom = -1
pageTo = -1
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
hash = null
}
fun setIndex(index: String?, isNext: Boolean = true) {
mNext = index?.takeIf { isNext }
mPrev = index?.takeUnless { isNext }
@@ -64,7 +80,8 @@ data class ListUrlBuilder(
mKeyword = keyword
}
constructor(q: QuickSearch) : this() {
fun set(q: QuickSearch) {
reset()
mode = q.mode
this.category = q.category
mKeyword = q.keyword
@@ -117,8 +134,9 @@ data class ListUrlBuilder(
/**
* @param query xxx=yyy&mmm=nnn
*/
constructor(query: String?) : this() {
// TODO page
// TODO page
fun setQuery(query: String?) {
reset()
if (query.isNullOrEmpty()) {
return
}
@@ -302,65 +320,66 @@ data class ListUrlBuilder(
// Pages
if (pageFrom != -1 || pageTo != -1) {
addQueryParameter("f_sp", "on")
addQueryParameterIfNotBlank(
addQueryParameter(
"f_spf",
pageFrom.takeUnless { it == -1 }?.toString(),
(if (pageFrom != -1) pageFrom.toString() else "").toString(),
)
addQueryParameterIfNotBlank(
addQueryParameter(
"f_spt",
pageTo.takeUnless { it == -1 }?.toString(),
(if (pageTo != -1) pageTo.toString() else "").toString(),
)
}
}
}.toString()
MODE_UPLOADER -> {
buildString {
append(EhUrl.host)
mKeyword?.let {
append("uploader/").append(encodeUTF8(it))
}
mPrev?.let {
append("?prev=").append(it)
}
mNext?.let {
append("?next=").append(it)
}
mJumpTo?.let {
append("&seek=").append(it)
}
val sb = StringBuilder(EhUrl.host)
mKeyword?.let {
sb.append("uploader/")
sb.append(encodeUTF8(it))
}
mPrev?.let {
sb.append("?prev=").append(it)
}
mNext?.let {
sb.append("?next=").append(it)
}
mJumpTo?.let {
sb.append("&seek=").append(it)
}
sb.toString()
}
MODE_TAG -> {
buildString {
append(EhUrl.host)
mKeyword?.let {
append("tag/").append(encodeUTF8(it))
}
mPrev?.let {
append("?prev=").append(it)
}
mNext?.let {
append("?next=").append(it)
}
mJumpTo?.let {
append("&seek=").append(it)
}
val sb = StringBuilder(EhUrl.host)
mKeyword?.let {
sb.append("tag/")
sb.append(encodeUTF8(it))
}
mPrev?.let {
sb.append("?prev=").append(it)
}
mNext?.let {
sb.append("?next=").append(it)
}
mJumpTo?.let {
sb.append("&seek=").append(it)
}
sb.toString()
}
MODE_WHATS_HOT -> EhUrl.popularUrl
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
MODE_TOPLIST -> {
buildString {
append(EhUrl.HOST_E)
append("toplist.php?tl=")
append(mKeyword!!)
mJumpTo?.let {
append("&p=").append(it)
}
val sb = StringBuilder(EhUrl.HOST_E)
sb.append("toplist.php?tl=")
mKeyword.orEmpty().let {
sb.append(encodeUTF8(it))
}
mJumpTo?.let {
sb.append("&p=").append(it)
}
sb.toString()
}
else -> throw IllegalStateException("Unexpected value: $mode")
@@ -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
}
}
@@ -1,19 +1,18 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.NotLoggedInException
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import moe.tarsin.coroutines.runSuspendCatching
import splitties.init.appCtx
object FavoritesParser {
suspend fun parse(body: ByteBuffer): Result {
val catArray = arrayOfNulls<String>(10)
val countArray = runSuspendCatching {
parseFav(body, favCat = catArray).also { check(it.isNotEmpty()) }
}.getOrElse {
if (it is RuntimeException && it.message == "Not logged in!") throw NotLoggedInException()
throw ParseException("Parse favorites error", it)
suspend fun parse(body: String): Result {
if (body.contains("This page requires you to log on.</p>")) {
throw EhException(appCtx.getString(R.string.need_sign_in))
}
val catArray = arrayOfNulls<String>(10)
val countArray = parseFav(body, catArray)
if (countArray.isEmpty()) throw ParseException("Parse favorites error", body)
val result = GalleryListParser.parse(body)
return Result(catArray.requireNoNulls(), countArray, result)
}
@@ -29,4 +28,4 @@ object FavoritesParser {
}
}
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit(), favCat: Array<String?>): IntArray
private external fun parseFav(body: String, favCat: Array<String?>): IntArray
@@ -33,7 +33,7 @@ object GalleryApiParser {
titleJpn = item.titleJpn.unescapeXml()
category = getCategory(item.category)
thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!)
uploader = item.uploader?.unescapeXml()
uploader = item.uploader.unescapeXml()
posted = ParserUtils.formatDate(item.posted * 1000)
rating = item.rating
simpleTags = item.tags
@@ -54,8 +54,7 @@ object GalleryApiParser {
val titleJpn: String,
val category: String,
val thumb: String,
// Null in some old galleries
val uploader: String?,
val uploader: String,
val posted: Long,
@SerialName("filecount")
val pages: Int,
@@ -17,7 +17,6 @@ package com.hippo.ehviewer.client.parser
import arrow.core.unzip
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.EhUtils.getCategory
@@ -42,10 +41,6 @@ import com.hippo.ehviewer.util.toFloatOrDefault
import com.hippo.ehviewer.util.toIntOrDefault
import com.hippo.ehviewer.util.trimAnd
import com.hippo.ehviewer.util.unescapeXml
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
@@ -53,6 +48,10 @@ import org.jsoup.nodes.Node
import org.jsoup.select.Elements
import org.jsoup.select.NodeTraversor
import org.jsoup.select.NodeVisitor
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
object GalleryDetailParser {
private val PATTERN_ERROR = Regex("<div class=\"d\">\n<p>([^<]+)</p>")
@@ -76,7 +75,8 @@ object GalleryDetailParser {
private val PATTERN_NEWER_DATE = Regex(", added (.+?)<br />")
private val PATTERN_FAVORITE_SLOT =
Regex("/fav.png\\); background-position:0px -(\\d+)px")
private val EMPTY_GALLERY_COMMENT_LIST = GalleryCommentList(emptyList(), false)
private val EMPTY_GALLERY_TAG_GROUP_ARRAY = arrayOf<GalleryTagGroup>()
private val EMPTY_GALLERY_COMMENT_ARRAY = GalleryCommentList(arrayOf(), false)
private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC)
private const val OFFENSIVE_STRING =
@@ -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
@@ -307,7 +307,7 @@ object GalleryDetailParser {
/**
* Parse tag groups with html parser
*/
private fun parseTagGroups(document: Document): List<GalleryTagGroup> {
private fun parseTagGroups(document: Document): Array<GalleryTagGroup> {
return try {
val taglist = document.getElementById("taglist")!!
val tagGroups = taglist.child(0).child(0).children()
@@ -315,69 +315,73 @@ object GalleryDetailParser {
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
emptyList()
EMPTY_GALLERY_TAG_GROUP_ARRAY
}
}
private fun parseTagGroups(trs: Elements): List<GalleryTagGroup> {
private fun parseTagGroups(trs: Elements): Array<GalleryTagGroup> {
return try {
trs.mapNotNull { parseTagGroup(it) }
trs.mapNotNull { parseTagGroup(it) }.toTypedArray()
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
emptyList()
EMPTY_GALLERY_TAG_GROUP_ARRAY
}
}
private suspend fun parseComment(element: Element): GalleryComment? {
return try {
val comment = GalleryComment()
// Id
val a = element.previousElementSibling()
val name = a!!.attr("name")
val id = name trimAnd { substring(1).toInt().toLong() }
comment.id = name trimAnd { substring(1).toInt().toLong() }
// Editable, vote up and vote down
val c4 = element.getElementsByClass("c4").first()
var voteUpAble = false
var voteUpEd = false
var voteDownAble = false
var voteDownEd = false
var editable = false
var uploader = false
if (null != c4) {
if ("Uploader Comment" == c4.text()) {
uploader = true
comment.uploader = true
}
for (e in c4.children()) {
when (e.text()) {
"Vote+" -> {
voteUpAble = true
voteUpEd = e.attr("style").trim().isNotEmpty()
comment.voteUpAble = true
comment.voteUpEd = e.attr("style").trim().isNotEmpty()
}
"Vote-" -> {
voteDownAble = true
voteDownEd = e.attr("style").trim().isNotEmpty()
comment.voteDownAble = true
comment.voteDownEd = e.attr("style").trim().isNotEmpty()
}
"Edit" -> editable = true
"Edit" -> comment.editable = true
}
}
}
// Vote state
val c7 = element.getElementsByClass("c7").first()
val voteState = c7?.text()?.trim()
if (null != c7) {
comment.voteState = c7.text().trim()
}
// Score
val c5 = element.getElementsByClass("c5").first()
val score = c5?.children()?.first()?.text()?.trim()?.toIntOrDefault(0) ?: 0
if (null != c5) {
val es = c5.children()
if (!es.isEmpty()) {
comment.score = es[0].text().trim().toIntOrDefault(0)
}
}
// time
val c3 = element.getElementsByClass("c3").first()
val temp = c3!!.ownText()
val (timeStr, user) = if (temp.endsWith(':')) {
temp.substring("Posted on ".length, temp.length - " by:".length) to c3.child(0).text()
val time = if (temp.endsWith(':')) {
// user
comment.user = c3.child(0).text()
temp.substring("Posted on ".length, temp.length - " by:".length)
} else {
temp.substring("Posted on ".length) to null
temp.substring("Posted on ".length)
}
val time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(timeStr)).toEpochMilli()
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).toEpochMilli()
// comment
val c6 = element.getElementsByClass("c6").first()
// fix underline support
@@ -386,21 +390,24 @@ object GalleryDetailParser {
e.tagName("u")
}
}
val commentHtml = c6.html()
comment.comment = c6.html()
// filter comment
if (!uploader) {
if (!comment.uploader) {
val sEhFilter = EhFilter
if (score <= Settings.commentThreshold || sEhFilter.filterCommenter(user.orEmpty()) || sEhFilter.filterComment(commentHtml)) {
if (sEhFilter.filterCommenter(comment.user!!) || sEhFilter.filterComment(comment.comment!!)) {
return null
}
}
// last edited
val c8 = element.getElementsByClass("c8").first()
val lastEdited = c8?.children()?.first()?.run { Instant.from(WEB_COMMENT_DATE_FORMAT.parse(text())).toEpochMilli() } ?: 0
GalleryComment(
id, score, editable, voteUpAble, voteUpEd, voteDownAble,
voteDownEd, uploader, voteState, time, user, commentHtml, lastEdited,
)
if (c8 != null) {
val e = c8.children().first()
if (e != null) {
comment.lastEdited =
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(e.text())).toEpochMilli()
}
}
comment
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
@@ -430,11 +437,11 @@ object GalleryDetailParser {
},
chd!!,
)
GalleryCommentList(list, hasMore)
GalleryCommentList(list.toTypedArray(), hasMore)
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
EMPTY_GALLERY_COMMENT_LIST
EMPTY_GALLERY_COMMENT_ARRAY
}
}
@@ -443,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)
}
/**
@@ -451,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>> {
@@ -0,0 +1,38 @@
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 {
parseGalleryInfoList(body).apply {
galleryInfoList.onEach {
if (it.favoriteSlot == NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
it.favoriteSlot = LOCAL_FAVORITED
}
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", body, it)
}
}
class GalleryListResult(
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
)
private external fun parseGalleryInfoList(e: String): GalleryListResult
@@ -42,7 +42,9 @@ object GalleryListUrlParser {
}
val path = url.path ?: return null
return if (PATH_NORMAL == path || path.isEmpty()) {
ListUrlBuilder(url.query)
val builder = ListUrlBuilder()
builder.setQuery(url.query)
builder
} else if (path.startsWith(PATH_UPLOADER)) {
parseUploader(path)
} else if (path.startsWith(PATH_TAG)) {
@@ -55,7 +57,10 @@ object GalleryListUrlParser {
} catch (e: NumberFormatException) {
return null
}
ListUrlBuilder(url.query).also { it.category = category }
val builder = ListUrlBuilder()
builder.setQuery(url.query)
builder.category = category
builder
} else {
null
}
@@ -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)
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.unescapeXml
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object GalleryPageApiParser {
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
fun parse(body: String): Result {
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error, body) }
val res = body.parseAs<JsonResult>()
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
groupValues[1].trim().unescapeXml()
}
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKey)?.run {
groupValues[1].trim().unescapeXml()
}
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.originImageUrl)?.run {
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
}
if (!imageUrl.isNullOrEmpty()) {
return Result(imageUrl, skipHathKey, originImageUrl)
} else {
throw ParseException("Parse image url and skip hath key error", body)
}
}
@Serializable
private data class JsonResult(
@SerialName("i3")
val imageUrl: String,
@SerialName("i6")
val skipHathKey: String,
@SerialName("i7")
val originImageUrl: String,
)
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
}
@@ -0,0 +1,69 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml
import java.util.regex.Pattern
object GalleryPageParser {
private val PATTERN_IMAGE_URL = Pattern.compile("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^)]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL =
Pattern.compile("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
// TODO Not sure about the size of show keys
private val PATTERN_SHOW_KEY = Pattern.compile("var showkey=\"([0-9a-z]+)\";")
fun parse(body: String): Result {
var m = PATTERN_IMAGE_URL.matcher(body)
val imageUrl = if (m.find()) {
m.group(1)!!.trim().unescapeXml()
} else {
null
}
m = PATTERN_SKIP_HATH_KEY.matcher(body)
val skipHathKey = if (m.find()) {
m.group(1)!!.trim().unescapeXml()
} else {
null
}
m = PATTERN_ORIGIN_IMAGE_URL.matcher(body)
val originImageUrl = if (m.find()) {
m.group(1)!!.unescapeXml() + "fullimg.php" + m.group(2)!!.unescapeXml()
} else {
null
}
m = PATTERN_SHOW_KEY.matcher(body)
val showKey = if (m.find()) {
m.group(1)
} else {
null
}
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {
Result(imageUrl, skipHathKey, originImageUrl, showKey)
} else {
throw ParseException("Parse image url and show error", body)
}
}
class Result(
val imageUrl: String,
val skipHathKey: String?,
val originImageUrl: String?,
val showKey: String,
)
}
@@ -1,19 +1,16 @@
package com.hippo.ehviewer.client.parser
import android.os.Parcelable
import androidx.annotation.Keep
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlinx.parcelize.Parcelize
import java.nio.ByteBuffer
object HomeParser {
private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
private const val INSUFFICIENT_FUNDS = "Insufficient funds."
fun parse(body: ByteBuffer) = runCatching {
parseLimit(body)
}.getOrElse { throw ParseException("Parse image limits error", it) }
fun parse(body: ByteBuffer) = parseLimit(body)
fun parseResetLimits(body: String) {
if (body.contains(INSUFFICIENT_FUNDS)) {
@@ -27,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
@@ -36,6 +33,6 @@ object HomeParser {
}
@Parcelize
data class Limits @Keep constructor(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Limits
@@ -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,16 +1,12 @@
package com.hippo.ehviewer.client.parser
import androidx.annotation.Keep
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 @Keep constructor(
class Torrent(
val posted: String,
val size: String,
val seeds: Int,
@@ -22,6 +18,6 @@ class Torrent @Keep constructor(
fun Torrent.format() = "[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloads]"
typealias TorrentResult = List<Torrent>
typealias TorrentResult = ArrayList<Torrent>
private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): ArrayList<Torrent>
@@ -15,7 +15,6 @@ import coil.request.Options
import com.hippo.ehviewer.cronet.copyToChannel
import com.hippo.ehviewer.cronet.cronetRequest
import com.hippo.ehviewer.cronet.execute
import com.hippo.ehviewer.cronet.noCache
import java.io.RandomAccessFile
/**
@@ -28,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) {
noCache()
disableCache()
}.execute {
diskCache.suspendEdit(diskCacheKey) {
RandomAccessFile(data.toFile(), "rw").use {
@@ -3,8 +3,6 @@ package com.hippo.ehviewer.coil
import coil.intercept.Interceptor
import coil.request.ImageResult
import com.hippo.ehviewer.Settings
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -13,6 +11,8 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
object MergeInterceptor : Interceptor {
private val pendingContinuationMap: HashMap<String, MutableList<CancellableContinuation<Unit>>> = hashMapOf()
@@ -1,38 +1,46 @@
package com.hippo.ehviewer.cronet
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.client.CHROME_ACCEPT
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
import com.hippo.ehviewer.client.CHROME_USER_AGENT
import com.hippo.ehviewer.client.EhCookieStore
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.utils.io.pool.DirectByteBufferPool
import kotlinx.coroutines.coroutineScope
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
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.RequestBody
import okio.Buffer
import okio.Path.Companion.toOkioPath
import org.chromium.net.CronetEngine
import org.chromium.net.CronetException
import org.chromium.net.UploadDataProvider
import org.chromium.net.UploadDataSink
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
import splitties.init.appCtx
private const val TAG = "CronetRequest"
val pool = DirectByteBufferPool(32)
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
enableBrotli(true)
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
setUserAgent(Settings.userAgent)
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
@@ -41,6 +49,7 @@ class CronetRequest {
lateinit var readerCont: Continuation<Unit>
val callback = object : UrlRequest.Callback() {
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
logcat(tag = TAG) { "Redirected to $url" }
req.followRedirect()
}
@@ -49,6 +58,7 @@ class CronetRequest {
}
override fun onReadCompleted(req: UrlRequest, info: UrlResponseInfo, data: ByteBuffer) {
data.flip()
consumer(data)
}
@@ -62,16 +72,39 @@ class CronetRequest {
}
}
inline fun cronetRequest(url: String, referer: String? = null, origin: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply {
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) }
origin?.let { addHeader("Origin", it) }
}.apply(conf).build()
}
suspend inline fun CronetRequest.awaitBodyFully(crossinline callback: (ByteBuffer) -> Unit) {
val buffer = pool.borrow()
return try {
suspendCancellableCoroutine { cont ->
consumer = {
callback(it)
buffer.clear()
request.read(buffer)
}
onError = { readerCont.resumeWithException(it) }
readerCont = cont
request.read(buffer)
}
} finally {
pool.recycle(buffer)
}
}
suspend inline fun CronetRequest.copyToChannel(chan: FileChannel, crossinline listener: ((Int) -> Unit) = {}) = awaitBodyFully {
val bytes = chan.write(it)
listener(bytes)
}
suspend inline fun <R> CronetRequest.execute(crossinline callback: suspend CronetRequest.(UrlResponseInfo) -> R): R {
contract {
callsInPlace(callback, InvocationKind.EXACTLY_ONCE)
@@ -85,22 +118,3 @@ suspend inline fun <R> CronetRequest.execute(crossinline callback: suspend Crone
}
}
}
fun UrlRequest.Builder.withRequestBody(body: RequestBody) {
addHeader("Content-Type", body.contentType().toString())
val buffer = Buffer().apply { body.writeTo(this) }
val provider = object : UploadDataProvider() {
override fun getLength() = body.contentLength()
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
buffer.read(byteBuffer)
uploadDataSink.onReadSucceeded(false)
}
override fun rewind(uploadDataSink: UploadDataSink) {
error("OneShot!")
}
}
setUploadDataProvider(provider, cronetHttpClientExecutor)
}
fun UrlRequest.Builder.noCache(): UrlRequest.Builder = disableCache()
fun UrlResponseInfo.getHeadersMap(): Map<String, List<String>> = allHeaders
@@ -24,12 +24,11 @@ import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import eu.kanade.tachiyomi.util.lang.withIOContext
import splitties.init.appCtx
import java.time.Duration
import java.time.Instant
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
import splitties.init.appCtx
private val signedIn
get() = EhCookieStore.hasSignedIn()
@@ -67,21 +66,21 @@ private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
.build()
return PeriodicWorkRequestBuilder<DailyCheckWork>(Duration.ofDays(1))
.setConstraints(constraints)
.setInitialDelay(initialDelay.seconds, TimeUnit.SECONDS)
.setInitialDelay(initialDelay)
.build()
}
private const val WORK_NAME = "DailyCheckWork"
private const val workName = "DailyCheckWork"
fun updateDailyCheckWork(context: Context) {
if (Settings.requestNews) {
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
WORK_NAME,
workName,
ExistingPeriodicWorkPolicy.UPDATE,
getDailyCheckWorkRequest(),
)
} else {
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
WorkManager.getInstance(context).cancelUniqueWork(workName)
}
}
Loaded 100 of 550 files, more files were not shown because too many files have changed in this diff. Show more