Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45eed26788 | ||
|
|
3482882648 | ||
|
|
2f92c9a2aa | ||
|
|
01b9c1d0f2 | ||
|
|
022cf9bdcf | ||
|
|
0326f52077 | ||
|
|
0bb18c703b | ||
|
|
5784dc9a9d | ||
|
|
cbaa826fbd |
No files matched your search
@@ -1,7 +0,0 @@
|
|||||||
[*.{kt,kts}]
|
|
||||||
ij_kotlin_imports_layout = *
|
|
||||||
ij_kotlin_line_break_after_multiline_when_entry = false
|
|
||||||
ktlint_code_style = intellij_idea
|
|
||||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
|
||||||
ktlint_standard_class-signature = disabled
|
|
||||||
ktlint_standard_mixed-condition-operators = disabled
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
name: Baseline profile generation
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '30 0 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '**'
|
|
||||||
paths:
|
|
||||||
- 'benchmark/**'
|
|
||||||
- '.github/workflows/baseline-profile.yml'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
baseline-profile:
|
|
||||||
runs-on: nekomoee
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: temurin
|
|
||||||
java-version-file: .java-version
|
|
||||||
|
|
||||||
- name: Setup Android SDK
|
|
||||||
uses: https://github.com/android-actions/setup-android@v4
|
|
||||||
|
|
||||||
- name: Setup Rust
|
|
||||||
uses: https://github.com/dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: nightly-2026-10-07
|
|
||||||
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: https://github.com/Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: "./app/src/main/rust"
|
|
||||||
|
|
||||||
- name: CMake Cache
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
|
||||||
path: app/.cxx
|
|
||||||
key: cmake-
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v6
|
|
||||||
with:
|
|
||||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
|
||||||
cache-read-only: true
|
|
||||||
|
|
||||||
- name: Generate Baseline Profile
|
|
||||||
run: ./gradlew generateBaselineProfile
|
|
||||||
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
|
||||||
|
|
||||||
- name: Upload reports
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: reports
|
|
||||||
path: benchmark/build/reports
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '*'
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: nekomoee
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: temurin
|
|
||||||
java-version-file: .java-version
|
|
||||||
|
|
||||||
- name: Setup Android SDK
|
|
||||||
uses: https://github.com/android-actions/setup-android@v4
|
|
||||||
|
|
||||||
- name: Setup Rust
|
|
||||||
uses: https://github.com/dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: nightly-2026-10-07
|
|
||||||
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: https://github.com/Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: "./app/src/main/rust"
|
|
||||||
shared-key: ${{ runner.os }}
|
|
||||||
|
|
||||||
- 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 --target aarch64-linux-android --all-features -- -D warnings
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v6
|
|
||||||
with:
|
|
||||||
cache-read-only: true
|
|
||||||
|
|
||||||
- name: Check build-logic
|
|
||||||
run: ./gradlew :build-logic:convention:check
|
|
||||||
|
|
||||||
- name: Spotless Check
|
|
||||||
run: ./gradlew spotlessCheck
|
|
||||||
|
|
||||||
default:
|
|
||||||
runs-on: nekomoee
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: temurin
|
|
||||||
java-version-file: .java-version
|
|
||||||
|
|
||||||
- name: Setup Android SDK
|
|
||||||
uses: https://github.com/android-actions/setup-android@v4
|
|
||||||
|
|
||||||
- name: Setup Rust
|
|
||||||
uses: https://github.com/dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: nightly-2026-10-07
|
|
||||||
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: https://github.com/Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: "./app/src/main/rust"
|
|
||||||
shared-key: ${{ runner.os }}
|
|
||||||
save-if: "false"
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v6
|
|
||||||
with:
|
|
||||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
|
||||||
|
|
||||||
- name: CMake Cache
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: app/.cxx
|
|
||||||
key: cmake-default-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
|
|
||||||
restore-keys: cmake-default-
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: ./gradlew assembleDefaultRelease
|
|
||||||
|
|
||||||
- name: Upload ARM64
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: default-arm64-v8a-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
|
|
||||||
|
|
||||||
- name: Upload x86_64
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: default-x86_64-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
|
|
||||||
|
|
||||||
- name: Upload ARM32
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: default-armeabi-v7a-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
|
|
||||||
|
|
||||||
- name: Upload mapping
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: default-mapping-${{ github.sha }}
|
|
||||||
path: app/build/outputs/mapping/defaultRelease/*.txt
|
|
||||||
|
|
||||||
- name: Upload native debug symbols
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: default-native-debug-symbols-${{ github.sha }}
|
|
||||||
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
name: Releases
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: nekomoee
|
|
||||||
steps:
|
|
||||||
- name: Free disk space
|
|
||||||
run: rm -rf $AGENT_TOOLSDIRECTORY
|
|
||||||
|
|
||||||
- name: Enable KVM group perms
|
|
||||||
run: |
|
|
||||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
|
||||||
sudo udevadm control --reload-rules
|
|
||||||
sudo udevadm trigger --name-match=kvm
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: temurin
|
|
||||||
java-version-file: .java-version
|
|
||||||
|
|
||||||
- name: Setup Rust
|
|
||||||
uses: https://github.com/dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
toolchain: nightly-2026-10-07
|
|
||||||
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: https://github.com/Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: "./app/src/main/rust"
|
|
||||||
|
|
||||||
- name: CMake Cache
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
|
||||||
path: app/.cxx
|
|
||||||
key: cmake-
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v6
|
|
||||||
with:
|
|
||||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
|
||||||
cache-read-only: true
|
|
||||||
|
|
||||||
- name: Generate Baseline Profile
|
|
||||||
run: ./gradlew generateBaselineProfile
|
|
||||||
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
|
||||||
-Prelease
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: ./gradlew assembleRelease
|
|
||||||
-Prelease
|
|
||||||
|
|
||||||
- name: Rename Apks
|
|
||||||
run: |
|
|
||||||
mkdir release
|
|
||||||
mv app/build/outputs/apk/default/release/app-default-universal-release.apk release/EhViewer-${{ github.ref_name }}-default-universal.apk
|
|
||||||
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk release/EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
|
||||||
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk release/EhViewer-${{ github.ref_name }}-default-x86_64.apk
|
|
||||||
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk release/EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
|
|
||||||
mv app/build/outputs/mapping/defaultRelease/mapping.txt release/EhViewer-${{ github.ref_name }}-default-mapping.txt
|
|
||||||
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip release/EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
|
||||||
|
|
||||||
- name: Releases
|
|
||||||
uses: actions/forgejo-release@v2
|
|
||||||
with:
|
|
||||||
direction: upload
|
|
||||||
url: https://codeberg.org
|
|
||||||
repo: EhViewer/EhViewer
|
|
||||||
release-dir: release
|
|
||||||
body: Bump Version
|
|
||||||
@@ -6,10 +6,10 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
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
|
Please finish verify steps which list in the end first before create bug report
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -18,9 +18,7 @@ body:
|
|||||||
label: 复现步骤 / Step to reproduce
|
label: 复现步骤 / Step to reproduce
|
||||||
description: |
|
description: |
|
||||||
请从全新安装应用开始,在此处写下复现的方式。请尽量附带录屏
|
请从全新安装应用开始,在此处写下复现的方式。请尽量附带录屏
|
||||||
如果不能稳定复现,请在下方上传应用日志
|
|
||||||
Describe how to reproduce here with fresh installation, with screen recording if possible.
|
Describe how to reproduce here with fresh installation, with screen recording if possible.
|
||||||
If you can't reproduce it consistently, please upload app logs below
|
|
||||||
placeholder: |
|
placeholder: |
|
||||||
0. 重新安装应用 / Fresh install
|
0. 重新安装应用 / Fresh install
|
||||||
1.
|
1.
|
||||||
@@ -57,15 +55,6 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: log
|
|
||||||
attributes:
|
|
||||||
label: 应用日志 / App logs
|
|
||||||
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
|
|
||||||
placeholder: 06-15 17:44:53.704 23382 23382 E ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: more
|
id: more
|
||||||
attributes:
|
attributes:
|
||||||
@@ -79,15 +68,6 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: site
|
|
||||||
attributes:
|
|
||||||
label: 浏览站点 / Browsing site
|
|
||||||
description: E-Hentai / ExHentai
|
|
||||||
placeholder: E-Hentai
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
@@ -101,8 +81,8 @@ body:
|
|||||||
id: ci
|
id: ci
|
||||||
attributes:
|
attributes:
|
||||||
label: EhViewer CI 版本 / EhViewer CI Version
|
label: EhViewer CI 版本 / EhViewer CI Version
|
||||||
description: 位于设置 - 关于中的版本号后 / Next to the version code in Settings - About
|
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
|
||||||
placeholder: 26f05a0
|
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -141,11 +121,17 @@ body:
|
|||||||
请确认您已经遵守所有必选项
|
请确认您已经遵守所有必选项
|
||||||
Please ensure you have obtained all needed options
|
Please ensure you have obtained all needed options
|
||||||
options:
|
options:
|
||||||
- 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: 如果您有足够的时间和能力,并愿意为此提交 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
|
required: true
|
||||||
|
|
||||||
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
|
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
|
||||||
required: true
|
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
|
required: true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
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: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
|
||||||
|
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
|
||||||
|
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
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
tags-ignore:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linux:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- name: Setup Java 19
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: 19
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: |
|
||||||
|
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||||
|
|
||||||
|
- name: Gradle Build
|
||||||
|
uses: gradle/gradle-build-action@v2
|
||||||
|
with:
|
||||||
|
gradle-home-cache-cleanup: true
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Update mapping
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: mapping-${{ github.sha }}
|
||||||
|
path: app/build/outputs/mapping/release/mapping.txt
|
||||||
|
retention-days: 5
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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 19
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: 19
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: |
|
||||||
|
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Releases
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linux:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- name: Setup Java 19
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: 19
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: |
|
||||||
|
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||||
|
|
||||||
|
- name: Gradle Build
|
||||||
|
uses: gradle/gradle-build-action@v2
|
||||||
|
with:
|
||||||
|
arguments: assembleRelease
|
||||||
|
|
||||||
|
- name: Rename Apks
|
||||||
|
run: |
|
||||||
|
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
|
||||||
|
discussion_category_name: Announcements
|
||||||
|
files: |
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Collect Apks
|
||||||
|
run: |
|
||||||
|
mkdir app/build/outputs/apk/release/fdroid
|
||||||
|
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME.apk
|
||||||
|
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
|
||||||
|
|
||||||
|
- name: Push Apks
|
||||||
|
uses: Ehviewer-Overhauled/github-action-push-to-another-repository@main
|
||||||
|
env:
|
||||||
|
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||||
|
with:
|
||||||
|
source-directory: app/build/outputs/apk/release/fdroid
|
||||||
|
destination-github-username: Ehviewer-Overhauled
|
||||||
|
destination-repository-name: fdroid-repo
|
||||||
|
user-name: 'github-actions[bot]'
|
||||||
|
user-email: 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
target-branch: deploy
|
||||||
|
create-target-branch: true
|
||||||
|
commit-message: Add apks from $GITHUB_REF
|
||||||
|
target-directory: fdroid/tmp-apks
|
||||||
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
.DS_Store
|
.DS_Store
|
||||||
build
|
/build
|
||||||
.cxx
|
/src
|
||||||
.kotlin
|
|
||||||
*.iml
|
*.iml
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
|
||||||
/captures
|
/captures
|
||||||
|
release
|
||||||
|
google-services.json
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "app/src/main/cpp/nettle/nettle"]
|
||||||
|
path = app/src/main/cpp/nettle/nettle
|
||||||
|
url = https://github.com/Ehviewer-Overhauled/nettle
|
||||||
@@ -1 +0,0 @@
|
|||||||
25
|
|
||||||
@@ -1,588 +0,0 @@
|
|||||||
# 变更日志
|
|
||||||
|
|
||||||
## [1.14.6] - 2025-12-17
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* 支持显示画廊阅读进度(于 EH 设置中开启) [#2277](https://github.com/FooIbar/EhViewer/issues/2277)
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 画廊列表变化时重新显示悬浮操作按钮 [#2681](https://github.com/FooIbar/EhViewer/issues/2681)
|
|
||||||
* 移除文件名中的尾随空格和点以兼容 Windows [#2727](https://github.com/FooIbar/EhViewer/issues/2727)
|
|
||||||
* 长按画廊详情中的标签时显示标签名称 [#2769](https://github.com/FooIbar/EhViewer/issues/2769)
|
|
||||||
* 杂项优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 对话框中的下拉菜单显示错位
|
|
||||||
* HTTP 状态错误显示为“什么都没有找到”
|
|
||||||
* 某些老设备无法通过 Cloudflare 验证 [#2794](https://github.com/FooIbar/EhViewer/issues/2794)
|
|
||||||
* 杂项修复
|
|
||||||
|
|
||||||
|
|
||||||
## [1.14.5] - 2025-10-19
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* [阅读器] 显示画廊标题
|
|
||||||
* [阅读器] 显示菜单时改变标题栏颜色
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 横屏模式下打开阅读器设置会闪退 [#2673](https://github.com/FooIbar/EhViewer/issues/2673)
|
|
||||||
* 强制横屏时竖直拿取设备加载图片会横竖屏乱跳 [#2674](https://github.com/FooIbar/EhViewer/issues/2674)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.14.4] - 2025-10-12
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* [阅读器] 支持禁用使用音量键翻页 [#2593](https://github.com/FooIbar/EhViewer/issues/2593)
|
|
||||||
* 视搜索警告为错误 [#2604](https://github.com/FooIbar/EhViewer/issues/2604)
|
|
||||||
* 余额不足时停止下载并显示通知 [#2479](https://github.com/FooIbar/EhViewer/issues/2479)
|
|
||||||
* [阅读器] 使用默认阅读模式时对带有 webtoon 标签的画廊自动使用条漫模式 [#2632](https://github.com/FooIbar/EhViewer/issues/2632)
|
|
||||||
* 优化按页数排序下载项目的速度
|
|
||||||
* 开启显示标签投票状态时为 power >= 100 的标签添加下划线
|
|
||||||
* 杂项优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* Android 16 QPR1 上画廊详情中的标签文本对比度不足
|
|
||||||
* 初始化 DownloadManager 用时过长导致 ANR [#2588](https://github.com/FooIbar/EhViewer/issues/2588)
|
|
||||||
* 某些设备上无法下载名称较长的画廊 [#2607](https://github.com/FooIbar/EhViewer/issues/2607)
|
|
||||||
* 评论区链接实际可点击区域比画面显示上的略长 [#2615](https://github.com/FooIbar/EhViewer/issues/2615)
|
|
||||||
* 主页快速搜索再次打开出现以前的画廊重复出现在最上层 [#2628](https://github.com/FooIbar/EhViewer/issues/2628)
|
|
||||||
* 非条漫模式下原始大小较小的动图不播放 [#2652](https://github.com/FooIbar/EhViewer/issues/2652)
|
|
||||||
* Android 9 以下打开评论区切屏后回来会闪退 [#2655](https://github.com/FooIbar/EhViewer/issues/2655)
|
|
||||||
* 杂项修复
|
|
||||||
|
|
||||||
|
|
||||||
## [1.14.3] - 2025-09-01
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* 支持以 label: 语法搜索下载标签
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* [阅读器] 关闭滤镜设置时崩溃 [#2573](https://github.com/FooIbar/EhViewer/issues/2573)
|
|
||||||
* 编辑评论时上下文菜单缺少格式化选项
|
|
||||||
* 某些情况下搜索相似画廊时出错
|
|
||||||
|
|
||||||
|
|
||||||
## [1.14.2] - 2025-08-16
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* [下载] 支持将最低下载速度设为 0 [#2545](https://github.com/FooIbar/EhViewer/issues/2545)
|
|
||||||
* [下载] 移除令人困惑的连接超时设置 [#2556](https://github.com/FooIbar/EhViewer/issues/2556)
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 下载速度计算不准确导致下载失败误报 [#2545](https://github.com/FooIbar/EhViewer/issues/2545)
|
|
||||||
* 无法解析带有 location: 标签的画廊
|
|
||||||
|
|
||||||
|
|
||||||
## [1.14.1] - 2025-07-31
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 更新 GitHub 访问令牌
|
|
||||||
|
|
||||||
|
|
||||||
## [1.14.0] - 2025-07-23
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* 默认隐藏分数为 -100 的评论
|
|
||||||
* 迁移部分组件到 Material 3 Expressive
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* [EH] 支持显示标签投票状态
|
|
||||||
* 支持添加画廊标签时自动补全
|
|
||||||
* [下载] 支持指定连接超时和最低响应速度 [#1915](https://github.com/FooIbar/EhViewer/issues/1915)
|
|
||||||
* [阅读器] 支持反转物理按键控制 [#1962](https://github.com/FooIbar/EhViewer/issues/1962)
|
|
||||||
* [高级] 支持禁用 QUIC 支持
|
|
||||||
* [高级] 重新添加桌面版网站选项以绕过 Cloudflare [#2230](https://github.com/FooIbar/EhViewer/issues/2230)
|
|
||||||
* 支持 Android 6.0 上的动画 WebP
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 优化压缩包加载性能
|
|
||||||
* 优化预加载策略
|
|
||||||
* 更新日本语翻译
|
|
||||||
* 为已选中的项目使用不同的颜色 [#2121](https://github.com/FooIbar/EhViewer/issues/2121)
|
|
||||||
* 缩略图模式下显示已收藏图标 [#2128](https://github.com/FooIbar/EhViewer/issues/2128)
|
|
||||||
* 迁移大部分解析器到 Rust 实现
|
|
||||||
* 优化动画 WebP 解码性能 [#2415](https://github.com/FooIbar/EhViewer/issues/2415)
|
|
||||||
* 优化标签自动补全命中率 [#2473](https://github.com/FooIbar/EhViewer/issues/2473)
|
|
||||||
* 在状态栏后绘制半透明背景以提高对比度
|
|
||||||
* 杂项优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 退出阅读器时崩溃
|
|
||||||
* Android 8.0 以下的设备无法读取某些压缩包
|
|
||||||
* 列表模式下点击画廊封面无法进入详情页
|
|
||||||
* 点击某些评论时崩溃
|
|
||||||
* 开启裁剪边缘时某些图片被不正确裁剪
|
|
||||||
* 下载数较多时无网络时启动 app 时崩溃 [#2041](https://github.com/FooIbar/EhViewer/issues/2041)
|
|
||||||
* 标签翻译数据无法更新 [#2075](https://github.com/FooIbar/EhViewer/issues/2075)
|
|
||||||
* 画廊详情页面显示的收藏夹名称错误 [#2081](https://github.com/FooIbar/EhViewer/issues/2081)
|
|
||||||
* 不使用 CI 频道时无法更新
|
|
||||||
* 添加后移动快捷搜索闪退 [#2124](https://github.com/FooIbar/EhViewer/issues/2124)
|
|
||||||
* 某些数据无法导入
|
|
||||||
* App 位于后台时清除下载通知时崩溃
|
|
||||||
* 杂项修复
|
|
||||||
|
|
||||||
|
|
||||||
## [1.13.1] - 2024-11-11
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* 由于网站限制,移除搜索封面功能
|
|
||||||
* 移除桌面版网站选项,默认使用 1280x 分辨率
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 更新日本语翻译
|
|
||||||
* 优化压缩包读取
|
|
||||||
* 缓解某些小米系统上应用闪退 [#1826](https://github.com/FooIbar/EhViewer/issues/1826)
|
|
||||||
* 杂项优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* Android 9 上的下载失败误报 [#1914](https://github.com/FooIbar/EhViewer/issues/1914)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.13.0] - 2024-11-01
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* 不再捆绑 Cronet 库,在不支持 HttpEngine 的设备上回退到 OkHttp
|
|
||||||
* 适配 E 站缩略图和 WebP 等相关变更
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 使用磁力链接代替种子下载
|
|
||||||
* 在搜索栏右侧显示论坛头像
|
|
||||||
* 支持重置下载路径
|
|
||||||
* 为 Android 10 以下且没有 DocumentsUI 的设备创建默认下载目录 [#1735](https://github.com/FooIbar/EhViewer/issues/1735)
|
|
||||||
* 重新添加了强制使用 e-hentai 缩略图服务器的选项
|
|
||||||
* 阅读器支持 PageUp/PageDown 和方向键上/下翻页 [#1801](https://github.com/FooIbar/EhViewer/issues/1801)
|
|
||||||
* 未找到搜索结果时显示可能存在的警告 [#1787](https://github.com/FooIbar/EhViewer/issues/1787)
|
|
||||||
* 更新了日本语翻译
|
|
||||||
* 杂项优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 阅读器屏幕方向与手机屏幕方向不同时闪退 [#1779](https://github.com/FooIbar/EhViewer/issues/1779)
|
|
||||||
* 下载文件哈希不匹配导致下载失败 [#1811](https://github.com/FooIbar/EhViewer/issues/1811)
|
|
||||||
* 无法同时下载和查看 [#1822](https://github.com/FooIbar/EhViewer/issues/1822)
|
|
||||||
* 杂项修复
|
|
||||||
|
|
||||||
|
|
||||||
## [1.12.1] - 2024-09-28
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* 屏蔽含有二维码的图片(位于高级设置)
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 移除高级设置中的自定义 User Agent,改为桌面版网站选项
|
|
||||||
* 画廊预览与画廊详情页面合并
|
|
||||||
* 为快速搜索和下载标签列表添加了滚动条
|
|
||||||
* 无网络条件下启动时自动转到下载页面
|
|
||||||
* 杂项优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 从左到右或右到左模式观看使用鼠标滚轮无法翻页 [#1541](https://github.com/FooIbar/EhViewer/issues/1541)
|
|
||||||
* 状态栏颜色未跟随阅读器主题变化 [#1542](https://github.com/FooIbar/EhViewer/issues/1542)
|
|
||||||
* 打开 EH 设置时闪退 [#1563](https://github.com/FooIbar/EhViewer/issues/1563)
|
|
||||||
* 画廊种子过多时显示没有种子 [#1561](https://github.com/FooIbar/EhViewer/issues/1561)
|
|
||||||
* 加载大于 100 MB 的位图时崩溃
|
|
||||||
* 阅读器加载小圆圈在黑色背景下不自动变色 [#1579](https://github.com/FooIbar/EhViewer/issues/1579)
|
|
||||||
* 因闪退造成的无法挽回的下载失败 [#1602](https://github.com/FooIbar/EhViewer/issues/1602)
|
|
||||||
* 某些情况下闪退 [#1548](https://github.com/FooIbar/EhViewer/issues/1548) [#1555](https://github.com/FooIbar/EhViewer/issues/1555) [#1568](https://github.com/FooIbar/EhViewer/issues/1568)
|
|
||||||
* Android 7.0 及以下的设备无法加载图片 [#1633](https://github.com/FooIbar/EhViewer/issues/1633)
|
|
||||||
* 某些情况下导入数据失败 [#1634](https://github.com/FooIbar/EhViewer/issues/1634)
|
|
||||||
* 下载时创建重复文件夹 [#1619](https://github.com/FooIbar/EhViewer/issues/1619)
|
|
||||||
* 滚动方向改变后预载图片不生效 [#1560](https://github.com/FooIbar/EhViewer/issues/1560)
|
|
||||||
* 选择无效的下载位置后闪退
|
|
||||||
* 杂项修复
|
|
||||||
|
|
||||||
|
|
||||||
## [1.12.0] - 2024-08-19
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* Compose 阅读器已稳定,移除了旧阅读器实现 [#847](https://github.com/FooIbar/EhViewer/issues/847)
|
|
||||||
* 由于 E 站缩略图服务器变更,移除了强制使用 e-hentai 缩略图服务器的选项
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 更新高级搜索选项中页数过滤器的范围限制以遵循 E 站变更
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 评论页面刷新指示器边距不正确 [#1428](https://github.com/FooIbar/EhViewer/issues/1428)
|
|
||||||
* 关闭打开失败的压缩包时崩溃
|
|
||||||
* 无法打开带密码的压缩包 [#1454](https://github.com/FooIbar/EhViewer/issues/1454)
|
|
||||||
* 使用音量键翻页时无法关闭过渡动画 [#1467](https://github.com/FooIbar/EhViewer/issues/1467)
|
|
||||||
* 带有图片的评论显示错位 [#1468](https://github.com/FooIbar/EhViewer/issues/1468)
|
|
||||||
* 在包含相同项目的页面间切换时崩溃 [#1490](https://github.com/FooIbar/EhViewer/issues/1490)
|
|
||||||
* 无法检查更新 [#1512](https://github.com/FooIbar/EhViewer/issues/1512)
|
|
||||||
* 画廊详情未加载完成时退出阅读器导致崩溃 [#1507](https://github.com/FooIbar/EhViewer/issues/1507)
|
|
||||||
* 无法为带有临时标签的画廊生成 ComicInfo.xml [#1509](https://github.com/FooIbar/EhViewer/issues/1509)
|
|
||||||
* 已下载的画廊翻页时出现加载动画 [#1510](https://github.com/FooIbar/EhViewer/issues/1510)
|
|
||||||
* 画廊详情页面的缩略图无法加载 [#1529](https://github.com/FooIbar/EhViewer/issues/1529)
|
|
||||||
* 图片未加载完成时无法通过点击翻页
|
|
||||||
* 搜索结果为空时解析失败 [#1418](https://github.com/FooIbar/EhViewer/issues/1418)
|
|
||||||
* 缩略图分辨率设置不起作用
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.7] - 2024-07-10
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 无法下载标题包含 # 或 % 的画廊
|
|
||||||
* 下载页数较多的画廊时应用无响应
|
|
||||||
* 退出搜索栏时文本闪烁
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.6] - 2024-07-09 [YANKED]
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 某些设备上无法加载长图时可尝试减小 设置-高级 中的硬件位图阈值 [#1321](https://github.com/FooIbar/EhViewer/issues/1321)
|
|
||||||
* 清理下载冗余时显示确认对话框 [#1369](https://github.com/FooIbar/EhViewer/issues/1369)
|
|
||||||
* 修改收藏时显示收藏备注 [#1392](https://github.com/FooIbar/EhViewer/issues/1392)
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 图片完整性校验误报 [#1286](https://github.com/FooIbar/EhViewer/issues/1286)
|
|
||||||
* 某些情况下应用崩溃 [#1290](https://github.com/FooIbar/EhViewer/issues/1290) [#1302](https://github.com/FooIbar/EhViewer/issues/1302)
|
|
||||||
* 某些情况下渲染问题 [#1284](https://github.com/FooIbar/EhViewer/issues/1284)
|
|
||||||
* 无法下载标题过长的画廊 [#1340](https://github.com/FooIbar/EhViewer/issues/1340)
|
|
||||||
* 某些界面拖动滚动条时黑屏 [#1329](https://github.com/FooIbar/EhViewer/issues/1329)
|
|
||||||
* 搜索栏多行文本遮挡其他内容 [#1365](https://github.com/FooIbar/EhViewer/issues/1365)
|
|
||||||
* CIFS Documents Provider 兼容问题 [#1356](https://github.com/FooIbar/EhViewer/issues/1356)
|
|
||||||
* 清理下载冗余未按预期运行 [#1369](https://github.com/FooIbar/EhViewer/issues/1369)
|
|
||||||
* 读取压缩包失败时崩溃
|
|
||||||
* 某些设备上崩溃 [#1276](https://github.com/FooIbar/EhViewer/issues/1276)
|
|
||||||
* 检查更新失效
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.6-RC2] - 2024-06-06
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 下载图片时执行完整性校验以防止文件损坏/被篡改
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.6-RC1] - 2024-06-02
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 下载列表默认显示全部下载项目
|
|
||||||
* 替换搜索文本中的换行符为空格 [#1258](https://github.com/FooIbar/EhViewer/issues/1258)
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 评价画廊时未使用输入的评分
|
|
||||||
* 搜索时意外选择 Non-H 类别 [#1246](https://github.com/FooIbar/EhViewer/issues/1246)
|
|
||||||
* 侧边栏与导航抽屉重叠 [#1262](https://github.com/FooIbar/EhViewer/issues/1262)
|
|
||||||
* 快速搜索/下载标签排序失效 [#1267](https://github.com/FooIbar/EhViewer/issues/1267)
|
|
||||||
* 加载压缩包/种子列表出错时 UI 冻结
|
|
||||||
* 低版本 Android 上某些 UI 被遮挡
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.5] - 2024-05-19
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* 不再支持 32 位 x86
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* 支持按作者对下载分组
|
|
||||||
* 由于 E 站不再强制要求人机验证,重新支持账号密码登录
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 在登录时加载收藏名称 [#1077](https://github.com/FooIbar/EhViewer/issues/1077)
|
|
||||||
* 优化归档和种子界面 UI
|
|
||||||
* 禁止在 cookie 界面截图以防止泄露
|
|
||||||
* 在缩略图模式下显示画廊页数
|
|
||||||
* 记住最低评分和页数搜索参数
|
|
||||||
* 更新 User-Agent 以缓解 IP 封禁 [#1182](https://github.com/FooIbar/EhViewer/issues/1182)
|
|
||||||
* 账号密码登录支持自动填充
|
|
||||||
* 更新中文(台灣)翻译
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 非触摸模式下无法退出搜索界面 [#1060](https://github.com/FooIbar/EhViewer/issues/1060)
|
|
||||||
* 某些设备上无法安装更新 [#1067](https://github.com/FooIbar/EhViewer/issues/1067)
|
|
||||||
* 某些设备上崩溃/卡顿 [#996](https://github.com/FooIbar/EhViewer/issues/996) [#1023](https://github.com/FooIbar/EhViewer/issues/1023)
|
|
||||||
* 搜索记录中存在重复条目时崩溃 [#1130](https://github.com/FooIbar/EhViewer/issues/1130)
|
|
||||||
* 条漫模式下缩小后点按区域偏移 [#127](https://github.com/FooIbar/EhViewer/issues/127)
|
|
||||||
* 多窗口模式下点按区域偏移
|
|
||||||
* 点击下载通知时未清除状态
|
|
||||||
* 保存未完成加载的图片时崩溃 [#1154](https://github.com/FooIbar/EhViewer/issues/1154)
|
|
||||||
* 历史记录界面无法在项目上滑动打开抽屉 [#464](https://github.com/FooIbar/EhViewer/issues/464)
|
|
||||||
* 导入数据失败时显示导入成功 [#1174](https://github.com/FooIbar/EhViewer/issues/1174)
|
|
||||||
* 某些条件下收藏界面崩溃 [#1190](https://github.com/FooIbar/EhViewer/issues/1190)
|
|
||||||
* 反转点按区域功能失效 [#1217](https://github.com/FooIbar/EhViewer/issues/1217)
|
|
||||||
|
|
||||||
### 已知问题
|
|
||||||
|
|
||||||
* 某些设备上从后台返回时某些 UI 消失,可通过关闭列表项目动画部分缓解 [#1184](https://github.com/FooIbar/EhViewer/issues/1184)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.4] - 2024-04-21
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* 更新前自动备份数据
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 在 设置-高级 中加入了禁用列表项目动画的选项,该功能在部分设备上导致崩溃/卡顿 [#996](https://github.com/FooIbar/EhViewer/issues/996) [#1023](https://github.com/FooIbar/EhViewer/issues/1023)
|
|
||||||
* 下载列表多选模式下避免误触 [#1016](https://github.com/FooIbar/EhViewer/issues/1016)
|
|
||||||
* 杂项 UI 优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 重命名下载标签后删除该标签时崩溃 [#1008](https://github.com/FooIbar/EhViewer/issues/1008)
|
|
||||||
* 搜索栏中某些输入法长按退格无法连续删除 [#606](https://github.com/FooIbar/EhViewer/issues/606)
|
|
||||||
* 评论/屏蔽/搜索建议/下载标签/快速搜索列表卡顿 [#1041](https://github.com/FooIbar/EhViewer/issues/1041)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.3] - 2024-04-06
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 优化预加载逻辑和图片加载顺序
|
|
||||||
* 优化画廊详情和评论页面 UI
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 某些模式下搜索结果不显示画廊语言 [#923](https://github.com/FooIbar/EhViewer/issues/923)
|
|
||||||
* 某些情况下在搜索结果中直接阅读画廊后崩溃 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
|
||||||
* 更新至最新版本后仍提示有新版本 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
|
||||||
* 从旧版本升级时包含不兼容数据导致崩溃/无法导出数据 [#940](https://github.com/FooIbar/EhViewer/issues/940) [#974](https://github.com/FooIbar/EhViewer/issues/974)
|
|
||||||
* Android 10 以下的设备打开 设置-隐私 时崩溃 [#953](https://github.com/FooIbar/EhViewer/issues/953)
|
|
||||||
* 下载标签无法拖动
|
|
||||||
* 刷新 和 刷新(原图) 无法在已下载的图片上使用
|
|
||||||
* 临时 IP 封禁错误显示为解析失败
|
|
||||||
* 某些情况下下载时闪退 [#980](https://github.com/FooIbar/EhViewer/issues/980)
|
|
||||||
* 某些设备上下载列表中的缩略图无法显示 [#977](https://github.com/FooIbar/EhViewer/issues/977)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.2] - 2024-03-17
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* Default 变种最低支持版本更改为 Android 8.0,重新添加了最低支持 Android 6.0 的 Marshmallow 变种
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 重新添加了删除快速搜索/下载标签时的确认对话框
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 收藏页面 搜索栏中文标签转英文异常 [#902](https://github.com/FooIbar/EhViewer/issues/902)
|
|
||||||
* 在收藏或者取消收藏之后,收藏按钮会无反应 [#908](https://github.com/FooIbar/EhViewer/issues/908)
|
|
||||||
* 横屏页面下阅读设置UI重合 [#909](https://github.com/FooIbar/EhViewer/issues/909)
|
|
||||||
* Search bar does not work properly in Japanese [#906](https://github.com/FooIbar/EhViewer/issues/906)
|
|
||||||
* 搜索结果中的 Disowned 画廊不显示上传者
|
|
||||||
* 使用 CI 频道更新时,有时会获取到错误的更新链接
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.1] - 2024-03-12
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* 由于 E 站强制要求人机验证,仅支持通过网页登录
|
|
||||||
* 默认支持 Android 6,不再提供 Marshmallow 变种
|
|
||||||
* 集成 Cronet 库,不再提供 GMS 变种
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 支持在排序下载列表前按下载标签分组
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 下载画廊时崩溃 [#893](https://github.com/FooIbar/EhViewer/issues/893)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.11.0] - 2024-03-09
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* 由于 E 站 Cookie 策略变更,不再支持 Cookie 登录
|
|
||||||
* 登录时自动选择 exhentai (如果可用)
|
|
||||||
* 不再提供默认下载路径
|
|
||||||
* 手动排序更改为以添加时间/上传时间/标题/下载标签/页数对下载列表排序
|
|
||||||
* 由于数据库结构变更,此版本导出的数据将无法被旧版本导入
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* 下载画廊时生成 ComicInfo.xml 格式元数据
|
|
||||||
* 归档下载时生成 ComicInfo.xml 格式元数据 [#711](https://github.com/FooIbar/EhViewer/issues/711)
|
|
||||||
* 归档下载支持第三方下载管理器 [#79](https://github.com/FooIbar/EhViewer/issues/79)
|
|
||||||
* 保存下载结果为 CBZ 压缩包 [#660](https://github.com/FooIbar/EhViewer/issues/660)
|
|
||||||
* 清除 `igneous` Cookie (将于下次搜索时重新获取)
|
|
||||||
* 随机打开本地收藏/下载中的画廊 [#695](https://github.com/FooIbar/EhViewer/issues/695)
|
|
||||||
* 随机在搜索结果中跳转
|
|
||||||
* 以网格模式查看下载列表
|
|
||||||
* 本地收藏/历史/下载中的标签搜索 [#860](https://github.com/FooIbar/EhViewer/issues/860)
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 搜索时优先显示已选择的类别卡片
|
|
||||||
* 多处性能优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 显示模式为 Thumbnail 时无法点击上传者卡片 [#653](https://github.com/FooIbar/EhViewer/issues/653)
|
|
||||||
* [Marshmallow] GIF 只显示第一帧
|
|
||||||
* 无法在 `定时请求新闻页面` 关闭时开启 `隐藏 HV 事件通知`
|
|
||||||
* 响应正文含有无效 UTF-8 字符导致解码失败 [#643](https://github.com/FooIbar/EhViewer/issues/643)
|
|
||||||
* 某些情况下应用崩溃 [#710](https://github.com/FooIbar/EhViewer/issues/710)
|
|
||||||
* 画廊名称包含非法路径字符时无法保存图片 [#815](https://github.com/FooIbar/EhViewer/issues/815)
|
|
||||||
* 无法解码超长图片 [#732](https://github.com/FooIbar/EhViewer/issues/732)
|
|
||||||
* E 站变更导致种子解析失败 [#880](https://github.com/FooIbar/EhViewer/issues/880)
|
|
||||||
* E 站不再支持相似扫描导致图片搜索无法使用
|
|
||||||
* 杂项修复
|
|
||||||
|
|
||||||
### 已知问题
|
|
||||||
|
|
||||||
* Android 14 上使用手势导航返回时过渡动画过早结束 [#775](https://github.com/FooIbar/EhViewer/issues/775)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.10.3] - 2024-01-15
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* [Marshmallow] 支持裁剪边缘、打开压缩包和 GIF
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 由于部分设备上的非预期行为 [#578](https://github.com/FooIbar/EhViewer/issues/578) [#596](https://github.com/FooIbar/EhViewer/issues/596),默认禁用预见式返回导航动画,可于 设置-高级 中开启
|
|
||||||
* 杂项优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 刷新后页面无响应 [#579](https://github.com/FooIbar/EhViewer/issues/579)
|
|
||||||
* 浏览中保存图片后点击操作被阻挡 [#581](https://github.com/FooIbar/EhViewer/issues/581)
|
|
||||||
* 在画廊详情页面切换至后台后点击评论闪退 [#580](https://github.com/FooIbar/EhViewer/issues/580)
|
|
||||||
* 已下载的画廊中未阅读缓存过的图片解码失败 [#592](https://github.com/FooIbar/EhViewer/issues/592)
|
|
||||||
* 在收藏页多选并下载会使得下载状态出现问题 [#603](https://github.com/FooIbar/EhViewer/issues/603)
|
|
||||||
* 未能在不重启情况下切换 Http 引擎 [#610](https://github.com/FooIbar/EhViewer/issues/610)
|
|
||||||
* 在有进度的画廊阅读界面无法直接回到第一页 [#614](https://github.com/FooIbar/EhViewer/issues/614)
|
|
||||||
* 请求新闻界面失效 [#605](https://github.com/FooIbar/EhViewer/issues/605)
|
|
||||||
* 收藏夹为空解析失败 [#630](https://github.com/FooIbar/EhViewer/issues/630)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.10.2] - 2023-12-31
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* [阅读器] 支持裁剪图片黑色/白色边缘
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 切换 HTTP 引擎无需重启
|
|
||||||
* 优化过渡动画
|
|
||||||
* 杂项性能优化
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 修复搜索建议中出现关键词相同的条目时崩溃 [#533](https://github.com/FooIbar/EhViewer/issues/533) [#555](https://github.com/FooIbar/EhViewer/issues/555)
|
|
||||||
* 修复重命名下载标签时不显示原标签 [#534](https://github.com/FooIbar/EhViewer/issues/534)
|
|
||||||
* 修复调整下载项目顺序时卡顿 [#552](https://github.com/FooIbar/EhViewer/issues/552)
|
|
||||||
* 修复新增或修改评论后黑屏 [#554](https://github.com/FooIbar/EhViewer/issues/554)
|
|
||||||
* 修复剪贴板读取bug [#566](https://github.com/FooIbar/EhViewer/issues/566)
|
|
||||||
* 修复画廊信息对话框无法通过返回按钮/手势关闭 [#413](https://github.com/FooIbar/EhViewer/issues/413)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.10.1] - 2023-12-19
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* 由于 Apache HttpClient 5 在 Android 10 以下无法使用,后备 HTTP 引擎恢复为 OkHttp [#514](https://github.com/FooIbar/EhViewer/issues/514)
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 修复获取 pToken 失败导致无限加载 [#508](https://github.com/FooIbar/EhViewer/issues/508)
|
|
||||||
* 修复阅读页快速滑动滑块导致下载延时过多累计 [#512](https://github.com/FooIbar/EhViewer/issues/512)
|
|
||||||
* 修复 Android 9 以下的设备上无法退出搜索栏 [#513](https://github.com/FooIbar/EhViewer/issues/513)
|
|
||||||
|
|
||||||
|
|
||||||
## [1.10.0] - 2023-12-14 [YANKED]
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* 后备 HTTP 引擎变更为 Apache HttpClient 5
|
|
||||||
* 类别和高级搜索选项移至搜索栏下方
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
|
|
||||||
* 支持在搜索时指定画廊语言
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
|
|
||||||
* 在下载中阅读画廊时会更新历史记录
|
|
||||||
* 多处性能改进
|
|
||||||
|
|
||||||
### Bug 修复
|
|
||||||
|
|
||||||
* 修复无法由外部应用进入画廊页面 [#436](https://github.com/FooIbar/EhViewer/issues/436)
|
|
||||||
* 修复点击精确到页的画廊链接闪退 [#430](https://github.com/FooIbar/EhViewer/issues/430)
|
|
||||||
* 修复无法搜索本地收藏 [#491](https://github.com/FooIbar/EhViewer/issues/491)
|
|
||||||
* 修复 CI 版本不显示变更日志
|
|
||||||
|
|
||||||
|
|
||||||
## [1.9.0] - 2023-12-01
|
|
||||||
|
|
||||||
### 重大变更
|
|
||||||
|
|
||||||
* 除阅读器外全部迁移至 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)
|
|
||||||
@@ -1,32 +1,100 @@
|
|||||||
|
<p align="right">
|
||||||
|
<strong>English</strong>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
|
||||||
|
简体中文
|
||||||
|
</a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
|
||||||
|
正體中文
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img src="https://github.com/FooIbar/EhViewer-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>
|
<br>EhViewer<br>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Standalone fork of
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
|
||||||
<a href="https://codeberg.org/EhViewer/EhViewer">EhViewer (codeberg)</a>
|
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
|
||||||
maintained at <a href="https://gitea.yrkaierx.com/yenru0/EhViewer">gitea.yrkaierx.com/yenru0/EhViewer</a>
|
</a>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
|
||||||
|
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
|
||||||
|
<img src="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer/badge" alt="CodeFactor">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases">
|
||||||
|
<img src="https://img.shields.io/github/v/release/Ehviewer-Overhauled/ehviewer?include_prereleases" alt="Release">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/issues">
|
||||||
|
<img src="https://img.shields.io/github/issues/Ehviewer-Overhauled/ehviewer" alt="Issues">
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Changes over upstream
|
<div align="center">
|
||||||
|
<h3>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#description">
|
||||||
|
Description
|
||||||
|
</a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#download">
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#screenshot">
|
||||||
|
Screenshot
|
||||||
|
</a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#thanks">
|
||||||
|
Thanks
|
||||||
|
</a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#license">
|
||||||
|
License
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
- Built-in hosts, Cloudflare CDN fallback and domain fronting with SNI spoofing for censored networks (`EhHostsConfig.kt`, `EhDns.kt`, `EhSSLSocketFactory.kt`)
|
|
||||||
- Cronet engine disabled by default (bypasses the anti-DPI OkHttp stack; `enable_quic` re-enables it)
|
|
||||||
- Update check disabled by default (manual check still available)
|
|
||||||
- API request timeout raised to 30 s
|
|
||||||
- Own application id (`yenru0.yrkaierx.ehviewer`), installs side by side with upstream builds
|
|
||||||
|
|
||||||
# Upstream
|
# Description
|
||||||
|
|
||||||
|
EhViewer fork dedicated to lightweight and high-performance
|
||||||
|
|
||||||
|
with Material Design 3 and Dynamic Color Support
|
||||||
|
|
||||||
|
# Download
|
||||||
|
|
||||||
|
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
|
||||||
|
|
||||||
|
# Screenshot
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
# Thanks
|
||||||
|
|
||||||
|
Here is the libraries
|
||||||
|
|
||||||
|
- [AOSP & AndroidX](http://source.android.com/)
|
||||||
|
- [Coil](https://coil-kt.github.io/coil/)
|
||||||
|
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
|
||||||
|
- [GnuTLS](https://gnutls.org/)
|
||||||
|
- [material-components-android](https://github.com/material-components/material-components-android)
|
||||||
|
- [material-design-icons](https://github.com/google/material-design-icons)
|
||||||
|
- [jsoup](https://github.com/jhy/jsoup)
|
||||||
|
- [okhttp](https://github.com/square/okhttp)
|
||||||
|
- [RikkaX](https://github.com/RikkaApps/RikkaX)
|
||||||
|
- [libarchive](http://www.libarchive.org/)
|
||||||
|
- [XZ Utils](https://tukaani.org/xz/)
|
||||||
|
|
||||||
Synced from [EhViewer/EhViewer on codeberg](https://codeberg.org/EhViewer/EhViewer), the continuation of FooIbar's EhViewer. Feature work happens there; this fork tracks it and carries the network/resilience patches above.
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
Copyright 2014-2019 Hippo Seven
|
Copyright 2014-2019 Hippo Seven
|
||||||
Copyright 2020-2022 NekoInverter
|
Copyright 2020-2022 NekoInverter
|
||||||
Copyright 2022-2023 Tarsin Norbin
|
Copyright 2022-2023 Tarsin Norbin
|
||||||
Copyright 2023-2024 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/build
|
||||||
|
/src/main/java-gen
|
||||||
|
manifest-merger-release-report.txt
|
||||||
|
/src/main/libs
|
||||||
|
/src/main/obj
|
||||||
|
# Intellij
|
||||||
|
*.iml
|
||||||
|
/.cxx
|
||||||
+141
-193
@@ -1,32 +1,27 @@
|
|||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
|
import java.io.ByteArrayOutputStream
|
||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateRule
|
import java.time.Instant
|
||||||
import java.util.regex.Pattern
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
val isRelease: Boolean
|
|
||||||
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.ehviewer.android.application)
|
id("com.android.application")
|
||||||
alias(libs.plugins.kotlin.serialization)
|
kotlin("android")
|
||||||
alias(libs.plugins.ksp)
|
id("kotlin-parcelize")
|
||||||
alias(libs.plugins.aboutlibraries)
|
kotlin("plugin.serialization")
|
||||||
alias(libs.plugins.aboutlibrariesAndroid)
|
id("com.google.devtools.ksp")
|
||||||
alias(libs.plugins.baselineprofile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val supportedAbis = arrayOf("arm64-v8a", "x86_64", "armeabi-v7a")
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
compileSdk = 33
|
||||||
|
buildToolsVersion = "33.0.2"
|
||||||
|
ndkVersion = "25.2.9519653"
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
isEnable = true
|
isEnable = true
|
||||||
reset()
|
reset()
|
||||||
if (isRelease) {
|
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
|
||||||
include(*supportedAbis)
|
isUniversalApk = true
|
||||||
isUniversalApk = true
|
|
||||||
} else {
|
|
||||||
include("arm64-v8a", "x86_64")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,55 +30,49 @@ android {
|
|||||||
storePassword = "000000"
|
storePassword = "000000"
|
||||||
keyAlias = "key0"
|
keyAlias = "key0"
|
||||||
keyPassword = "000000"
|
keyPassword = "000000"
|
||||||
|
|
||||||
enableV3Signing = true
|
enableV3Signing = true
|
||||||
enableV4Signing = true
|
enableV4Signing = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val commitSha = providers.exec {
|
val commitSha by lazy {
|
||||||
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
val stdout = ByteArrayOutputStream()
|
||||||
}.standardOutput.asText.get().trim()
|
exec {
|
||||||
|
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
||||||
val commitTime = providers.exec {
|
standardOutput = stdout
|
||||||
commandLine = "git log -1 --format=%ct".split(' ')
|
|
||||||
}.standardOutput.asText.get().trim()
|
|
||||||
|
|
||||||
val repoName = providers.exec {
|
|
||||||
commandLine = "git remote get-url origin".split(' ')
|
|
||||||
}.standardOutput.asText.get().trim().removeSuffix(".git").removePrefix("https://codeberg.org/")
|
|
||||||
|
|
||||||
val snapshot = !hasProperty("release")
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "yenru0.yrkaierx.ehviewer"
|
|
||||||
versionCode = 180064
|
|
||||||
versionName = if (snapshot) {
|
|
||||||
"1.15.0-SNAPSHOT"
|
|
||||||
} else {
|
|
||||||
"1.15.0"
|
|
||||||
}
|
|
||||||
buildConfigField("boolean", "SNAPSHOT", "$snapshot")
|
|
||||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName\"")
|
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
|
||||||
buildConfigField("long", "COMMIT_TIME", commitTime)
|
|
||||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
|
||||||
ndk {
|
|
||||||
if (isRelease) {
|
|
||||||
abiFilters.addAll(supportedAbis)
|
|
||||||
}
|
|
||||||
debugSymbolLevel = "FULL"
|
|
||||||
}
|
}
|
||||||
|
stdout.toString().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions += "api"
|
val buildTime by lazy {
|
||||||
|
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
|
||||||
|
formatter.format(Instant.now())
|
||||||
|
}
|
||||||
|
|
||||||
productFlavors {
|
defaultConfig {
|
||||||
create("default") {
|
applicationId = "moe.tarsin.ehviewer"
|
||||||
minSdk = 26
|
minSdk = 28
|
||||||
}
|
targetSdk = 33
|
||||||
create("marshmallow") {
|
versionCode = 180036
|
||||||
applicationIdSuffix = ".m"
|
versionName = "1.8.7.1"
|
||||||
versionNameSuffix = "-M"
|
resourceConfigurations.addAll(
|
||||||
}
|
listOf(
|
||||||
|
"zh",
|
||||||
|
"zh-rCN",
|
||||||
|
"zh-rHK",
|
||||||
|
"zh-rTW",
|
||||||
|
"es",
|
||||||
|
"ja",
|
||||||
|
"ko",
|
||||||
|
"fr",
|
||||||
|
"de",
|
||||||
|
"th",
|
||||||
|
"tr",
|
||||||
|
"nb-rNO"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||||
|
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
@@ -93,51 +82,46 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
sourceCompatibility = JavaVersion.VERSION_19
|
||||||
|
targetCompatibility = JavaVersion.VERSION_19
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "19"
|
||||||
|
freeCompilerArgs = listOf(
|
||||||
|
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||||
|
"-progressive",
|
||||||
|
|
||||||
|
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||||
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
|
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||||
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
abortOnError = true
|
||||||
|
checkReleaseBuilds = false
|
||||||
|
disable.add("MissingTranslation")
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
dex {
|
|
||||||
useLegacyPackaging = false
|
|
||||||
}
|
|
||||||
jniLibs {
|
|
||||||
excludes += "**/libdatastore_shared_counter.so" // DataStore multi-process
|
|
||||||
}
|
|
||||||
resources {
|
resources {
|
||||||
// Required by Layout Inspector
|
excludes += "/META-INF/CHANGES"
|
||||||
pickFirsts += "/META-INF/androidx.compose.ui_ui.version"
|
excludes += "/META-INF/README.md"
|
||||||
|
excludes += "/kotlin/**"
|
||||||
excludes += listOf(
|
excludes += "**.txt"
|
||||||
"/META-INF/**",
|
excludes += "**.bin"
|
||||||
"/kotlin/**",
|
|
||||||
"**.txt",
|
|
||||||
"**.bin",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidResources {
|
|
||||||
ignoreAssetsPatterns += listOf(
|
|
||||||
"!PublicSuffixDatabase.list", // OkHttp
|
|
||||||
"!composepreference.preference.generated.resources",
|
|
||||||
)
|
|
||||||
generateLocaleConfig = true
|
|
||||||
localeFilters += listOf(
|
|
||||||
"zh",
|
|
||||||
"zh-rCN",
|
|
||||||
"zh-rHK",
|
|
||||||
"zh-rTW",
|
|
||||||
"es",
|
|
||||||
"ja",
|
|
||||||
"ko",
|
|
||||||
"fr",
|
|
||||||
"de",
|
|
||||||
"th",
|
|
||||||
"tr",
|
|
||||||
"nb-rNO",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependenciesInfo.includeInApk = false
|
dependenciesInfo.includeInApk = false
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -150,127 +134,91 @@ android {
|
|||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
}
|
}
|
||||||
create("benchmarkRelease") {
|
|
||||||
initWith(buildTypes.getByName("release"))
|
|
||||||
matchingFallbacks += listOf("release")
|
|
||||||
applicationIdSuffix = ".benchmark"
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
isDebuggable = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
compose = true
|
compose = true
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.4.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace = "com.hippo.ehviewer"
|
namespace = "com.hippo.ehviewer"
|
||||||
}
|
}
|
||||||
|
|
||||||
baselineProfile {
|
|
||||||
mergeIntoMain = true
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.core.data)
|
|
||||||
implementation(projects.core.i18n)
|
|
||||||
implementation(projects.core.ui)
|
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation("androidx.activity:activity-compose:1.6.1")
|
||||||
implementation(libs.androidx.appcompat)
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation(libs.androidx.biometric)
|
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
|
||||||
implementation(libs.androidx.browser)
|
implementation("androidx.browser:browser:1.5.0")
|
||||||
implementation(libs.androidx.webkit)
|
implementation("androidx.collection:collection-ktx:1.3.0-alpha02")
|
||||||
|
|
||||||
implementation(libs.compose.destinations.core)
|
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
||||||
ksp(libs.compose.destinations.compiler)
|
implementation(platform("androidx.compose:compose-bom:2023.01.00"))
|
||||||
|
implementation("androidx.compose.material:material-icons-extended")
|
||||||
|
implementation("androidx.compose.material3:material3")
|
||||||
|
|
||||||
implementation(libs.compose.preference) {
|
implementation("androidx.core:core-ktx:1.10.0-beta01")
|
||||||
// R8 won't remove it because it adds a content provider
|
|
||||||
exclude(group = "org.jetbrains.compose.components", module = "components-resources")
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation(libs.androidx.core)
|
|
||||||
implementation(libs.androidx.core.splashscreen)
|
|
||||||
|
|
||||||
implementation(libs.androidx.datastore)
|
|
||||||
|
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:2.2.0-alpha07")
|
||||||
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||||
implementation(libs.androidx.lifecycle.process)
|
implementation("androidx.lifecycle:lifecycle-process:2.5.1")
|
||||||
implementation(libs.androidx.lifecycle.compose)
|
|
||||||
|
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||||
|
val nav_version = "2.5.3"
|
||||||
|
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
|
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||||
implementation(libs.androidx.paging.compose)
|
implementation("androidx.paging:paging-runtime-ktx:3.2.0-alpha04")
|
||||||
|
implementation("androidx.paging:paging-compose:1.0.0-alpha18")
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||||
|
implementation("androidx.recyclerview:recyclerview:1.3.0-rc01")
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/room
|
// https://developer.android.com/jetpack/androidx/releases/room
|
||||||
implementation(libs.androidx.room.paging)
|
val room_version = "2.5.0"
|
||||||
|
ksp("androidx.room:room-compiler:$room_version")
|
||||||
|
implementation("androidx.room:room-paging:$room_version")
|
||||||
|
|
||||||
implementation(libs.androidx.work.runtime)
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
implementation(libs.material.motion.core)
|
implementation("androidx.work:work-runtime-ktx:2.8.0")
|
||||||
implementation(libs.material.kolor)
|
implementation("com.drakeet.drawer:drawer:1.0.3")
|
||||||
|
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
|
||||||
implementation(libs.bundles.splitties)
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
|
||||||
|
// https://github.com/google/accompanist/releases
|
||||||
|
implementation("com.google.accompanist:accompanist-themeadapter-material3:0.28.0")
|
||||||
|
implementation("com.google.android.material:material:1.8.0")
|
||||||
|
|
||||||
// https://square.github.io/okhttp/changelogs/changelog/
|
// https://square.github.io/okhttp/changelogs/changelog/
|
||||||
implementation(platform(libs.okhttp.bom))
|
implementation("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11")
|
||||||
|
implementation("com.squareup.okhttp3:okhttp-coroutines")
|
||||||
|
|
||||||
implementation(libs.logcat)
|
implementation("com.squareup.okio:okio-jvm:3.3.0")
|
||||||
|
|
||||||
implementation(libs.diff)
|
|
||||||
|
|
||||||
implementation(libs.aboutlibraries.core)
|
|
||||||
implementation(libs.aboutlibraries.compose.m3)
|
|
||||||
implementation(libs.accompanist.drawable.painter)
|
|
||||||
|
|
||||||
implementation(libs.reorderable)
|
|
||||||
|
|
||||||
implementation(platform(libs.arrow.stack))
|
|
||||||
implementation(libs.bundles.arrow)
|
|
||||||
|
|
||||||
|
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.2.0")
|
||||||
|
implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3")
|
||||||
|
implementation("dev.rikka.rikkax.material:material-preference:2.0.0")
|
||||||
// https://coil-kt.github.io/coil/changelog/
|
// https://coil-kt.github.io/coil/changelog/
|
||||||
implementation(platform(libs.coil.bom))
|
implementation("io.coil-kt:coil-compose:2.2.2")
|
||||||
implementation(libs.bundles.coil)
|
|
||||||
|
|
||||||
implementation(libs.telephoto.zoomable)
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.0")
|
||||||
implementation(libs.ktor.client.okhttp)
|
implementation("org.jsoup:jsoup:1.15.4")
|
||||||
|
|
||||||
implementation(libs.bundles.kotlinx.serialization)
|
|
||||||
|
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
|
||||||
implementation(libs.kotlinx.datetime)
|
|
||||||
|
|
||||||
coreLibraryDesugaring(libs.desugar)
|
|
||||||
|
|
||||||
implementation(libs.androidx.profileinstaller)
|
|
||||||
"baselineProfile"(project(":benchmark"))
|
|
||||||
|
|
||||||
debugImplementation(libs.compose.ui.tooling)
|
|
||||||
implementation(libs.compose.ui.tooling.preview)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
configurations.all {
|
||||||
compilerOptions {
|
exclude("dev.rikka.rikkax.appcompat", "appcompat")
|
||||||
optIn.addAll(
|
exclude("dev.rikka.rikkax.material", "material")
|
||||||
"coil3.annotation.ExperimentalCoilApi",
|
exclude("org.jetbrains.kotlin", "kotlin-android-extensions-runtime")
|
||||||
"androidx.paging.ExperimentalPagingApi",
|
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
|
||||||
"me.saket.telephoto.ExperimentalTelephotoApi",
|
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ksp {
|
|
||||||
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
|
|
||||||
}
|
|
||||||
|
|
||||||
aboutLibraries {
|
|
||||||
collect {
|
|
||||||
includePlatform = false
|
|
||||||
}
|
|
||||||
library {
|
|
||||||
exclusionPatterns.add(Pattern.compile("org\\.jetbrains\\.(?:compose|androidx)\\..*"))
|
|
||||||
duplicationMode = DuplicateMode.MERGE
|
|
||||||
duplicationRule = DuplicateRule.GROUP
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Vendored
+11
-19
@@ -1,32 +1,24 @@
|
|||||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
|
||||||
native <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers enum * {
|
|
||||||
public static **[] values();
|
|
||||||
public static ** valueOf(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
-keepclassmembers,allowobfuscation class com.hippo.ehviewer.coil.AnimatedWebPDrawable {
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
java.nio.ByteBuffer source;
|
native <methods>;
|
||||||
}
|
}
|
||||||
|
|
||||||
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
|
# https://issuetracker.google.com/issues/190382641
|
||||||
static boolean isLinkBufferComposerEnabled() return true;
|
-keepclassmembers class kotlin.SafePublicationLazyImpl {
|
||||||
|
java.lang.Object _value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
|
||||||
|
|
||||||
# Ktor logger
|
# Ktor logger
|
||||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
|
|
||||||
# https://issuetracker.google.com/222232895
|
-keepattributes LineNumberTable,SourceFile
|
||||||
-dontwarn androidx.window.extensions.**
|
-renamesourcefileattribute SourceFile
|
||||||
-dontwarn androidx.window.sidecar.Sidecar*
|
|
||||||
|
|
||||||
-keepattributes LineNumberTable
|
|
||||||
|
|
||||||
-allowaccessmodification
|
|
||||||
-repackageclasses
|
-repackageclasses
|
||||||
|
-allowaccessmodification
|
||||||
|
-overloadaggressively
|
||||||
@@ -12,8 +12,7 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="28" />
|
android:maxSdkVersion="28" />
|
||||||
@@ -28,21 +27,23 @@
|
|||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="@bool/large_heap"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
<profileable android:shell="true" />
|
<activity
|
||||||
|
android:name=".ui.login.LoginActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:windowSoftInputMode="adjustResize"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:banner="@mipmap/ic_leanback_banner"
|
android:banner="@mipmap/ic_leanback_banner"
|
||||||
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
|
android:configChanges="screenSize"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/AppTheme.Splash"
|
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
|
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -69,39 +70,8 @@
|
|||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
<data android:host="exhentai.org" />
|
<data android:host="exhentai.org" />
|
||||||
<data android:host="e-hentai.org" />
|
<data android:host="e-hentai.org" />
|
||||||
</intent-filter>
|
<data android:host="g.e-hentai.org" />
|
||||||
<intent-filter>
|
<data android:host="lofi.e-hentai.org" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="file" />
|
|
||||||
<data android:scheme="content" />
|
|
||||||
<data android:host="*" />
|
|
||||||
<data android:mimeType="application/octet-stream" />
|
|
||||||
<data android:mimeType="application/zip" />
|
|
||||||
<data android:mimeType="application/x-7z-compressed" />
|
|
||||||
<data android:mimeType="application/x-tar" />
|
|
||||||
<data android:mimeType="application/x-xz" />
|
|
||||||
<data android:mimeType="application/gzip" />
|
|
||||||
<data android:mimeType="application/vnd.rar" />
|
|
||||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
|
||||||
<data android:mimeType="application/vnd.comicbook-rar" />
|
|
||||||
|
|
||||||
<!-- unofficial -->
|
|
||||||
<data android:mimeType="application/7z" />
|
|
||||||
<data android:mimeType="application/rar" />
|
|
||||||
<data android:mimeType="application/x-cbz" />
|
|
||||||
<data android:mimeType="application/x-cbr" />
|
|
||||||
<data android:mimeType="application/x-cbt" />
|
|
||||||
<data android:mimeType="application/x-cb7" />
|
|
||||||
<data android:mimeType="application/x-zip-compressed" />
|
|
||||||
<data android:mimeType="application/x-rar-compressed" />
|
|
||||||
<data android:mimeType="application/x-compressed-tar" />
|
|
||||||
<data android:mimeType="application/x-bzip-compressed-tar" />
|
|
||||||
<data android:mimeType="application/x-lzma-compressed-tar" />
|
|
||||||
<data android:mimeType="application/x-xz-compressed-tar" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:label="@string/keyword_search">
|
<intent-filter android:label="@string/keyword_search">
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
@@ -118,6 +88,54 @@
|
|||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="eu.kanade.tachiyomi.ui.reader.ReaderActivity"
|
||||||
|
android:configChanges="screenSize"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="file" />
|
||||||
|
<data android:scheme="content" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:mimeType="application/octet-stream" />
|
||||||
|
<data android:mimeType="application/7z" />
|
||||||
|
<data android:mimeType="application/rar" />
|
||||||
|
<data android:mimeType="application/zip" />
|
||||||
|
<data android:mimeType="application/x-7z-compressed" />
|
||||||
|
<data android:mimeType="application/x-rar-compressed" />
|
||||||
|
<data android:mimeType="application/x-zip-compressed" />
|
||||||
|
<data android:mimeType="application/x-tar" />
|
||||||
|
<data android:mimeType="application/x-xz" />
|
||||||
|
<data android:mimeType="application/gzip" />
|
||||||
|
<data android:mimeType="application/x-cbz" />
|
||||||
|
<data android:mimeType="application/x-cbr" />
|
||||||
|
<data android:mimeType="application/x-cbt" />
|
||||||
|
<data android:mimeType="application/x-cb7" />
|
||||||
|
|
||||||
|
<!-- untested -->
|
||||||
|
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||||
|
<data android:mimeType="application/vnd.comicbook-rar" />
|
||||||
|
<data android:mimeType="application/x-compressed-tar" />
|
||||||
|
<data android:mimeType="application/x-bzip-compressed-tar" />
|
||||||
|
<data android:mimeType="application/x-lzma-compressed-tar" />
|
||||||
|
<data android:mimeType="application/x-xz-compressed-tar" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.SettingsActivity"
|
||||||
|
android:configChanges="screenSize"
|
||||||
|
android:label="@string/settings"
|
||||||
|
android:theme="@style/AppTheme" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.SecurityActivity"
|
||||||
|
android:configChanges="screenSize"
|
||||||
|
android:label="@string/settings"
|
||||||
|
android:theme="@style/AppTheme" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".shortcuts.ShortcutsActivity"
|
android:name=".shortcuts.ShortcutsActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
From fc9fb0ea245259e346ab46c334f03b557fe63b6b Mon Sep 17 00:00:00 2001
|
||||||
|
From: tarsin <yuanqingxiang233@163.com>
|
||||||
|
Date: Tue, 22 Nov 2022 12:37:12 +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 01c73cd..b461e68 100644
|
||||||
|
--- a/CMakeLists.txt
|
||||||
|
+++ b/CMakeLists.txt
|
||||||
|
@@ -592,7 +592,7 @@ check_symbol_exists(getopt_long getopt.h HAVE_GETOPT_LONG)
|
||||||
|
# xzdec
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
-if(HAVE_GETOPT_LONG)
|
||||||
|
+if(0)
|
||||||
|
add_executable(xzdec
|
||||||
|
src/common/sysdefs.h
|
||||||
|
src/common/tuklib_common.h
|
||||||
|
@@ -630,7 +630,7 @@ endif()
|
||||||
|
# xz
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
-if(NOT MSVC AND HAVE_GETOPT_LONG)
|
||||||
|
+if(0)
|
||||||
|
add_executable(xz
|
||||||
|
src/common/mythread.h
|
||||||
|
src/common/sysdefs.h
|
||||||
|
--
|
||||||
|
2.39.2
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
From 2a67861e6f06794f3671c4988bac12330d8422e3 Mon Sep 17 00:00:00 2001
|
||||||
|
From: tarsin <yuanqingxiang233@163.com>
|
||||||
|
Date: Fri, 27 May 2022 23:21:20 +0800
|
||||||
|
Subject: [PATCH 1/2] Don't build shared lib
|
||||||
|
|
||||||
|
---
|
||||||
|
libarchive/CMakeLists.txt | 8 +-------
|
||||||
|
1 file changed, 1 insertion(+), 7 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/libarchive/CMakeLists.txt b/libarchive/CMakeLists.txt
|
||||||
|
index ff7ade00..00c2e170 100644
|
||||||
|
--- a/libarchive/CMakeLists.txt
|
||||||
|
+++ b/libarchive/CMakeLists.txt
|
||||||
|
@@ -242,12 +242,6 @@ ELSEIF(ARCHIVE_ACL_SUNOS)
|
||||||
|
LIST(APPEND libarchive_SOURCES archive_disk_acl_sunos.c)
|
||||||
|
ENDIF()
|
||||||
|
|
||||||
|
-# Libarchive is a shared library
|
||||||
|
-ADD_LIBRARY(archive SHARED ${libarchive_SOURCES} ${include_HEADERS})
|
||||||
|
-TARGET_INCLUDE_DIRECTORIES(archive PUBLIC .)
|
||||||
|
-TARGET_LINK_LIBRARIES(archive ${ADDITIONAL_LIBS})
|
||||||
|
-SET_TARGET_PROPERTIES(archive PROPERTIES SOVERSION ${SOVERSION})
|
||||||
|
-
|
||||||
|
# archive_static is a static library
|
||||||
|
ADD_LIBRARY(archive_static STATIC ${libarchive_SOURCES} ${include_HEADERS})
|
||||||
|
TARGET_LINK_LIBRARIES(archive_static ${ADDITIONAL_LIBS})
|
||||||
|
@@ -260,7 +254,7 @@ ENDIF(NOT WIN32 OR CYGWIN)
|
||||||
|
|
||||||
|
IF(ENABLE_INSTALL)
|
||||||
|
# How to install the libraries
|
||||||
|
- INSTALL(TARGETS archive archive_static
|
||||||
|
+ INSTALL(TARGETS archive_static
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
LIBRARY DESTINATION lib
|
||||||
|
ARCHIVE DESTINATION lib)
|
||||||
|
--
|
||||||
|
2.39.2
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
From e971b9f23727833cc39b9e325db30f383e5bfc30 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Dude so hot <djohn@fbi.gov>
|
|
||||||
Date: Thu, 14 Nov 2024 00:28:13 +0800
|
|
||||||
Subject: [PATCH] Fix zip_time performance
|
|
||||||
|
|
||||||
Signed-off-by: Dude so hot <djohn@fbi.gov>
|
|
||||||
---
|
|
||||||
libarchive/archive_time.c | 1 +
|
|
||||||
1 file changed, 1 insertion(+)
|
|
||||||
|
|
||||||
diff --git a/libarchive/archive_time.c b/libarchive/archive_time.c
|
|
||||||
index 3352c809..9289bf1e 100644
|
|
||||||
--- a/libarchive/archive_time.c
|
|
||||||
+++ b/libarchive/archive_time.c
|
|
||||||
@@ -53,6 +53,7 @@ FILETIME_to_ntfs(const FILETIME* filetime)
|
|
||||||
int64_t
|
|
||||||
dos_to_unix(uint32_t dos_time)
|
|
||||||
{
|
|
||||||
+ return 0;
|
|
||||||
uint16_t msTime, msDate;
|
|
||||||
struct tm ts;
|
|
||||||
time_t t;
|
|
||||||
--
|
|
||||||
2.47.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
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
From 86d199429b3fabc83f7dcc9afd72e7b4c7750b1a Mon Sep 17 00:00:00 2001
|
|
||||||
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
|
||||||
Date: Thu, 14 Nov 2024 20:22:52 +0800
|
|
||||||
Subject: [PATCH] Use UTF-8 as default charset on bionic
|
|
||||||
|
|
||||||
---
|
|
||||||
libarchive/archive_string.c | 4 +++-
|
|
||||||
1 file changed, 3 insertions(+), 1 deletion(-)
|
|
||||||
|
|
||||||
diff --git a/libarchive/archive_string.c b/libarchive/archive_string.c
|
|
||||||
index abf7ad66..63a73c58 100644
|
|
||||||
--- a/libarchive/archive_string.c
|
|
||||||
+++ b/libarchive/archive_string.c
|
|
||||||
@@ -423,7 +423,9 @@ static const char *
|
|
||||||
default_iconv_charset(const char *charset) {
|
|
||||||
if (charset != NULL && charset[0] != '\0')
|
|
||||||
return charset;
|
|
||||||
-#if HAVE_LOCALE_CHARSET && !defined(__APPLE__)
|
|
||||||
+#ifdef __BIONIC__
|
|
||||||
+ return "UTF-8";
|
|
||||||
+#elif HAVE_LOCALE_CHARSET && !defined(__APPLE__)
|
|
||||||
/* locale_charset() is broken on Mac OS */
|
|
||||||
return locale_charset();
|
|
||||||
#elif HAVE_NL_LANGINFO
|
|
||||||
--
|
|
||||||
2.43.0
|
|
||||||
|
|
||||||
+32
-129
@@ -1,79 +1,40 @@
|
|||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 3.14)
|
||||||
project(ehviewer C)
|
project(ehviewer C)
|
||||||
include(ExternalProject)
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
string(CONCAT CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -ffast-math -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto "
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto=thin \
|
||||||
"-ffile-prefix-map=${ANDROID_TOOLCHAIN_ROOT}= "
|
-mllvm -polly \
|
||||||
"-ffile-prefix-map=${CMAKE_SOURCE_DIR}= "
|
-mllvm -polly-run-dce \
|
||||||
"-ffile-prefix-map=${CMAKE_BINARY_DIR}= "
|
-mllvm -polly-run-inliner \
|
||||||
"-mllvm -polly "
|
-mllvm -polly-isl-arg=--no-schedule-serialize-sccs \
|
||||||
"-mllvm -polly-run-dce "
|
-mllvm -polly-ast-use-context \
|
||||||
"-mllvm -polly-run-inliner "
|
-mllvm -polly-detect-keep-going \
|
||||||
"-mllvm -polly-isl-arg=--no-schedule-serialize-sccs "
|
-mllvm -polly-position=before-vectorizer \
|
||||||
"-mllvm -polly-ast-use-context "
|
-mllvm -polly-vectorizer=stripmine \
|
||||||
"-mllvm -polly-detect-keep-going "
|
-mllvm -polly-detect-profitability-min-per-loop-insts=40 \
|
||||||
"-mllvm -polly-position=before-vectorizer "
|
-mllvm -polly-invariant-load-hoisting")
|
||||||
"-mllvm -polly-vectorizer=stripmine "
|
|
||||||
"-mllvm -polly-detect-profitability-min-per-loop-insts=40 "
|
|
||||||
"-mllvm -polly-invariant-load-hoisting")
|
|
||||||
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
|
|
||||||
option(BUILD_TESTING OFF)
|
# Change the default path of fetch content to avoid downloading the same dependencies per architecture
|
||||||
option(XZ_DOC OFF)
|
get_filename_component(deps "../_deps" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
|
||||||
option(XZ_LZIP_DECODER OFF)
|
set(FETCHCONTENT_BASE_DIR ${deps})
|
||||||
option(XZ_MICROLZMA_DECODER OFF)
|
|
||||||
option(XZ_MICROLZMA_ENCODER OFF)
|
FetchContent_Declare(liblzma
|
||||||
option(XZ_TOOL_LZMADEC OFF)
|
GIT_REPOSITORY https://git.tukaani.org/xz.git
|
||||||
option(XZ_TOOL_LZMAINFO OFF)
|
GIT_TAG v5.4.1
|
||||||
option(XZ_TOOL_XZ OFF)
|
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binarys.patch || true
|
||||||
option(XZ_TOOL_XZDEC OFF)
|
)
|
||||||
FetchContent_Declare(
|
|
||||||
liblzma
|
|
||||||
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
|
|
||||||
GIT_TAG v5.8.3
|
|
||||||
GIT_SHALLOW 1
|
|
||||||
)
|
|
||||||
|
|
||||||
FetchContent_MakeAvailable(liblzma)
|
FetchContent_MakeAvailable(liblzma)
|
||||||
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
|
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
|
||||||
|
|
||||||
# Build GNUTLS libnettle
|
# Build GNUTLS libnettle
|
||||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
|
add_subdirectory(nettle)
|
||||||
set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}")
|
|
||||||
set(TOOLCHAIN "$ANDROID_HOME/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin")
|
|
||||||
set(WSL wsl)
|
|
||||||
else ()
|
|
||||||
set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin")
|
|
||||||
endif ()
|
|
||||||
set(CONFIGURE_COMMAND ${WSL} ./.bootstrap && ${WSL} ./configure --disable-public-key --disable-dependency-tracking
|
|
||||||
"--host=${CMAKE_C_COMPILER_TARGET}"
|
|
||||||
"CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments"
|
|
||||||
"AR=${TOOLCHAIN}/llvm-ar"
|
|
||||||
"CC=${TOOLCHAIN}/clang --target=${CMAKE_C_COMPILER_TARGET}"
|
|
||||||
"RANLIB=${TOOLCHAIN}/llvm-ranlib"
|
|
||||||
)
|
|
||||||
ExternalProject_Add(
|
|
||||||
nettle
|
|
||||||
GIT_REPOSITORY https://gitlab.com/gnutls/nettle.git
|
|
||||||
GIT_TAG nettle_4.0_release_20260205
|
|
||||||
GIT_SHALLOW 1
|
|
||||||
GIT_CONFIG core.autocrlf=input
|
|
||||||
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
|
|
||||||
BUILD_COMMAND ${WSL} make -j4 libnettle.a
|
|
||||||
INSTALL_COMMAND ""
|
|
||||||
BUILD_IN_SOURCE 1
|
|
||||||
BUILD_BYPRODUCTS <SOURCE_DIR>/libnettle.a
|
|
||||||
)
|
|
||||||
|
|
||||||
ExternalProject_Get_Property(nettle SOURCE_DIR)
|
|
||||||
include_directories(${SOURCE_DIR}/..)
|
|
||||||
add_library(libnettle STATIC IMPORTED)
|
|
||||||
set_property(TARGET libnettle PROPERTY IMPORTED_LOCATION ${SOURCE_DIR}/libnettle.a)
|
|
||||||
add_dependencies(libnettle nettle)
|
|
||||||
|
|
||||||
# Configure libnettle support for libarchive
|
# Configure libnettle support for libarchive
|
||||||
|
include_directories(nettle)
|
||||||
|
include_directories(.)
|
||||||
set(HAVE_LIBNETTLE 1)
|
set(HAVE_LIBNETTLE 1)
|
||||||
set(HAVE_NETTLE_AES_H 1)
|
set(HAVE_NETTLE_AES_H 1)
|
||||||
set(HAVE_NETTLE_HMAC_H 1)
|
set(HAVE_NETTLE_HMAC_H 1)
|
||||||
@@ -89,76 +50,18 @@ SET(HAVE_LZMA_STREAM_ENCODER_MT 1)
|
|||||||
SET(HAVE_LZMADEC_H 1)
|
SET(HAVE_LZMADEC_H 1)
|
||||||
SET(HAVE_LIBLZMADEC 1)
|
SET(HAVE_LIBLZMADEC 1)
|
||||||
|
|
||||||
option(ENABLE_OPENSSL OFF)
|
# Configure libarchive link's static lib
|
||||||
option(ENABLE_TAR OFF)
|
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
|
||||||
option(ENABLE_CPIO OFF)
|
|
||||||
option(ENABLE_CAT OFF)
|
|
||||||
option(ENABLE_UNZIP OFF)
|
|
||||||
option(ENABLE_TEST OFF)
|
|
||||||
|
|
||||||
set(LIBARCHIVE_PATCH
|
FetchContent_Declare(libarchive
|
||||||
${CMAKE_CURRENT_LIST_DIR}/0001-Fix-zip_time-performance.patch
|
GIT_REPOSITORY https://github.com/libarchive/libarchive
|
||||||
${CMAKE_CURRENT_LIST_DIR}/0002-Use-UTF-8-as-default-charset-on-bionic.patch
|
GIT_TAG v3.6.2
|
||||||
)
|
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-shared-lib.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || true
|
||||||
FetchContent_Declare(
|
)
|
||||||
libarchive
|
|
||||||
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
|
||||||
GIT_TAG v3.8.7
|
|
||||||
GIT_SHALLOW 1
|
|
||||||
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
|
|
||||||
)
|
|
||||||
|
|
||||||
FetchContent_MakeAvailable(libarchive)
|
FetchContent_MakeAvailable(libarchive)
|
||||||
target_link_libraries(archive_static liblzma libnettle)
|
|
||||||
include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
||||||
|
|
||||||
FetchContent_Declare(
|
|
||||||
libwebp
|
|
||||||
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
|
|
||||||
GIT_TAG v1.6.0
|
|
||||||
GIT_SHALLOW 1
|
|
||||||
)
|
|
||||||
|
|
||||||
option(WEBP_BUILD_ANIM_UTILS OFF)
|
|
||||||
option(WEBP_BUILD_CWEBP OFF)
|
|
||||||
option(WEBP_BUILD_DWEBP OFF)
|
|
||||||
option(WEBP_BUILD_VWEBP OFF)
|
|
||||||
option(WEBP_BUILD_WEBPINFO OFF)
|
|
||||||
option(WEBP_BUILD_LIBWEBPMUX OFF)
|
|
||||||
option(WEBP_BUILD_EXTRAS OFF)
|
|
||||||
option(WEBP_ENABLE_SWAP_16BIT_CSP ON)
|
|
||||||
|
|
||||||
# Disable runtime detection of NEON support as NEON is required since API level 23
|
|
||||||
# https://source.android.com/docs/compatibility/6.0/android-6.0-cdd#3_3_1_application_binary_interfaces
|
|
||||||
unset(ANDROID)
|
|
||||||
FetchContent_MakeAvailable(libwebp)
|
|
||||||
if (CMAKE_SYSTEM_NAME STREQUAL "Android")
|
|
||||||
set(ANDROID 1)
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ../../../../rust-toolchain.toml)
|
|
||||||
unset(Rust_TOOLCHAIN CACHE)
|
|
||||||
FetchContent_Declare(
|
|
||||||
Corrosion
|
|
||||||
GIT_REPOSITORY https://github.com/FooIbar/corrosion.git
|
|
||||||
GIT_TAG v0.6.1
|
|
||||||
GIT_SHALLOW 1
|
|
||||||
)
|
|
||||||
|
|
||||||
FetchContent_MakeAvailable(Corrosion)
|
|
||||||
corrosion_import_crate(
|
|
||||||
MANIFEST_PATH ../rust/Cargo.toml
|
|
||||||
LOCKED
|
|
||||||
FEATURES $<IF:$<VERSION_GREATER_EQUAL:${ANDROID_NATIVE_API_LEVEL},26>,android-26,android>
|
|
||||||
FLAGS $<$<NOT:$<CONFIG:Debug>>:-Z build-std -Z build-std-features=>
|
|
||||||
)
|
|
||||||
corrosion_add_target_rustflags(ehviewer_rust $<$<NOT:$<CONFIG:Debug>>:-Z unstable-options -C panic=immediate-abort -Z location-detail=none -Z fmt-debug=none>)
|
|
||||||
corrosion_link_libraries(ehviewer_rust android jnigraphics log webp webpdemux)
|
|
||||||
|
|
||||||
# Build and link our app's native lib
|
# Build and link our app's native lib
|
||||||
add_library(${PROJECT_NAME} SHARED archive.c gifutils.c hash.c natsort/strnatcmp.c)
|
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c natsort/strnatcmp.c)
|
||||||
target_link_libraries(
|
target_link_libraries(${PROJECT_NAME} archive_static log)
|
||||||
${PROJECT_NAME} archive_static ehviewer_rust
|
|
||||||
-Wl,-u,JNI_OnLoad -Wl,--undefined-glob,Java_*
|
|
||||||
-Wl,--version-script,${CMAKE_SOURCE_DIR}/libehviewer.map.txt
|
|
||||||
)
|
|
||||||
+220
-229
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2022-2024 Tarsin Norbin
|
* Copyright 2022 Tarsin Norbin
|
||||||
*
|
*
|
||||||
* This file is part of EhViewer
|
* This file is part of EhViewer
|
||||||
*
|
*
|
||||||
@@ -45,29 +45,33 @@ typedef struct {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
const char *filename;
|
const char *filename;
|
||||||
int index;
|
int index;
|
||||||
ssize_t size;
|
size_t size;
|
||||||
void *addr;
|
|
||||||
} entry;
|
} entry;
|
||||||
|
|
||||||
|
pthread_mutex_t ctx_lock;
|
||||||
|
static archive_ctx **ctx_pool;
|
||||||
#define CTX_POOL_SIZE 20
|
#define CTX_POOL_SIZE 20
|
||||||
#define MAX_PARALLEL_DECOMP 4
|
|
||||||
#define max(a, b) ((a) > (b) ? (a) : (b))
|
|
||||||
|
|
||||||
static pthread_mutex_t ctx_pool_mutex = PTHREAD_MUTEX_INITIALIZER;
|
static void *mempool = NULL;
|
||||||
static archive_ctx **ctx_pool = NULL;
|
static size_t *mempoolofs = NULL;
|
||||||
static pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
||||||
static void *decode_buffer[MAX_PARALLEL_DECOMP];
|
#define PAGE_ALIGN(x) ((x + ~PAGE_MASK) & PAGE_MASK)
|
||||||
|
|
||||||
|
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
|
||||||
|
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
|
||||||
|
#define MEMPOOL_ENTITY_SIZE(x) PAGE_ALIGN(entries[x].size)
|
||||||
|
|
||||||
|
#define PROT_RW (PROT_WRITE | PROT_READ)
|
||||||
|
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
|
||||||
|
|
||||||
static bool need_encrypt = false;
|
static bool need_encrypt = false;
|
||||||
static char *passwd = NULL;
|
static char *passwd = NULL;
|
||||||
static void *archiveAddr = MAP_FAILED;
|
static void *archiveAddr = NULL;
|
||||||
static size_t archiveSize = 0;
|
static size_t archiveSize = 0;
|
||||||
static entry *entries = NULL;
|
static entry *entries = NULL;
|
||||||
static size_t entryCount = 0;
|
static size_t entryCount = 0;
|
||||||
static ssize_t max_file_size = 0;
|
|
||||||
|
|
||||||
#define SUPPORT_EXT_COUNT 11
|
const char supportExt[9][6] = {
|
||||||
|
|
||||||
const char supportExt[SUPPORT_EXT_COUNT][5] = {
|
|
||||||
"jpeg",
|
"jpeg",
|
||||||
"jpg",
|
"jpg",
|
||||||
"png",
|
"png",
|
||||||
@@ -76,104 +80,92 @@ const char supportExt[SUPPORT_EXT_COUNT][5] = {
|
|||||||
"bmp",
|
"bmp",
|
||||||
"ico",
|
"ico",
|
||||||
"wbmp",
|
"wbmp",
|
||||||
"heic",
|
"heif"
|
||||||
"heif",
|
|
||||||
"avif"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline int filename_is_playable_file(const char *name) {
|
static inline int filename_is_playable_file(const char *name) {
|
||||||
if (!name)
|
|
||||||
return false;
|
|
||||||
const char *dotptr = strrchr(name, '.');
|
const char *dotptr = strrchr(name, '.');
|
||||||
if (!dotptr++)
|
if (!dotptr++)
|
||||||
return false;
|
return false;
|
||||||
int i;
|
int i;
|
||||||
for (i = 0; i < SUPPORT_EXT_COUNT; i++)
|
for (i = 0; i < 9; i++)
|
||||||
if (strcmp(dotptr, supportExt[i]) == 0)
|
if (strcmp(dotptr, supportExt[i]) == 0)
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline bool archive_entry_is_file(struct archive_entry *entry) {
|
|
||||||
return archive_entry_filetype(entry) == AE_IFREG;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool archive_entry_is_playable(struct archive_entry *entry) {
|
|
||||||
return archive_entry_is_file(entry) &&
|
|
||||||
filename_is_playable_file(archive_entry_pathname(entry));
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int compare_entries(const void *a, const void *b) {
|
static inline int compare_entries(const void *a, const void *b) {
|
||||||
const char *fa = ((entry *) a)->filename;
|
const char *fa = ((entry *) a)->filename;
|
||||||
const char *fb = ((entry *) b)->filename;
|
const char *fb = ((entry *) b)->filename;
|
||||||
return strnatcmp(fa, fb);
|
return strnatcmp(fa, fb);
|
||||||
}
|
}
|
||||||
|
|
||||||
#define ADDR_IN_FILE_MAPPING(addr) (addr >= archiveAddr && addr < archiveAddr + archiveSize)
|
static long archive_map_entries_index(archive_ctx *ctx) {
|
||||||
|
long count = 0;
|
||||||
static bool fill_entry_zero_copy(struct archive *arc, entry *entry) {
|
|
||||||
void *buffer = NULL;
|
|
||||||
size_t buffer_size = 0;
|
|
||||||
la_int64_t output_ofs = 0;
|
|
||||||
archive_read_data_block(arc, (const void **) &buffer, &buffer_size, &output_ofs);
|
|
||||||
bool zero_copy = ADDR_IN_FILE_MAPPING(buffer) && !output_ofs && buffer_size == entry->size;
|
|
||||||
entry->addr = zero_copy ? buffer : NULL;
|
|
||||||
return zero_copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void archive_map_entries_index(archive_ctx *ctx, bool sort) {
|
|
||||||
int count = 0;
|
|
||||||
bool zero_copy = true;
|
|
||||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||||
const char *name = archive_entry_pathname(ctx->entry);
|
const char *name = archive_entry_pathname(ctx->entry);
|
||||||
if (archive_entry_is_file(ctx->entry) && filename_is_playable_file(name)) {
|
if (filename_is_playable_file(name)) {
|
||||||
entries[count].filename = strdup(name);
|
entries[count].filename = strdup(name);
|
||||||
entries[count].index = count;
|
entries[count].index = count;
|
||||||
ssize_t size = archive_entry_size(ctx->entry);
|
entries[count].size = archive_entry_size(ctx->entry);
|
||||||
max_file_size = max(size, max_file_size);
|
|
||||||
entries[count].size = size;
|
|
||||||
// We don't expect zero copy if first content can't do zero copy
|
|
||||||
if (zero_copy) zero_copy = fill_entry_zero_copy(ctx->arc, &entries[count]);
|
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sort) qsort(entries, entryCount, sizeof(entry), compare_entries);
|
if (!count)
|
||||||
|
LOGE("%s", archive_error_string(ctx->arc));
|
||||||
|
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void *acquire_decode_buffer() {
|
static bool archive_prealloc_mempool() {
|
||||||
void *addr = NULL;
|
mempoolofs = calloc(entryCount, sizeof(size_t));
|
||||||
pthread_mutex_lock(&buffer_mutex);
|
for (int i = 0; i < entryCount; ++i) {
|
||||||
for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {
|
if (!i)
|
||||||
addr = decode_buffer[i];
|
mempoolofs[i] = PAGE_ALIGN(entries[i].size);
|
||||||
if (addr) {
|
else
|
||||||
decode_buffer[i] = NULL;
|
mempoolofs[i] = PAGE_ALIGN(entries[i].size) + mempoolofs[i - 1];
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pthread_mutex_unlock(&buffer_mutex);
|
mempool = mmap(0, MEMPOOL_SIZE, PROT_RW, MAP_ANON_POOL, -1, 0);
|
||||||
if (!addr) addr = malloc(max_file_size);
|
if (mempool == MAP_FAILED) {
|
||||||
return addr;
|
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
||||||
}
|
return false;
|
||||||
|
|
||||||
static void release_decode_buffer(void *buffer) {
|
|
||||||
pthread_mutex_lock(&buffer_mutex);
|
|
||||||
for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {
|
|
||||||
void *addr = decode_buffer[i];
|
|
||||||
if (!addr) {
|
|
||||||
decode_buffer[i] = buffer;
|
|
||||||
pthread_mutex_unlock(&buffer_mutex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pthread_mutex_unlock(&buffer_mutex);
|
return true;
|
||||||
free(buffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int archive_list_all_entries(archive_ctx *ctx) {
|
|
||||||
int count = 0;
|
// Mincore does not support anon mapping yet
|
||||||
|
// Now that we use MADV_FREE, how can we detect whether pages is reclaimed?
|
||||||
|
static bool is_mempool_pages_present_and_lock(int index) {
|
||||||
|
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||||
|
size_t size = MEMPOOL_ENTITY_SIZE(index);
|
||||||
|
mlock(addr, size);
|
||||||
|
#if 0
|
||||||
|
unsigned char vec[size / PAGE_SIZE];
|
||||||
|
unsigned char r = 1;
|
||||||
|
mincore(addr, size, vec);
|
||||||
|
for (int i = 0; i < size / PAGE_SIZE; ++i) r &= vec[i];
|
||||||
|
return r;
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MEMPOOL_IN_CORE_AND_LOCK(x) is_mempool_pages_present_and_lock(x)
|
||||||
|
|
||||||
|
static void mempool_release_pages(void *addr, size_t size) {
|
||||||
|
size = PAGE_ALIGN(size);
|
||||||
|
munlock(addr, size);
|
||||||
|
int ret = madvise(addr, size, MADV_FREE);
|
||||||
|
if (ret == -EINVAL) madvise(addr, size, MADV_DONTNEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||||
|
long count = 0;
|
||||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||||
if (archive_entry_is_playable(ctx->entry))
|
if (filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||||
count++;
|
count++;
|
||||||
|
if (!count)
|
||||||
|
LOGE("%s", archive_error_string(ctx->arc));
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,10 +177,18 @@ static void archive_release_ctx(archive_ctx *ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static archive_ctx *archive_alloc_ctx() {
|
static int archive_alloc_ctx(archive_ctx **ctxptr) {
|
||||||
archive_ctx *ctx = calloc(1, sizeof(archive_ctx));
|
archive_ctx *ctx;
|
||||||
|
int err;
|
||||||
|
ctx = calloc(1, sizeof(archive_ctx));
|
||||||
|
if (!ctx)
|
||||||
|
return -ENOMEM;
|
||||||
ctx->arc = archive_read_new();
|
ctx->arc = archive_read_new();
|
||||||
ctx->using = 1;
|
ctx->using = 1;
|
||||||
|
if (!ctx->arc) {
|
||||||
|
free(ctx);
|
||||||
|
return -ENOMEM;
|
||||||
|
}
|
||||||
archive_read_support_format_tar(ctx->arc);
|
archive_read_support_format_tar(ctx->arc);
|
||||||
archive_read_support_format_7zip(ctx->arc);
|
archive_read_support_format_7zip(ctx->arc);
|
||||||
archive_read_support_format_rar5(ctx->arc);
|
archive_read_support_format_rar5(ctx->arc);
|
||||||
@@ -198,19 +198,19 @@ static archive_ctx *archive_alloc_ctx() {
|
|||||||
archive_read_set_option(ctx->arc, "zip", "ignorecrc32", "1");
|
archive_read_set_option(ctx->arc, "zip", "ignorecrc32", "1");
|
||||||
if (passwd)
|
if (passwd)
|
||||||
archive_read_add_passphrase(ctx->arc, passwd);
|
archive_read_add_passphrase(ctx->arc, passwd);
|
||||||
int err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);
|
err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);
|
||||||
if (err < ARCHIVE_OK) {
|
if (err) {
|
||||||
LOGE("%s%s", "Open archive failed: ", archive_error_string(ctx->arc));
|
|
||||||
archive_read_free(ctx->arc);
|
archive_read_free(ctx->arc);
|
||||||
free(ctx);
|
free(ctx);
|
||||||
return NULL;
|
return err;
|
||||||
}
|
}
|
||||||
return ctx;
|
*ctxptr = ctx;
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||||
if (!archive_entry_is_playable(ctx->entry))
|
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||||
continue;
|
continue;
|
||||||
if (ctx->next_index++ == index) {
|
if (ctx->next_index++ == index) {
|
||||||
return ctx->next_index - 1;
|
return ctx->next_index - 1;
|
||||||
@@ -222,7 +222,7 @@ static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
|||||||
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||||
int ret;
|
int ret;
|
||||||
archive_ctx *ctx = NULL;
|
archive_ctx *ctx = NULL;
|
||||||
pthread_mutex_lock(&ctx_pool_mutex);
|
pthread_mutex_lock(&ctx_lock);
|
||||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||||
if (!ctx_pool[i])
|
if (!ctx_pool[i])
|
||||||
continue;
|
continue;
|
||||||
@@ -237,14 +237,16 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
|||||||
}
|
}
|
||||||
if (ctx)
|
if (ctx)
|
||||||
ctx->using = 1;
|
ctx->using = 1;
|
||||||
pthread_mutex_unlock(&ctx_pool_mutex);
|
pthread_mutex_unlock(&ctx_lock);
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
archive_ctx *victimCtx = NULL;
|
archive_ctx *victimCtx = NULL;
|
||||||
int victimIdx = 0;
|
int victimIdx = 0;
|
||||||
int replace = 1;
|
int replace = 1;
|
||||||
ctx = archive_alloc_ctx();
|
ret = archive_alloc_ctx(&ctx);
|
||||||
pthread_mutex_lock(&ctx_pool_mutex);
|
if (ret)
|
||||||
|
return ret;
|
||||||
|
pthread_mutex_lock(&ctx_lock);
|
||||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||||
if (!ctx_pool[i]) {
|
if (!ctx_pool[i]) {
|
||||||
ctx_pool[i] = ctx;
|
ctx_pool[i] = ctx;
|
||||||
@@ -258,14 +260,16 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
|||||||
victimIdx = i;
|
victimIdx = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (replace) ctx_pool[victimIdx] = ctx;
|
if (replace) {
|
||||||
pthread_mutex_unlock(&ctx_pool_mutex);
|
archive_release_ctx(victimCtx);
|
||||||
if (replace) archive_release_ctx(victimCtx);
|
ctx_pool[victimIdx] = ctx;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&ctx_lock);
|
||||||
}
|
}
|
||||||
ret = archive_skip_to_index(ctx, idx);
|
ret = archive_skip_to_index(ctx, idx);
|
||||||
if (ret != idx) {
|
if (ret != idx) {
|
||||||
ret = archive_errno(ctx->arc);
|
ret = archive_errno(ctx->arc);
|
||||||
LOGE("Skip to index failed: %s", archive_error_string(ctx->arc));
|
LOGE("Skip to index failed:%s", archive_error_string(ctx->arc));
|
||||||
archive_release_ctx(ctx);
|
archive_release_ctx(ctx);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -274,146 +278,163 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd, jlong size, jboolean sort_entries) {
|
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd, jlong size) {
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
archive_ctx *ctx = NULL;
|
archive_ctx *ctx = NULL;
|
||||||
archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
int mmap_flags = MAP_PRIVATE;
|
||||||
|
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
|
||||||
if (archiveAddr == MAP_FAILED) {
|
if (archiveAddr == MAP_FAILED) {
|
||||||
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
archiveSize = size;
|
archiveSize = size;
|
||||||
|
madvise_log_if_error(archiveAddr, archiveSize, MADV_WILLNEED);
|
||||||
|
pthread_mutex_init(&ctx_lock, 0);
|
||||||
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
|
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
|
||||||
ctx = archive_alloc_ctx();
|
if (!ctx_pool) {
|
||||||
if (!ctx) return 0;
|
LOGE("Allocate archive ctx pool failed:ENOMEM");
|
||||||
|
|
||||||
entryCount = archive_list_all_entries(ctx);
|
|
||||||
LOGI("%s%zu%s", "Found ", entryCount, " images in archive");
|
|
||||||
if (!entryCount) {
|
|
||||||
LOGE("%s%s", "Archive read failed: ", archive_error_string(ctx->arc));
|
|
||||||
archive_release_ctx(ctx);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
long r = archive_alloc_ctx(&ctx);
|
||||||
|
if (r) {
|
||||||
|
if (r == -ENOMEM)
|
||||||
|
LOGE("%s", "Mem alloc failed");
|
||||||
|
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||||
|
} else {
|
||||||
|
entryCount = archive_list_all_entries(ctx);
|
||||||
|
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
|
||||||
|
|
||||||
// We must read through the file|vm then we can know whether it is encrypted
|
// We must read through the file|vm then we can know whether it is encrypted
|
||||||
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
||||||
switch (encryptRet) {
|
switch (encryptRet) {
|
||||||
case 1: // At lease 1 encrypted entry
|
case 1: // At lease 1 encrypted entry
|
||||||
need_encrypt = true;
|
need_encrypt = true;
|
||||||
break;
|
break;
|
||||||
case 0: // format supports but no encrypted entry found
|
case 0: // format supports but no encrypted entry found
|
||||||
default:
|
default:
|
||||||
need_encrypt = false;
|
need_encrypt = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int format = archive_format(ctx->arc);
|
||||||
|
switch (format) {
|
||||||
|
case ARCHIVE_FORMAT_ZIP:
|
||||||
|
case ARCHIVE_FORMAT_RAR_V5:
|
||||||
|
madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);
|
||||||
|
break;
|
||||||
|
case ARCHIVE_FORMAT_7ZIP: // Seek is bad
|
||||||
|
madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);
|
||||||
|
break;
|
||||||
|
default:;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int format = archive_format(ctx->arc);
|
|
||||||
switch (format) {
|
|
||||||
case ARCHIVE_FORMAT_ZIP:
|
|
||||||
case ARCHIVE_FORMAT_RAR_V5:
|
|
||||||
madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);
|
|
||||||
break;
|
|
||||||
case ARCHIVE_FORMAT_7ZIP: // Seek is bad
|
|
||||||
madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);
|
|
||||||
break;
|
|
||||||
default:;
|
|
||||||
}
|
|
||||||
archive_release_ctx(ctx);
|
archive_release_ctx(ctx);
|
||||||
|
|
||||||
ctx = archive_alloc_ctx();
|
r = archive_alloc_ctx(&ctx);
|
||||||
if (!ctx) return 0;
|
if (r) {
|
||||||
entries = calloc(entryCount, sizeof(entry));
|
if (r == -ENOMEM)
|
||||||
archive_map_entries_index(ctx, sort_entries);
|
LOGE("%s", "Mem alloc failed");
|
||||||
|
r = 0;
|
||||||
|
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||||
|
} else {
|
||||||
|
entries = calloc(entryCount, sizeof(entry));
|
||||||
|
r = archive_map_entries_index(ctx);
|
||||||
|
if (!archive_prealloc_mempool()) {
|
||||||
|
r = 0;
|
||||||
|
for (int i = 0; i < entryCount; ++i) {
|
||||||
|
free((void *) entries[i].filename);
|
||||||
|
}
|
||||||
|
free(entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
archive_release_ctx(ctx);
|
archive_release_ctx(ctx);
|
||||||
return (int) entryCount;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma ide diagnostic ignored "ConstantConditionsOC"
|
||||||
|
#pragma ide diagnostic ignored "UnreachableCode"
|
||||||
|
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz, jint index) {
|
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
entry *entry = &entries[index];
|
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||||
ssize_t size = entry->size;
|
size_t size = entries[index].size;
|
||||||
if (entry->addr) {
|
if (!MEMPOOL_IN_CORE_AND_LOCK(index)) {
|
||||||
return (*env)->NewDirectByteBuffer(env, entry->addr, size);
|
index = entries[index].index;
|
||||||
} else {
|
|
||||||
archive_ctx *ctx = NULL;
|
archive_ctx *ctx = NULL;
|
||||||
if (!archive_get_ctx(&ctx, entry->index)) {
|
int ret;
|
||||||
void *addr = acquire_decode_buffer();
|
ret = archive_get_ctx(&ctx, index);
|
||||||
ssize_t bytes = archive_read_data(ctx->arc, addr, size);
|
if (ret) return 0;
|
||||||
ctx->using = 0;
|
ret = archive_read_data(ctx->arc, addr, size);
|
||||||
if (bytes == size) {
|
ctx->using = 0;
|
||||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
if (ret == size)
|
||||||
} else {
|
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||||
if (bytes < 0) {
|
if (ret != size)
|
||||||
LOGE("%s%s", "Archive read failed: ", archive_error_string(ctx->arc));
|
LOGE("%s", "No enough data read, WTF?");
|
||||||
} else {
|
if (ret < 0)
|
||||||
LOGE("%s", "No enough data read, WTF?");
|
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||||
}
|
mempool_release_pages(addr, size);
|
||||||
}
|
} else {
|
||||||
release_decode_buffer(addr);
|
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
if (ctx_pool) {
|
for (int i = 0; i < CTX_POOL_SIZE; i++)
|
||||||
for (int i = 0; i < CTX_POOL_SIZE; i++)
|
archive_release_ctx(ctx_pool[i]);
|
||||||
archive_release_ctx(ctx_pool[i]);
|
|
||||||
free(ctx_pool);
|
|
||||||
ctx_pool = NULL;
|
|
||||||
}
|
|
||||||
free(passwd);
|
free(passwd);
|
||||||
|
free(ctx_pool);
|
||||||
|
pthread_mutex_destroy(&ctx_lock);
|
||||||
passwd = NULL;
|
passwd = NULL;
|
||||||
need_encrypt = false;
|
need_encrypt = false;
|
||||||
if (archiveAddr != MAP_FAILED) {
|
munmap(archiveAddr, archiveSize);
|
||||||
munmap(archiveAddr, archiveSize);
|
munmap(mempool, MEMPOOL_SIZE);
|
||||||
archiveAddr = MAP_FAILED;
|
free(mempoolofs);
|
||||||
}
|
for (int i = 0; i < entryCount; ++i) {
|
||||||
for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {
|
free((void *) entries[i].filename);
|
||||||
free(decode_buffer[i]);
|
|
||||||
decode_buffer[i] = NULL;
|
|
||||||
}
|
|
||||||
max_file_size = 0;
|
|
||||||
if (entries) {
|
|
||||||
for (int i = 0; i < entryCount; ++i) {
|
|
||||||
free((void *) entries[i].filename);
|
|
||||||
}
|
|
||||||
free(entries);
|
|
||||||
entries = NULL;
|
|
||||||
}
|
}
|
||||||
|
free(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {
|
Java_com_hippo_UriArchiveAccessor_needPassword(JNIEnv *env, jobject thiz) {
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
return need_encrypt;
|
return need_encrypt;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz, jstring str) {
|
Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *env, jobject thiz, jstring str) {
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
struct archive_entry *entry;
|
struct archive_entry *entry;
|
||||||
archive_ctx *ctx;
|
archive_ctx *ctx;
|
||||||
jboolean ret = true;
|
jboolean ret = true;
|
||||||
int len = (*env)->GetStringUTFLength(env, str);
|
int len = (*env)->GetStringUTFLength(env, str);
|
||||||
passwd = realloc(passwd, len + 1);
|
passwd = realloc(passwd, len * sizeof(char));
|
||||||
(*env)->GetStringUTFRegion(env, str, 0, len, passwd);
|
if (!passwd) {
|
||||||
passwd[len] = 0;
|
LOGE("Allocate passwd buffer failed");
|
||||||
ctx = archive_alloc_ctx();
|
return false;
|
||||||
char tmpBuf[4096];
|
}
|
||||||
|
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
|
||||||
|
archive_alloc_ctx(&ctx);
|
||||||
|
void *tmpBuf = alloca(4096);
|
||||||
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
|
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
|
||||||
if (!archive_entry_is_playable(entry))
|
if (!filename_is_playable_file(archive_entry_pathname(entry)))
|
||||||
continue;
|
continue;
|
||||||
if (!archive_entry_is_encrypted(entry))
|
if (!archive_entry_is_encrypted(entry))
|
||||||
continue;
|
continue;
|
||||||
if (archive_read_data(ctx->arc, tmpBuf, 4096) < ARCHIVE_OK) {
|
if (archive_read_data(ctx->arc, tmpBuf, 4096) < ARCHIVE_OK) {
|
||||||
LOGE("%s%s", "Archive read failed: ", archive_error_string(ctx->arc));
|
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||||
ret = false;
|
ret = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -423,15 +444,22 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
|||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL
|
JNIEXPORT jstring JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_getExtension(JNIEnv *env, jclass thiz, jint index) {
|
Java_com_hippo_UriArchiveAccessor_getFilename(JNIEnv *env, jobject thiz, jint index) {
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
const char *ext = strrchr(entries[index].filename, '.') + 1;
|
index = entries[index].index;
|
||||||
return (*env)->NewStringUTF(env, ext);
|
archive_ctx *ctx = NULL;
|
||||||
|
int ret;
|
||||||
|
ret = archive_get_ctx(&ctx, index);
|
||||||
|
if (ret)
|
||||||
|
return NULL;
|
||||||
|
jstring str = (*env)->NewStringUTF(env, archive_entry_pathname(ctx->entry));
|
||||||
|
ctx->using = 0;
|
||||||
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz, jint index, jint fd) {
|
Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint index, jint fd) {
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
index = entries[index].index;
|
index = entries[index].index;
|
||||||
@@ -439,52 +467,15 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz, jint
|
|||||||
int ret;
|
int ret;
|
||||||
ret = archive_get_ctx(&ctx, index);
|
ret = archive_get_ctx(&ctx, index);
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
ret = archive_read_data_into_fd(ctx->arc, fd);
|
archive_read_data_into_fd(ctx->arc, fd);
|
||||||
ctx->using = 0;
|
ctx->using = 0;
|
||||||
}
|
}
|
||||||
return ret == ARCHIVE_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz, jobject buffer) {
|
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||||
if (!ADDR_IN_FILE_MAPPING(addr)) {
|
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||||
release_decode_buffer(addr);
|
mempool_release_pages(addr, size);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_archiveFdBatch(JNIEnv *env, jclass clazz, jintArray fd_batch, jobjectArray names, jint arc_fd, jint size) {
|
|
||||||
EH_UNUSED(clazz);
|
|
||||||
struct archive *arc = archive_write_new();
|
|
||||||
struct stat st;
|
|
||||||
char buff[8192];
|
|
||||||
jint fdBatch[size];
|
|
||||||
(*env)->GetIntArrayRegion(env, fd_batch, 0, size, fdBatch);
|
|
||||||
archive_write_set_format_zip(arc);
|
|
||||||
archive_write_zip_set_compression_store(arc);
|
|
||||||
archive_write_open_fd(arc, arc_fd);
|
|
||||||
struct archive_entry *entry = archive_entry_new();
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
int fd = fdBatch[i];
|
|
||||||
jobject name = (*env)->GetObjectArrayElement(env, names, i);
|
|
||||||
const char *cname = (*env)->GetStringUTFChars(env, name, false);
|
|
||||||
archive_entry_set_pathname(entry, cname);
|
|
||||||
(*env)->ReleaseStringUTFChars(env, name, cname);
|
|
||||||
fstat(fd, &st);
|
|
||||||
archive_entry_copy_stat(entry, &st);
|
|
||||||
archive_entry_set_perm(entry, 0644);
|
|
||||||
archive_write_header(arc, entry);
|
|
||||||
size_t len;
|
|
||||||
do {
|
|
||||||
len = read(fd, buff, sizeof(buff));
|
|
||||||
archive_write_data(arc, buff, len);
|
|
||||||
} while (len > 0);
|
|
||||||
archive_write_finish_entry(arc);
|
|
||||||
archive_entry_clear(entry);
|
|
||||||
}
|
|
||||||
archive_entry_free(entry);
|
|
||||||
archive_write_close(arc);
|
|
||||||
archive_write_free(arc);
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#include <jni.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/sendfile.h>
|
||||||
|
|
||||||
|
#include <android/log.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#define LOG_TAG "mmap_utils"
|
||||||
|
|
||||||
|
#include "ehviewer.h"
|
||||||
|
|
||||||
|
// TODO: Replace it with AFileDescriptor_getFd when minsdk 31
|
||||||
|
// https://developer.android.com/ndk/reference/group/file-descriptor
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||||
|
jint fd = -1;
|
||||||
|
jclass fdClass = (*env)->FindClass(env, "java/io/FileDescriptor");
|
||||||
|
|
||||||
|
if (fdClass != NULL) {
|
||||||
|
jfieldID fdClassDescriptorFieldID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
|
||||||
|
if (fdClassDescriptorFieldID != NULL && fileDescriptor != NULL) {
|
||||||
|
fd = (*env)->GetIntField(env, fileDescriptor, fdClassDescriptorFieldID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_hippo_Native_sendfile(JNIEnv *env, jclass clazz, jint from, jint to) {
|
||||||
|
struct stat st;
|
||||||
|
fstat(from, &st);
|
||||||
|
sendfile(to, from, 0, st.st_size);
|
||||||
|
}
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <sys/mman.h>
|
|
||||||
|
|
||||||
#define LOG_TAG "gifUtils"
|
#define LOG_TAG "gifUtils"
|
||||||
|
|
||||||
@@ -12,7 +9,6 @@
|
|||||||
|
|
||||||
#define GIF_HEADER_87A "GIF87a"
|
#define GIF_HEADER_87A "GIF87a"
|
||||||
#define GIF_HEADER_89A "GIF89a"
|
#define GIF_HEADER_89A "GIF89a"
|
||||||
#define GIF_HEADER_LENGTH 6
|
|
||||||
|
|
||||||
static int FRAME_DELAY_START_MARKER = 0x0021F904;
|
static int FRAME_DELAY_START_MARKER = 0x0021F904;
|
||||||
|
|
||||||
@@ -23,11 +19,13 @@ typedef signed char byte;
|
|||||||
#define DEFAULT_FRAME_DELAY 10
|
#define DEFAULT_FRAME_DELAY 10
|
||||||
|
|
||||||
static inline bool isGif(void *addr) {
|
static inline bool isGif(void *addr) {
|
||||||
return !memcmp(addr, GIF_HEADER_87A, GIF_HEADER_LENGTH) ||
|
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
|
||||||
!memcmp(addr, GIF_HEADER_89A, GIF_HEADER_LENGTH);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void doRewrite(byte *addr, size_t size) {
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_hippo_image_Image_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||||
|
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||||
|
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||||
if (size < 7 || !isGif(addr)) return;
|
if (size < 7 || !isGif(addr)) return;
|
||||||
for (size_t i = 0; i < size - 8; i++) {
|
for (size_t i = 0; i < size - 8; i++) {
|
||||||
// TODO: Optimize this hex find with SIMD?
|
// TODO: Optimize this hex find with SIMD?
|
||||||
@@ -37,41 +35,10 @@ static void doRewrite(byte *addr, size_t size) {
|
|||||||
byte *end = addr + i + 4;
|
byte *end = addr + i + 4;
|
||||||
if (end[4] != 0) continue;
|
if (end[4] != 0) continue;
|
||||||
int frameDelay = end[2] << 8 | end[1];
|
int frameDelay = end[2] << 8 | end[1];
|
||||||
if (frameDelay >= MINIMUM_FRAME_DELAY)
|
if (frameDelay > MINIMUM_FRAME_DELAY)
|
||||||
break; // Quit if the first block looks normal, for performance
|
break; // Quit if the first block looks normal, for performance
|
||||||
end[1] = DEFAULT_FRAME_DELAY;
|
end[1] = DEFAULT_FRAME_DELAY;
|
||||||
end[2] = 0;
|
end[2] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
|
||||||
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) {
|
|
||||||
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);
|
|
||||||
munmap(addr, size);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
#include <nettle/sha1.h>
|
|
||||||
|
|
||||||
#include "ehviewer.h"
|
|
||||||
|
|
||||||
#define BUFFER_SIZE 8192
|
|
||||||
|
|
||||||
typedef uint8_t byte;
|
|
||||||
|
|
||||||
const char hex_digits[] = "0123456789abcdef";
|
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL
|
|
||||||
Java_com_hippo_ehviewer_jni_HashKt_sha1(JNIEnv *env, jclass clazz, jint fd) {
|
|
||||||
EH_UNUSED(clazz);
|
|
||||||
struct sha1_ctx ctx;
|
|
||||||
sha1_init(&ctx);
|
|
||||||
|
|
||||||
size_t bytes_read;
|
|
||||||
byte buffer[BUFFER_SIZE];
|
|
||||||
while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) {
|
|
||||||
sha1_update(&ctx, bytes_read, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte digest[SHA1_DIGEST_SIZE];
|
|
||||||
sha1_digest(&ctx, digest);
|
|
||||||
|
|
||||||
byte byte;
|
|
||||||
char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
|
|
||||||
for (int i = 0; i < SHA1_DIGEST_SIZE; i++) {
|
|
||||||
byte = digest[i];
|
|
||||||
hex_digest[2 * i] = hex_digits[byte >> 4 & 0xF];
|
|
||||||
hex_digest[2 * i + 1] = hex_digits[byte & 0xF];
|
|
||||||
}
|
|
||||||
hex_digest[2 * SHA1_DIGEST_SIZE] = '\0';
|
|
||||||
|
|
||||||
return (*env)->NewStringUTF(env, hex_digest);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
global:
|
|
||||||
JNI_OnLoad;
|
|
||||||
Java_*;
|
|
||||||
local:
|
|
||||||
*;
|
|
||||||
};
|
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
* negative chars in their default char type.
|
* negative chars in their default char type.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <stddef.h> /* size_t */
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
|
||||||
#include "strnatcmp.h"
|
#include "strnatcmp.h"
|
||||||
@@ -38,21 +39,28 @@
|
|||||||
/* These are defined as macros to make it easier to adapt this code to
|
/* These are defined as macros to make it easier to adapt this code to
|
||||||
* different characters types or comparison functions. */
|
* different characters types or comparison functions. */
|
||||||
static inline int
|
static inline int
|
||||||
nat_isdigit(const char *a)
|
nat_isdigit(nat_char a)
|
||||||
{
|
{
|
||||||
return isdigit((unsigned char) *a);
|
return isdigit((unsigned char) a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static inline int
|
static inline int
|
||||||
nat_isspace(const char *a)
|
nat_isspace(nat_char a)
|
||||||
{
|
{
|
||||||
return isspace((unsigned char) *a) || *a == '0' && nat_isdigit(a + 1);
|
return isspace((unsigned char) a);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline nat_char
|
||||||
|
nat_toupper(nat_char a)
|
||||||
|
{
|
||||||
|
return toupper((unsigned char) a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
compare_right(const char *a, const char *b)
|
compare_right(nat_char const *a, nat_char const *b)
|
||||||
{
|
{
|
||||||
int bias = 0;
|
int bias = 0;
|
||||||
|
|
||||||
@@ -61,11 +69,11 @@ compare_right(const char *a, const char *b)
|
|||||||
both numbers to know that they have the same magnitude, so we
|
both numbers to know that they have the same magnitude, so we
|
||||||
remember it in BIAS. */
|
remember it in BIAS. */
|
||||||
for (;; a++, b++) {
|
for (;; a++, b++) {
|
||||||
if (!nat_isdigit(a) && !nat_isdigit(b))
|
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||||
return bias;
|
return bias;
|
||||||
if (!nat_isdigit(a))
|
if (!nat_isdigit(*a))
|
||||||
return -1;
|
return -1;
|
||||||
if (!nat_isdigit(b))
|
if (!nat_isdigit(*b))
|
||||||
return +1;
|
return +1;
|
||||||
if (*a < *b) {
|
if (*a < *b) {
|
||||||
if (!bias)
|
if (!bias)
|
||||||
@@ -76,41 +84,94 @@ compare_right(const char *a, const char *b)
|
|||||||
} else if (!*a && !*b)
|
} else if (!*a && !*b)
|
||||||
return bias;
|
return bias;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int
|
static int
|
||||||
strnatcmp(const char *a, const char *b)
|
compare_left(nat_char const *a, nat_char const *b)
|
||||||
{
|
{
|
||||||
int result;
|
/* Compare two left-aligned numbers: the first to have a
|
||||||
|
different value wins. */
|
||||||
|
for (;; a++, b++) {
|
||||||
|
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||||
|
return 0;
|
||||||
|
if (!nat_isdigit(*a))
|
||||||
|
return -1;
|
||||||
|
if (!nat_isdigit(*b))
|
||||||
|
return +1;
|
||||||
|
if (*a < *b)
|
||||||
|
return -1;
|
||||||
|
if (*a > *b)
|
||||||
|
return +1;
|
||||||
|
}
|
||||||
|
|
||||||
while (1) {
|
return 0;
|
||||||
/* skip over leading spaces or zeros */
|
}
|
||||||
while (nat_isspace(a))
|
|
||||||
a++;
|
|
||||||
|
|
||||||
while (nat_isspace(b))
|
|
||||||
b++;
|
static int
|
||||||
|
strnatcmp0(nat_char const *a, nat_char const *b, int fold_case)
|
||||||
|
{
|
||||||
|
int ai, bi;
|
||||||
|
nat_char ca, cb;
|
||||||
|
int fractional, result;
|
||||||
|
|
||||||
|
ai = bi = 0;
|
||||||
|
while (1) {
|
||||||
|
ca = a[ai]; cb = b[bi];
|
||||||
|
|
||||||
|
/* skip over leading spaces or zeros */
|
||||||
|
while (nat_isspace(ca))
|
||||||
|
ca = a[++ai];
|
||||||
|
|
||||||
|
while (nat_isspace(cb))
|
||||||
|
cb = b[++bi];
|
||||||
|
|
||||||
/* process run of digits */
|
/* process run of digits */
|
||||||
if (nat_isdigit(a) && nat_isdigit(b)) {
|
if (nat_isdigit(ca) && nat_isdigit(cb)) {
|
||||||
if ((result = compare_right(a, b)) != 0)
|
fractional = (ca == '0' || cb == '0');
|
||||||
return result;
|
|
||||||
|
if (fractional) {
|
||||||
|
if ((result = compare_left(a+ai, b+bi)) != 0)
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
if ((result = compare_right(a+ai, b+bi)) != 0)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!*a && !*b) {
|
if (!ca && !cb) {
|
||||||
/* The strings compare the same. Perhaps the caller
|
/* The strings compare the same. Perhaps the caller
|
||||||
will want to call strcmp to break the tie. */
|
will want to call strcmp to break the tie. */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*a < *b)
|
if (fold_case) {
|
||||||
|
ca = nat_toupper(ca);
|
||||||
|
cb = nat_toupper(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ca < cb)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
if (*a > *b)
|
if (ca > cb)
|
||||||
return +1;
|
return +1;
|
||||||
|
|
||||||
a++; b++;
|
++ai; ++bi;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
strnatcmp(nat_char const *a, nat_char const *b) {
|
||||||
|
return strnatcmp0(a, b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Compare, recognizing numeric string and ignoring case. */
|
||||||
|
int
|
||||||
|
strnatcasecmp(nat_char const *a, nat_char const *b) {
|
||||||
|
return strnatcmp0(a, b, 1);
|
||||||
|
}
|
||||||
@@ -24,7 +24,14 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int strnatcmp(const char *a, const char *b);
|
/* CUSTOMIZATION SECTION
|
||||||
|
*
|
||||||
|
* You can change this typedef, but must then also change the inline
|
||||||
|
* functions in strnatcmp.c */
|
||||||
|
typedef char nat_char;
|
||||||
|
|
||||||
|
int strnatcmp(nat_char const *a, nat_char const *b);
|
||||||
|
int strnatcasecmp(nat_char const *a, nat_char const *b);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.4.1)
|
||||||
|
project(nettle C)
|
||||||
|
|
||||||
|
set(LIBNETTLE_DEFINITIONS
|
||||||
|
-DHAVE_CONFIG_H)
|
||||||
|
|
||||||
|
set(LIBNETTLE_SOURCES
|
||||||
|
nettle/aes-decrypt-internal.c
|
||||||
|
nettle/aes-decrypt.c
|
||||||
|
nettle/aes-decrypt-table.c
|
||||||
|
nettle/aes128-decrypt.c
|
||||||
|
nettle/aes192-decrypt.c
|
||||||
|
nettle/aes256-decrypt.c
|
||||||
|
nettle/aes-encrypt-internal.c
|
||||||
|
nettle/aes-encrypt.c
|
||||||
|
nettle/aes-encrypt-table.c
|
||||||
|
nettle/aes128-encrypt.c
|
||||||
|
nettle/aes192-encrypt.c
|
||||||
|
nettle/aes256-encrypt.c
|
||||||
|
nettle/aes-invert-internal.c
|
||||||
|
nettle/aes-set-key-internal.c
|
||||||
|
nettle/aes-set-encrypt-key.c
|
||||||
|
nettle/aes-set-decrypt-key.c
|
||||||
|
nettle/aes128-set-encrypt-key.c
|
||||||
|
nettle/aes128-set-decrypt-key.c
|
||||||
|
nettle/aes128-meta.c
|
||||||
|
nettle/aes192-set-encrypt-key.c
|
||||||
|
nettle/aes192-set-decrypt-key.c
|
||||||
|
nettle/aes192-meta.c
|
||||||
|
nettle/aes256-set-encrypt-key.c
|
||||||
|
nettle/aes256-set-decrypt-key.c
|
||||||
|
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
|
||||||
|
nettle/blowfish-bcrypt.c
|
||||||
|
nettle/base16-encode.c
|
||||||
|
nettle/base16-decode.c
|
||||||
|
nettle/base16-meta.c
|
||||||
|
nettle/base64-encode.c
|
||||||
|
nettle/base64-decode.c
|
||||||
|
nettle/base64-meta.c
|
||||||
|
nettle/base64url-encode.c
|
||||||
|
nettle/base64url-decode.c
|
||||||
|
nettle/base64url-meta.c
|
||||||
|
nettle/buffer.c
|
||||||
|
nettle/buffer-init.c
|
||||||
|
nettle/camellia-crypt-internal.c
|
||||||
|
nettle/camellia-table.c
|
||||||
|
nettle/camellia-absorb.c
|
||||||
|
nettle/camellia-invert-key.c
|
||||||
|
nettle/camellia128-set-encrypt-key.c
|
||||||
|
nettle/camellia128-crypt.c
|
||||||
|
nettle/camellia128-set-decrypt-key.c
|
||||||
|
nettle/camellia128-meta.c
|
||||||
|
nettle/camellia192-meta.c
|
||||||
|
nettle/camellia256-set-encrypt-key.c
|
||||||
|
nettle/camellia256-crypt.c
|
||||||
|
nettle/camellia256-set-decrypt-key.c
|
||||||
|
nettle/camellia256-meta.c
|
||||||
|
nettle/cast128.c
|
||||||
|
nettle/cast128-meta.c
|
||||||
|
nettle/cbc.c
|
||||||
|
nettle/cbc-aes128-encrypt.c
|
||||||
|
nettle/cbc-aes192-encrypt.c
|
||||||
|
nettle/cbc-aes256-encrypt.c
|
||||||
|
nettle/ccm.c
|
||||||
|
nettle/ccm-aes128.c
|
||||||
|
nettle/ccm-aes192.c
|
||||||
|
nettle/ccm-aes256.c
|
||||||
|
nettle/cfb.c
|
||||||
|
nettle/siv-cmac.c
|
||||||
|
nettle/siv-cmac-aes128.c
|
||||||
|
nettle/siv-cmac-aes256.c
|
||||||
|
nettle/cnd-memcpy.c
|
||||||
|
nettle/chacha-crypt.c
|
||||||
|
nettle/chacha-core-internal.c
|
||||||
|
nettle/chacha-poly1305.c
|
||||||
|
nettle/chacha-poly1305-meta.c
|
||||||
|
nettle/chacha-set-key.c
|
||||||
|
nettle/chacha-set-nonce.c
|
||||||
|
nettle/ctr.c
|
||||||
|
nettle/ctr16.c
|
||||||
|
nettle/des.c
|
||||||
|
nettle/des3.c
|
||||||
|
nettle/eax.c
|
||||||
|
nettle/eax-aes128.c
|
||||||
|
nettle/eax-aes128-meta.c
|
||||||
|
nettle/ghash-set-key.c
|
||||||
|
nettle/ghash-update.c
|
||||||
|
nettle/gcm.c
|
||||||
|
nettle/gcm-aes.c
|
||||||
|
nettle/gcm-aes128.c
|
||||||
|
nettle/gcm-aes128-meta.c
|
||||||
|
nettle/gcm-aes192.c
|
||||||
|
nettle/gcm-aes192-meta.c
|
||||||
|
nettle/gcm-aes256.c
|
||||||
|
nettle/gcm-aes256-meta.c
|
||||||
|
nettle/gcm-camellia128.c
|
||||||
|
nettle/gcm-camellia128-meta.c
|
||||||
|
nettle/gcm-camellia256.c
|
||||||
|
nettle/gcm-camellia256-meta.c
|
||||||
|
nettle/cmac.c
|
||||||
|
nettle/cmac64.c
|
||||||
|
nettle/cmac-aes128.c
|
||||||
|
nettle/cmac-aes256.c
|
||||||
|
nettle/cmac-des3.c
|
||||||
|
nettle/cmac-aes128-meta.c
|
||||||
|
nettle/cmac-aes256-meta.c
|
||||||
|
nettle/cmac-des3-meta.c
|
||||||
|
nettle/gost28147.c
|
||||||
|
nettle/gosthash94.c
|
||||||
|
nettle/gosthash94-meta.c
|
||||||
|
nettle/hmac.c
|
||||||
|
nettle/hmac-gosthash94.c
|
||||||
|
nettle/hmac-md5.c
|
||||||
|
nettle/hmac-ripemd160.c
|
||||||
|
nettle/hmac-sha1.c
|
||||||
|
nettle/hmac-sha224.c
|
||||||
|
nettle/hmac-sha256.c
|
||||||
|
nettle/hmac-sha384.c
|
||||||
|
nettle/hmac-sha512.c
|
||||||
|
nettle/hmac-streebog.c
|
||||||
|
nettle/hmac-sm3.c
|
||||||
|
nettle/hmac-md5-meta.c
|
||||||
|
nettle/hmac-ripemd160-meta.c
|
||||||
|
nettle/hmac-sha1-meta.c
|
||||||
|
nettle/hmac-sha224-meta.c
|
||||||
|
nettle/hmac-sha256-meta.c
|
||||||
|
nettle/hmac-sha384-meta.c
|
||||||
|
nettle/hmac-sha512-meta.c
|
||||||
|
nettle/hmac-streebog-meta.c
|
||||||
|
nettle/hmac-sm3-meta.c
|
||||||
|
nettle/knuth-lfib.c
|
||||||
|
nettle/hkdf.c
|
||||||
|
nettle/md2.c
|
||||||
|
nettle/md2-meta.c
|
||||||
|
nettle/md4.c
|
||||||
|
nettle/md4-meta.c
|
||||||
|
nettle/md5.c
|
||||||
|
nettle/md5-compress.c
|
||||||
|
nettle/md5-compat.c
|
||||||
|
nettle/md5-meta.c
|
||||||
|
nettle/memeql-sec.c
|
||||||
|
nettle/memxor.c
|
||||||
|
nettle/memxor3.c
|
||||||
|
nettle/nettle-lookup-hash.c
|
||||||
|
nettle/nettle-meta-aeads.c
|
||||||
|
nettle/nettle-meta-armors.c
|
||||||
|
nettle/nettle-meta-ciphers.c
|
||||||
|
nettle/nettle-meta-hashes.c
|
||||||
|
nettle/nettle-meta-macs.c
|
||||||
|
nettle/pbkdf2.c
|
||||||
|
nettle/pbkdf2-hmac-gosthash94.c
|
||||||
|
nettle/pbkdf2-hmac-sha1.c
|
||||||
|
nettle/pbkdf2-hmac-sha256.c
|
||||||
|
nettle/pbkdf2-hmac-sha384.c
|
||||||
|
nettle/pbkdf2-hmac-sha512.c
|
||||||
|
nettle/poly1305-aes.c
|
||||||
|
nettle/poly1305-internal.c
|
||||||
|
nettle/realloc.c
|
||||||
|
nettle/ripemd160.c
|
||||||
|
nettle/ripemd160-compress.c
|
||||||
|
nettle/ripemd160-meta.c
|
||||||
|
nettle/salsa20-core-internal.c
|
||||||
|
nettle/salsa20-crypt-internal.c
|
||||||
|
nettle/salsa20-crypt.c
|
||||||
|
nettle/salsa20r12-crypt.c
|
||||||
|
nettle/salsa20-set-key.c
|
||||||
|
nettle/salsa20-set-nonce.c
|
||||||
|
nettle/salsa20-128-set-key.c
|
||||||
|
nettle/salsa20-256-set-key.c
|
||||||
|
nettle/sha1.c
|
||||||
|
nettle/sha1-compress.c
|
||||||
|
nettle/sha1-meta.c
|
||||||
|
nettle/sha256.c
|
||||||
|
nettle/sha256-compress.c
|
||||||
|
nettle/sha224-meta.c
|
||||||
|
nettle/sha256-meta.c
|
||||||
|
nettle/sha512.c
|
||||||
|
nettle/sha512-compress.c
|
||||||
|
nettle/sha384-meta.c
|
||||||
|
nettle/sha512-meta.c
|
||||||
|
nettle/sha512-224-meta.c
|
||||||
|
nettle/sha512-256-meta.c
|
||||||
|
nettle/sha3.c
|
||||||
|
nettle/sha3-permute.c
|
||||||
|
nettle/sha3-224.c
|
||||||
|
nettle/sha3-224-meta.c
|
||||||
|
nettle/sha3-256.c
|
||||||
|
nettle/sha3-256-meta.c
|
||||||
|
nettle/sha3-384.c
|
||||||
|
nettle/sha3-384-meta.c
|
||||||
|
nettle/sha3-512.c
|
||||||
|
nettle/sha3-512-meta.c
|
||||||
|
nettle/shake256.c
|
||||||
|
nettle/sm3.c
|
||||||
|
nettle/sm3-meta.c
|
||||||
|
nettle/serpent-set-key.c
|
||||||
|
nettle/serpent-encrypt.c
|
||||||
|
nettle/serpent-decrypt.c
|
||||||
|
nettle/serpent-meta.c
|
||||||
|
nettle/streebog.c
|
||||||
|
nettle/streebog-meta.c
|
||||||
|
nettle/twofish.c
|
||||||
|
nettle/twofish-meta.c
|
||||||
|
nettle/umac-nh.c
|
||||||
|
nettle/umac-nh-n.c
|
||||||
|
nettle/umac-l2.c
|
||||||
|
nettle/umac-l3.c
|
||||||
|
nettle/umac-poly64.c
|
||||||
|
nettle/umac-poly128.c
|
||||||
|
nettle/umac-set-key.c
|
||||||
|
nettle/umac32.c
|
||||||
|
nettle/umac64.c
|
||||||
|
nettle/umac96.c
|
||||||
|
nettle/umac128.c
|
||||||
|
nettle/version.c
|
||||||
|
nettle/write-be32.c
|
||||||
|
nettle/write-le32.c
|
||||||
|
nettle/write-le64.c
|
||||||
|
nettle/yarrow256.c
|
||||||
|
nettle/yarrow_key_event.c
|
||||||
|
nettle/xts.c
|
||||||
|
nettle/xts-aes128.c
|
||||||
|
nettle/xts-aes256.c
|
||||||
|
)
|
||||||
|
|
||||||
|
set(LIBNETTLE_INCLUDES
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/nettle
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(nettle STATIC ${LIBNETTLE_SOURCES})
|
||||||
|
target_include_directories(nettle PUBLIC ${LIBNETTLE_INCLUDES})
|
||||||
|
target_compile_definitions(nettle PRIVATE ${LIBNETTLE_DEFINITIONS})
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
/* config.h. Generated from config.h.in by configure. */
|
||||||
|
/* config.h.in. Generated from configure.ac by autoheader. */
|
||||||
|
|
||||||
|
/* Define if building universal (internal helper macro) */
|
||||||
|
/* #undef AC_APPLE_UNIVERSAL_BUILD */
|
||||||
|
|
||||||
|
/* Define to 1 if using 'alloca.c'. */
|
||||||
|
/* #undef C_ALLOCA */
|
||||||
|
|
||||||
|
/* Define to 1 if you have 'alloca', as a function or macro. */
|
||||||
|
#define HAVE_ALLOCA 1
|
||||||
|
|
||||||
|
/* Define to 1 if <alloca.h> works. */
|
||||||
|
#define HAVE_ALLOCA_H 1
|
||||||
|
|
||||||
|
/* Define if __builtin_bswap64 is available */
|
||||||
|
#define HAVE_BUILTIN_BSWAP64 1
|
||||||
|
|
||||||
|
/* Define if clock_gettime is available */
|
||||||
|
#define HAVE_CLOCK_GETTIME 1
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <dlfcn.h> header file. */
|
||||||
|
#define HAVE_DLFCN_H 1
|
||||||
|
|
||||||
|
/* Define if fcntl file locking is available */
|
||||||
|
#define HAVE_FCNTL_LOCKING 1
|
||||||
|
|
||||||
|
/* Define if the compiler understands __attribute__ */
|
||||||
|
#define HAVE_GCC_ATTRIBUTE 1
|
||||||
|
|
||||||
|
/* Define to 1 if you have the `getline' function. */
|
||||||
|
#define HAVE_GETLINE 1
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <inttypes.h> header file. */
|
||||||
|
#define HAVE_INTTYPES_H 1
|
||||||
|
|
||||||
|
/* Define to 1 if you have dlopen (with -ldl). */
|
||||||
|
#define HAVE_LIBDL 1
|
||||||
|
|
||||||
|
/* Define to 1 if you have the `gmp' library (-lgmp). */
|
||||||
|
/* #undef HAVE_LIBGMP */
|
||||||
|
|
||||||
|
/* Define if compiler and linker supports __attribute__ ifunc */
|
||||||
|
#define HAVE_LINK_IFUNC 1
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <malloc.h> header file. */
|
||||||
|
#define HAVE_MALLOC_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 */
|
||||||
|
/* #undef HAVE_NATIVE_aes_decrypt */
|
||||||
|
/* #undef HAVE_NATIVE_aes_encrypt */
|
||||||
|
/* #undef HAVE_NATIVE_aes128_decrypt */
|
||||||
|
/* #undef HAVE_NATIVE_aes128_encrypt */
|
||||||
|
/* #undef HAVE_NATIVE_aes128_invert_key */
|
||||||
|
/* #undef HAVE_NATIVE_aes128_set_decrypt_key */
|
||||||
|
/* #undef HAVE_NATIVE_aes128_set_encrypt_key */
|
||||||
|
/* #undef HAVE_NATIVE_aes192_decrypt */
|
||||||
|
/* #undef HAVE_NATIVE_aes192_encrypt */
|
||||||
|
/* #undef HAVE_NATIVE_aes192_invert_key */
|
||||||
|
/* #undef HAVE_NATIVE_aes192_set_decrypt_key */
|
||||||
|
/* #undef HAVE_NATIVE_aes192_set_encrypt_key */
|
||||||
|
/* #undef HAVE_NATIVE_aes256_decrypt */
|
||||||
|
/* #undef HAVE_NATIVE_aes256_encrypt */
|
||||||
|
/* #undef HAVE_NATIVE_aes256_invert_key */
|
||||||
|
/* #undef HAVE_NATIVE_aes256_set_decrypt_key */
|
||||||
|
/* #undef HAVE_NATIVE_aes256_set_encrypt_key */
|
||||||
|
/* #undef HAVE_NATIVE_cbc_aes128_encrypt */
|
||||||
|
/* #undef HAVE_NATIVE_cbc_aes192_encrypt */
|
||||||
|
/* #undef HAVE_NATIVE_cbc_aes256_encrypt */
|
||||||
|
/* #undef HAVE_NATIVE_chacha_core */
|
||||||
|
/* #undef HAVE_NATIVE_chacha_2core */
|
||||||
|
/* #undef HAVE_NATIVE_chacha_3core */
|
||||||
|
/* #undef HAVE_NATIVE_chacha_4core */
|
||||||
|
/* #undef HAVE_NATIVE_fat_chacha_2core */
|
||||||
|
/* #undef HAVE_NATIVE_fat_chacha_3core */
|
||||||
|
/* #undef HAVE_NATIVE_fat_chacha_4core */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_curve25519_modp */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_curve448_modp */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp192r1_modp */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp192r1_redc */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp224r1_modp */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp224r1_redc */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp256r1_modp */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp256r1_redc */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp384r1_modp */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp384r1_redc */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp521r1_modp */
|
||||||
|
/* #undef HAVE_NATIVE_ecc_secp521r1_redc */
|
||||||
|
/* #undef HAVE_NATIVE_ghash_set_key */
|
||||||
|
/* #undef HAVE_NATIVE_ghash_update */
|
||||||
|
/* #undef HAVE_NATIVE_salsa20_core */
|
||||||
|
/* #undef HAVE_NATIVE_salsa20_2core */
|
||||||
|
/* #undef HAVE_NATIVE_fat_salsa20_2core */
|
||||||
|
/* #undef HAVE_NATIVE_sha1_compress */
|
||||||
|
/* #undef HAVE_NATIVE_sha256_compress */
|
||||||
|
/* #undef HAVE_NATIVE_sha512_compress */
|
||||||
|
/* #undef HAVE_NATIVE_sha3_permute */
|
||||||
|
/* #undef HAVE_NATIVE_umac_nh */
|
||||||
|
/* #undef HAVE_NATIVE_umac_nh_n */
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <openssl/ecdsa.h> header file. */
|
||||||
|
/* #undef HAVE_OPENSSL_ECDSA_H */
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <openssl/evp.h> header file. */
|
||||||
|
/* #undef HAVE_OPENSSL_EVP_H */
|
||||||
|
|
||||||
|
/* Define to 1 if you have the `secure_getenv' function. */
|
||||||
|
/* #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
|
||||||
|
|
||||||
|
/* Define to 1 if you have the `strerror' function. */
|
||||||
|
#define HAVE_STRERROR 1
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <strings.h> header file. */
|
||||||
|
#define HAVE_STRINGS_H 1
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <string.h> header file. */
|
||||||
|
#define HAVE_STRING_H 1
|
||||||
|
|
||||||
|
/* 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
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <unistd.h> header file. */
|
||||||
|
#define HAVE_UNISTD_H 1
|
||||||
|
|
||||||
|
/* Define to 1 if you have the <valgrind/memcheck.h> header file. */
|
||||||
|
/* #undef HAVE_VALGRIND_MEMCHECK_H */
|
||||||
|
|
||||||
|
/* Define to the address where bug reports for this package should be sent. */
|
||||||
|
#define PACKAGE_BUGREPORT "nettle-bugs@lists.lysator.liu.se"
|
||||||
|
|
||||||
|
/* Define to the full name of this package. */
|
||||||
|
#define PACKAGE_NAME "nettle"
|
||||||
|
|
||||||
|
/* Define to the full name and version of this package. */
|
||||||
|
#define PACKAGE_STRING "nettle 3.8.1"
|
||||||
|
|
||||||
|
/* Define to the one symbol short name of this package. */
|
||||||
|
#define PACKAGE_TARNAME "nettle"
|
||||||
|
|
||||||
|
/* Define to the home page for this package. */
|
||||||
|
#define PACKAGE_URL ""
|
||||||
|
|
||||||
|
/* Define to the version of this package. */
|
||||||
|
#define PACKAGE_VERSION "3.8.1"
|
||||||
|
|
||||||
|
/* The size of `long', as computed by sizeof. */
|
||||||
|
#define SIZEOF_LONG __SIZEOF_LONG__
|
||||||
|
|
||||||
|
/* The size of `size_t', as computed by sizeof. */
|
||||||
|
#define SIZEOF_SIZE_T __SIZEOF_SIZE_T__
|
||||||
|
|
||||||
|
/* If using the C implementation of alloca, define if you know the
|
||||||
|
direction of stack growth for your system; otherwise it will be
|
||||||
|
automatically deduced at runtime.
|
||||||
|
STACK_DIRECTION > 0 => grows toward higher addresses
|
||||||
|
STACK_DIRECTION < 0 => grows toward lower addresses
|
||||||
|
STACK_DIRECTION = 0 => direction of growth unknown */
|
||||||
|
/* #undef STACK_DIRECTION */
|
||||||
|
|
||||||
|
/* 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>. This
|
||||||
|
macro is obsolete. */
|
||||||
|
#define TIME_WITH_SYS_TIME 1
|
||||||
|
|
||||||
|
/* Defined if public key features are enabled */
|
||||||
|
/* #undef WITH_HOGWEED */
|
||||||
|
|
||||||
|
/* Define if you have openssl's libcrypto (used for benchmarking) */
|
||||||
|
/* #undef WITH_OPENSSL */
|
||||||
|
|
||||||
|
/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
|
||||||
|
significant byte first (like Motorola and SPARC, unlike Intel). */
|
||||||
|
#if defined AC_APPLE_UNIVERSAL_BUILD
|
||||||
|
# if defined __BIG_ENDIAN__
|
||||||
|
# define WORDS_BIGENDIAN 1
|
||||||
|
# endif
|
||||||
|
#else
|
||||||
|
# ifndef WORDS_BIGENDIAN
|
||||||
|
/* # undef WORDS_BIGENDIAN */
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Define to empty if `const' does not conform to ANSI C. */
|
||||||
|
/* #undef const */
|
||||||
|
|
||||||
|
/* Define to `int' if <sys/types.h> doesn't define. */
|
||||||
|
/* #undef gid_t */
|
||||||
|
|
||||||
|
/* Define to `__inline__' or `__inline' if that's what the C compiler
|
||||||
|
calls it, or to nothing if 'inline' is not supported under any name. */
|
||||||
|
#ifndef __cplusplus
|
||||||
|
/* #undef inline */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Define to `unsigned int' if <sys/types.h> does not define. */
|
||||||
|
/* #undef size_t */
|
||||||
|
|
||||||
|
/* Define to `int' if <sys/types.h> doesn't define. */
|
||||||
|
/* #undef uid_t */
|
||||||
|
|
||||||
|
/* AIX requires this to be the first thing in the file. */
|
||||||
|
#ifndef __GNUC__
|
||||||
|
# if HAVE_ALLOCA_H
|
||||||
|
# include <alloca.h>
|
||||||
|
# else
|
||||||
|
# ifdef _AIX
|
||||||
|
#pragma alloca
|
||||||
|
# else
|
||||||
|
# ifndef alloca /* predefined by HP cc +Olibcalls */
|
||||||
|
char *alloca ();
|
||||||
|
# endif
|
||||||
|
# endif
|
||||||
|
/* Needed for alloca on windows */
|
||||||
|
# if HAVE_MALLOC_H
|
||||||
|
# include <malloc.h>
|
||||||
|
# endif
|
||||||
|
# endif
|
||||||
|
#else /* defined __GNUC__ */
|
||||||
|
# if HAVE_ALLOCA_H
|
||||||
|
# include <alloca.h>
|
||||||
|
# else
|
||||||
|
/* Needed for alloca on windows, also with gcc */
|
||||||
|
# if HAVE_MALLOC_H
|
||||||
|
# include <malloc.h>
|
||||||
|
# endif
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#if HAVE_STRERROR
|
||||||
|
#define STRERROR strerror
|
||||||
|
#else
|
||||||
|
#define STRERROR(x) (sys_errlist[x])
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#if __GNUC__ && HAVE_GCC_ATTRIBUTE
|
||||||
|
# define NORETURN __attribute__ ((__noreturn__))
|
||||||
|
# define PRINTF_STYLE(f, a) __attribute__ ((__format__ (__printf__, f, a)))
|
||||||
|
# define UNUSED __attribute__ ((__unused__))
|
||||||
|
#else
|
||||||
|
# define NORETURN
|
||||||
|
# define PRINTF_STYLE(f, a)
|
||||||
|
# define UNUSED
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#if defined(__x86_64__) || defined(__arch64__)
|
||||||
|
# define HAVE_NATIVE_64_BIT 1
|
||||||
|
#else
|
||||||
|
/* Needs include of <limits.h> before use. */
|
||||||
|
# define HAVE_NATIVE_64_BIT (SIZEOF_LONG * CHAR_BIT >= 64)
|
||||||
|
#endif
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Submodule
+1
Submodule app/src/main/cpp/nettle/nettle added at 990abad16c.
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/* version.h
|
||||||
|
|
||||||
|
Information about library version.
|
||||||
|
|
||||||
|
Copyright (C) 2015 Red Hat, Inc.
|
||||||
|
Copyright (C) 2015 Niels Möller
|
||||||
|
|
||||||
|
This file is part of GNU Nettle.
|
||||||
|
|
||||||
|
GNU Nettle is free software: you can redistribute it and/or
|
||||||
|
modify it under the terms of either:
|
||||||
|
|
||||||
|
* the GNU Lesser General Public License as published by the Free
|
||||||
|
Software Foundation; either version 3 of the License, or (at your
|
||||||
|
option) any later version.
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
* the GNU General Public License as published by the Free
|
||||||
|
Software Foundation; either version 2 of the License, or (at your
|
||||||
|
option) any later version.
|
||||||
|
|
||||||
|
or both in parallel, as here.
|
||||||
|
|
||||||
|
GNU Nettle is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received copies of the GNU General Public License and
|
||||||
|
the GNU Lesser General Public License along with this program. If
|
||||||
|
not, see http://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef NETTLE_VERSION_H_INCLUDED
|
||||||
|
#define NETTLE_VERSION_H_INCLUDED
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Individual version numbers in decimal */
|
||||||
|
#define NETTLE_VERSION_MAJOR 3
|
||||||
|
#define NETTLE_VERSION_MINOR 8
|
||||||
|
|
||||||
|
#define NETTLE_USE_MINI_GMP 0
|
||||||
|
|
||||||
|
/* We need a preprocessor constant for GMP_NUMB_BITS, simply using
|
||||||
|
sizeof(mp_limb_t) * CHAR_BIT is not good enough. */
|
||||||
|
#if NETTLE_USE_MINI_GMP
|
||||||
|
# define GMP_NUMB_BITS n/a
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int
|
||||||
|
nettle_version_major (void);
|
||||||
|
|
||||||
|
int
|
||||||
|
nettle_version_minor (void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* NETTLE_VERSION_H_INCLUDED */
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package androidx.navigation.ui
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
|
||||||
|
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
|
||||||
|
val builder = NavOptions.Builder().setLaunchSingleTop(true)
|
||||||
|
if (
|
||||||
|
navController.currentDestination!!.parent!!.findNode(item.itemId)
|
||||||
|
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(com.hippo.ehviewer.R.anim.scene_open_enter)
|
||||||
|
.setExitAnim(com.hippo.ehviewer.R.anim.scene_open_exit)
|
||||||
|
.setPopEnterAnim(com.hippo.ehviewer.R.anim.scene_close_enter)
|
||||||
|
.setPopExitAnim(com.hippo.ehviewer.R.anim.scene_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,66 @@
|
|||||||
|
@file:JvmName("GifDecodeUtils")
|
||||||
|
|
||||||
|
package coil.decode
|
||||||
|
|
||||||
|
import kotlin.experimental.and
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.ByteString.Companion.encodeUtf8
|
||||||
|
|
||||||
|
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||||
|
private val GIF_HEADER_87A = "GIF87a".encodeUtf8()
|
||||||
|
private val GIF_HEADER_89A = "GIF89a".encodeUtf8()
|
||||||
|
|
||||||
|
// https://developers.google.com/speed/webp/docs/riff_container
|
||||||
|
private val WEBP_HEADER_RIFF = "RIFF".encodeUtf8()
|
||||||
|
private val WEBP_HEADER_WEBP = "WEBP".encodeUtf8()
|
||||||
|
private val WEBP_HEADER_VPX8 = "VP8X".encodeUtf8()
|
||||||
|
|
||||||
|
// https://nokiatech.github.io/heif/technical.html
|
||||||
|
private val HEIF_HEADER_FTYP = "ftyp".encodeUtf8()
|
||||||
|
private val HEIF_HEADER_MSF1 = "msf1".encodeUtf8()
|
||||||
|
private val HEIF_HEADER_HEVC = "hevc".encodeUtf8()
|
||||||
|
private val HEIF_HEADER_HEVX = "hevx".encodeUtf8()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return 'true' if the [source] contains a GIF image. The [source] is not consumed.
|
||||||
|
*/
|
||||||
|
fun DecodeUtils.isGif(source: BufferedSource): Boolean {
|
||||||
|
return source.rangeEquals(0, GIF_HEADER_89A) ||
|
||||||
|
source.rangeEquals(0, GIF_HEADER_87A)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return 'true' if the [source] contains a WebP image. The [source] is not consumed.
|
||||||
|
*/
|
||||||
|
fun DecodeUtils.isWebP(source: BufferedSource): Boolean {
|
||||||
|
return source.rangeEquals(0, WEBP_HEADER_RIFF) &&
|
||||||
|
source.rangeEquals(8, WEBP_HEADER_WEBP)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return 'true' if the [source] contains an animated WebP image. The [source] is not consumed.
|
||||||
|
*/
|
||||||
|
fun DecodeUtils.isAnimatedWebP(source: BufferedSource): Boolean {
|
||||||
|
return isWebP(source) &&
|
||||||
|
source.rangeEquals(12, WEBP_HEADER_VPX8) &&
|
||||||
|
source.request(17) &&
|
||||||
|
(source.buffer[16] and 0b00000010) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return 'true' if the [source] contains an HEIF image. The [source] is not consumed.
|
||||||
|
*/
|
||||||
|
fun DecodeUtils.isHeif(source: BufferedSource): Boolean {
|
||||||
|
return source.rangeEquals(4, HEIF_HEADER_FTYP)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return 'true' if the [source] contains an animated HEIF image sequence. The [source] is not
|
||||||
|
* consumed.
|
||||||
|
*/
|
||||||
|
fun DecodeUtils.isAnimatedHeif(source: BufferedSource): Boolean {
|
||||||
|
return isHeif(source) &&
|
||||||
|
(source.rangeEquals(8, HEIF_HEADER_MSF1) ||
|
||||||
|
source.rangeEquals(8, HEIF_HEADER_HEVC) ||
|
||||||
|
source.rangeEquals(8, HEIF_HEADER_HEVX))
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
@file:Suppress("SameParameterValue")
|
||||||
|
|
||||||
|
package coil.decode
|
||||||
|
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.ByteString
|
||||||
|
import okio.ByteString.Companion.decodeHex
|
||||||
|
import okio.ForwardingSource
|
||||||
|
import okio.Source
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
|
||||||
|
* below a threshold.
|
||||||
|
*/
|
||||||
|
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
|
||||||
|
|
||||||
|
// An intermediary buffer so we can read and alter the data before it's written to the destination.
|
||||||
|
private val buffer = Buffer()
|
||||||
|
|
||||||
|
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||||
|
// Ensure our buffer has enough bytes to satisfy this read.
|
||||||
|
request(byteCount)
|
||||||
|
|
||||||
|
// Short circuit if there are no bytes in the buffer.
|
||||||
|
if (buffer.size == 0L) {
|
||||||
|
return if (byteCount == 0L) 0L else -1L
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search through the buffer and rewrite any frame delays below the threshold.
|
||||||
|
var bytesWritten = 0L
|
||||||
|
while (true) {
|
||||||
|
val index = indexOf(FRAME_DELAY_START_MARKER)
|
||||||
|
if (index == -1L) break
|
||||||
|
|
||||||
|
// Write up until the end of the frame delay start marker.
|
||||||
|
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
|
||||||
|
|
||||||
|
// Check for the end of the graphics control extension block.
|
||||||
|
if (!request(5) || buffer[4] != 0.toByte()) continue
|
||||||
|
|
||||||
|
// Rewrite the frame delay if it is below the threshold.
|
||||||
|
// The frame delay is stored as two unsigned bytes in reverse order
|
||||||
|
// (i.e. the most significant bits are in the byte that's read second).
|
||||||
|
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
|
||||||
|
if (frameDelay < MINIMUM_FRAME_DELAY) {
|
||||||
|
sink.writeByte(buffer[0].toInt())
|
||||||
|
sink.writeByte(DEFAULT_FRAME_DELAY)
|
||||||
|
sink.writeByte(0)
|
||||||
|
buffer.skip(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write anything left in the source.
|
||||||
|
if (bytesWritten < byteCount) {
|
||||||
|
bytesWritten += write(sink, byteCount - bytesWritten)
|
||||||
|
}
|
||||||
|
return if (bytesWritten == 0L) -1 else bytesWritten
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun indexOf(bytes: ByteString): Long {
|
||||||
|
var index = -1L
|
||||||
|
while (true) {
|
||||||
|
index = buffer.indexOf(bytes[0], index + 1)
|
||||||
|
if (index == -1L) break
|
||||||
|
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun write(sink: Buffer, byteCount: Long): Long {
|
||||||
|
return buffer.read(sink, byteCount).coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun request(byteCount: Long): Boolean {
|
||||||
|
if (buffer.size >= byteCount) return true
|
||||||
|
val toRead = byteCount - buffer.size
|
||||||
|
return super.read(buffer, toRead) == toRead
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
|
||||||
|
// See: "Graphics Control Extension"
|
||||||
|
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
|
||||||
|
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
|
||||||
|
private const val MINIMUM_FRAME_DELAY = 2
|
||||||
|
private const val DEFAULT_FRAME_DELAY = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,53 @@
|
|||||||
|
package com.davemorrissey.labs.subscaleview.decoder
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.graphics.Rect
|
||||||
|
import kotlin.Throws
|
||||||
|
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
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,62 @@
|
|||||||
|
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 @JvmOverloads constructor(
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
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
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Tarsin Norbin
|
||||||
|
*
|
||||||
|
* This file is part of EhViewer
|
||||||
|
*
|
||||||
|
* EhViewer is free software: you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* EhViewer is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.hippo
|
||||||
|
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import java.io.FileDescriptor
|
||||||
|
|
||||||
|
object Native {
|
||||||
|
fun initialize() {
|
||||||
|
System.loadLibrary("ehviewer")
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun getFd(fd: FileDescriptor?): Int
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
external fun sendfile(from: Int, to: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
val FileDescriptor.fd
|
||||||
|
get() = Native.getFd(this)
|
||||||
|
|
||||||
|
infix fun ParcelFileDescriptor.receivefrom(fd: FileDescriptor) {
|
||||||
|
Native.sendfile(fd.fd, getFd())
|
||||||
|
}
|
||||||
|
|
||||||
|
infix fun ParcelFileDescriptor.copyTo(fd: FileDescriptor) {
|
||||||
|
Native.sendfile(getFd(), fd.fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
infix fun ParcelFileDescriptor.copyTo(fd: ParcelFileDescriptor) {
|
||||||
|
Native.sendfile(getFd(), fd.fd)
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Tarsin Norbin
|
||||||
|
*
|
||||||
|
* This file is part of EhViewer
|
||||||
|
*
|
||||||
|
* EhViewer is free software: you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* EhViewer is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.hippo
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import com.hippo.image.Image
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
class UriArchiveAccessor(ctx: Context, private val uri: Uri) {
|
||||||
|
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
|
||||||
|
fun open(): Int {
|
||||||
|
Log.d(DEBUG_TAG, "Open archive $uri")
|
||||||
|
return openArchive(pfd.fd, pfd.statSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun openArchive(fd: Int, size: Long): Int
|
||||||
|
private external fun extractToByteBuffer(index: Int): ByteBuffer?
|
||||||
|
external fun extractToFd(index: Int, fd: Int)
|
||||||
|
external fun getFilename(index: Int): String
|
||||||
|
external fun needPassword(): Boolean
|
||||||
|
external fun providePassword(str: String): Boolean
|
||||||
|
private external fun closeArchive()
|
||||||
|
fun close() {
|
||||||
|
closeArchive()
|
||||||
|
pfd.close()
|
||||||
|
Log.d(DEBUG_TAG, "Close archive $uri successfully!")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getImageSource(index: Int): Image.CloseableSource? {
|
||||||
|
val buffer = extractToByteBuffer(index)
|
||||||
|
buffer ?: return null
|
||||||
|
check(buffer.isDirect)
|
||||||
|
Image.rewriteGifSource(buffer)
|
||||||
|
val source = ImageDecoder.createSource(buffer)
|
||||||
|
return object : Image.CloseableSource {
|
||||||
|
override val source: ImageDecoder.Source
|
||||||
|
get() = source
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
releaseByteBuffer(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
private external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val DEBUG_TAG = "UriArchiveAccessor"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.hippo.app
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
|
||||||
|
open class BaseDialogBuilder constructor(
|
||||||
|
context: Context
|
||||||
|
) : MaterialAlertDialogBuilder(context)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.app
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import com.hippo.ehviewer.R
|
||||||
|
|
||||||
|
class CheckBoxDialogBuilder constructor(
|
||||||
|
context: Context,
|
||||||
|
message: String?,
|
||||||
|
checkText: String?,
|
||||||
|
checked: Boolean
|
||||||
|
) : BaseDialogBuilder(
|
||||||
|
context
|
||||||
|
) {
|
||||||
|
private val mCheckBox: CheckBox
|
||||||
|
val isChecked: Boolean
|
||||||
|
get() = mCheckBox.isChecked
|
||||||
|
|
||||||
|
init {
|
||||||
|
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_checkbox_builder, null)
|
||||||
|
setView(view)
|
||||||
|
setMessage(message)
|
||||||
|
mCheckBox = view.findViewById(R.id.checkbox)
|
||||||
|
mCheckBox.text = checkText
|
||||||
|
mCheckBox.isChecked = checked
|
||||||
|
view.setOnClickListener { mCheckBox.toggle() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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.app
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.TextView.OnEditorActionListener
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.hippo.ehviewer.R
|
||||||
|
|
||||||
|
class EditTextDialogBuilder constructor(
|
||||||
|
context: Context,
|
||||||
|
text: String?,
|
||||||
|
hint: String?
|
||||||
|
) : BaseDialogBuilder(
|
||||||
|
context
|
||||||
|
), OnEditorActionListener {
|
||||||
|
private val mTextInputLayout: TextInputLayout
|
||||||
|
val editText: EditText
|
||||||
|
private var mDialog: AlertDialog? = null
|
||||||
|
val text: String
|
||||||
|
get() = editText.text.toString()
|
||||||
|
|
||||||
|
fun setError(error: CharSequence?) {
|
||||||
|
mTextInputLayout.error = error
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(): AlertDialog {
|
||||||
|
mDialog = super.create()
|
||||||
|
return mDialog as AlertDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEditorAction(v: TextView?, p1: Int, event: KeyEvent?): Boolean {
|
||||||
|
return if (mDialog != null) {
|
||||||
|
val button = mDialog!!.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||||
|
button?.performClick()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittext_builder, null)
|
||||||
|
setView(view)
|
||||||
|
mTextInputLayout = view as TextInputLayout
|
||||||
|
editText = view.findViewById(R.id.edit_text)
|
||||||
|
editText.setText(text)
|
||||||
|
editText.setSelection(editText.text.length)
|
||||||
|
editText.setOnEditorActionListener(this)
|
||||||
|
mTextInputLayout.hint = hint
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.hippo.app
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import com.google.android.material.slider.Slider
|
||||||
|
|
||||||
|
class HapticSlider @JvmOverloads constructor(
|
||||||
|
context: Context, attrs: AttributeSet? = null
|
||||||
|
) : Slider(context, attrs) {
|
||||||
|
init {
|
||||||
|
addOnChangeListener { _, _, _ -> performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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.app
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.ListView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.hippo.ehviewer.R
|
||||||
|
import com.hippo.yorozuya.ViewUtils
|
||||||
|
|
||||||
|
class ListCheckBoxDialogBuilder(
|
||||||
|
context: Context,
|
||||||
|
items: List<CharSequence>,
|
||||||
|
listener: (ListCheckBoxDialogBuilder?, AlertDialog?, Int) -> Unit,
|
||||||
|
checkText: String?,
|
||||||
|
checked: Boolean
|
||||||
|
) : BaseDialogBuilder(
|
||||||
|
context
|
||||||
|
) {
|
||||||
|
private val mCheckBox: CheckBox
|
||||||
|
private var mDialog: AlertDialog? = null
|
||||||
|
val isChecked: Boolean
|
||||||
|
get() = mCheckBox.isChecked
|
||||||
|
|
||||||
|
override fun create(): AlertDialog {
|
||||||
|
mDialog = super.create()
|
||||||
|
return mDialog as AlertDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val view =
|
||||||
|
LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null)
|
||||||
|
setView(view)
|
||||||
|
val listView = ViewUtils.`$$`(view, R.id.list_view) as ListView
|
||||||
|
mCheckBox = ViewUtils.`$$`(view, R.id.checkbox) as CheckBox
|
||||||
|
listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)
|
||||||
|
listView.onItemClickListener =
|
||||||
|
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||||
|
listener(this@ListCheckBoxDialogBuilder, mDialog, position)
|
||||||
|
mDialog?.dismiss()
|
||||||
|
}
|
||||||
|
mCheckBox.text = checkText
|
||||||
|
mCheckBox.isChecked = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.hippo.compat
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.spider.SpiderInfo
|
||||||
|
import com.hippo.yorozuya.IOUtils
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
||||||
|
fun read(): String {
|
||||||
|
return IOUtils.readAsciiLine(inputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readInt(): Int {
|
||||||
|
return read().toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readLong(): Long {
|
||||||
|
return read().toLong()
|
||||||
|
}
|
||||||
|
repeat(2) { read() } // We assert that only info v2
|
||||||
|
val gid = readLong()
|
||||||
|
val token = read()
|
||||||
|
read()
|
||||||
|
val previewPages = readInt()
|
||||||
|
val previewPerPage = readInt()
|
||||||
|
val pages = read().toInt()
|
||||||
|
val pTokenMap = hashMapOf<Int, String>()
|
||||||
|
val info = SpiderInfo(gid, pages, pTokenMap, 0, token, previewPages, previewPerPage)
|
||||||
|
runCatching {
|
||||||
|
val line = read()
|
||||||
|
val pos = line.indexOf(" ")
|
||||||
|
if (pos > 0) {
|
||||||
|
val index = line.substring(0, pos).toInt()
|
||||||
|
val pToken = line.substring(pos + 1)
|
||||||
|
if (pToken.isNotEmpty()) {
|
||||||
|
pTokenMap[index] = pToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 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.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
|
import android.util.SparseArray
|
||||||
|
|
||||||
|
class MSQLiteBuilder {
|
||||||
|
companion object {
|
||||||
|
const val COLUMN_ID = "_id"
|
||||||
|
private val JAVA_TYPE_TO_SQLITE_TYPE: MutableMap<Class<*>?, String> = HashMap()
|
||||||
|
private fun javaTypeToSQLiteType(clazz: Class<*>?): String {
|
||||||
|
return JAVA_TYPE_TO_SQLITE_TYPE[clazz]
|
||||||
|
?: throw IllegalStateException("Unknown type: $clazz")
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
JAVA_TYPE_TO_SQLITE_TYPE[Boolean::class.javaPrimitiveType] =
|
||||||
|
"INTEGER NOT NULL DEFAULT 0"
|
||||||
|
JAVA_TYPE_TO_SQLITE_TYPE[Byte::class.javaPrimitiveType] =
|
||||||
|
"INTEGER NOT NULL DEFAULT 0"
|
||||||
|
JAVA_TYPE_TO_SQLITE_TYPE[Short::class.javaPrimitiveType] =
|
||||||
|
"INTEGER NOT NULL DEFAULT 0"
|
||||||
|
JAVA_TYPE_TO_SQLITE_TYPE[Int::class.javaPrimitiveType] = "INTEGER NOT NULL DEFAULT 0"
|
||||||
|
JAVA_TYPE_TO_SQLITE_TYPE[Long::class.javaPrimitiveType] =
|
||||||
|
"INTEGER NOT NULL DEFAULT 0"
|
||||||
|
JAVA_TYPE_TO_SQLITE_TYPE[Float::class.javaPrimitiveType] =
|
||||||
|
"REAL NOT NULL DEFAULT 0"
|
||||||
|
JAVA_TYPE_TO_SQLITE_TYPE[Double::class.javaPrimitiveType] = "REAL NOT NULL DEFAULT 0"
|
||||||
|
JAVA_TYPE_TO_SQLITE_TYPE[String::class.java] = "TEXT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val statementsMap = SparseArray<List<String>?>()
|
||||||
|
private var version = 0
|
||||||
|
private var statements: MutableList<String>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bump database version.
|
||||||
|
*/
|
||||||
|
fun version(version: Int): MSQLiteBuilder {
|
||||||
|
check(version > this.version) {
|
||||||
|
("New version must be bigger than current version. "
|
||||||
|
+ "current version: " + this.version + ", new version: " + version + ".")
|
||||||
|
}
|
||||||
|
this.version = version
|
||||||
|
statements = ArrayList()
|
||||||
|
statementsMap.put(version, statements)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a table.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Creates a table with int [.COLUMN_ID] primary key.
|
||||||
|
*/
|
||||||
|
@JvmOverloads
|
||||||
|
fun createTable(
|
||||||
|
table: String,
|
||||||
|
column: String = COLUMN_ID,
|
||||||
|
clazz: Class<*>? = Int::class.javaPrimitiveType
|
||||||
|
): MSQLiteBuilder {
|
||||||
|
return statement("CREATE TABLE " + table + " (" + column + " " + javaTypeToSQLiteType(clazz) + " PRIMARY KEY);")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops a table.
|
||||||
|
*/
|
||||||
|
fun dropTable(table: String): MSQLiteBuilder {
|
||||||
|
return statement("DROP TABLE $table;")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a column to the table.
|
||||||
|
*/
|
||||||
|
fun insertColumn(table: String, column: String, clazz: Class<*>?): MSQLiteBuilder {
|
||||||
|
return statement(
|
||||||
|
"ALTER TABLE $table ADD COLUMN $column " + javaTypeToSQLiteType(
|
||||||
|
clazz
|
||||||
|
) + ";"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a statement.
|
||||||
|
*/
|
||||||
|
fun statement(statement: String): MSQLiteBuilder {
|
||||||
|
check(!(version == 0 || statements == null)) { "Call version() first!" }
|
||||||
|
statements!!.add(statement)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a SQLiteOpenHelper from it.
|
||||||
|
*/
|
||||||
|
fun build(context: Context?, name: String?, version: Int): SQLiteOpenHelper {
|
||||||
|
return MSQLiteOpenHelper(context, name, version, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatements(oldVersion: Int, newVersion: Int): List<String> {
|
||||||
|
val result: MutableList<String> = ArrayList()
|
||||||
|
for (i in oldVersion + 1..newVersion) {
|
||||||
|
val list = statementsMap[i]
|
||||||
|
if (list != null) {
|
||||||
|
result.addAll(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 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.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
|
import com.hippo.util.SqlUtils
|
||||||
|
|
||||||
|
internal class MSQLiteOpenHelper(
|
||||||
|
context: Context?,
|
||||||
|
name: String?,
|
||||||
|
private val version: Int,
|
||||||
|
private val builder: MSQLiteBuilder
|
||||||
|
) : SQLiteOpenHelper(context, name, null, version) {
|
||||||
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
|
onUpgrade(db, 0, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
for (command in builder.getStatements(oldVersion, newVersion)) {
|
||||||
|
db.execSQL(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
SqlUtils.dropAllTable(db)
|
||||||
|
onCreate(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 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.drawable;
|
||||||
|
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.R;
|
||||||
|
import com.hippo.yorozuya.MathUtils;
|
||||||
|
|
||||||
|
public class AddDeleteDrawable extends Drawable {
|
||||||
|
|
||||||
|
private final Paint mPaint = new Paint();
|
||||||
|
private final Path mPath = new Path();
|
||||||
|
|
||||||
|
private final int mSize;
|
||||||
|
|
||||||
|
private float mProgress;
|
||||||
|
|
||||||
|
private boolean mAutoUpdateMirror = false;
|
||||||
|
private boolean mVerticalMirror = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context used to get the configuration for the drawable from
|
||||||
|
*/
|
||||||
|
public AddDeleteDrawable(Context context, int color) {
|
||||||
|
Resources resources = context.getResources();
|
||||||
|
|
||||||
|
mSize = resources.getDimensionPixelSize(R.dimen.add_size);
|
||||||
|
float barThickness = Math.round(resources.getDimension(R.dimen.add_thickness));
|
||||||
|
|
||||||
|
mPaint.setColor(color);
|
||||||
|
mPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
mPaint.setStrokeJoin(Paint.Join.MITER);
|
||||||
|
mPaint.setStrokeCap(Paint.Cap.BUTT);
|
||||||
|
mPaint.setStrokeWidth(barThickness);
|
||||||
|
|
||||||
|
float halfSize = mSize / 2;
|
||||||
|
mPath.moveTo(0f, -halfSize);
|
||||||
|
mPath.lineTo(0, halfSize);
|
||||||
|
mPath.moveTo(-halfSize, 0);
|
||||||
|
mPath.lineTo(halfSize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(Canvas canvas) {
|
||||||
|
Rect bounds = getBounds();
|
||||||
|
float canvasRotate;
|
||||||
|
if (mVerticalMirror) {
|
||||||
|
canvasRotate = MathUtils.lerp(270, 135f, mProgress);
|
||||||
|
} else {
|
||||||
|
canvasRotate = MathUtils.lerp(0f, 135f, mProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(bounds.centerX(), bounds.centerY());
|
||||||
|
canvas.rotate(canvasRotate);
|
||||||
|
canvas.drawPath(mPath, mPaint);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColor(int color) {
|
||||||
|
mPaint.setColor(color);
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
mPaint.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(ColorFilter cf) {
|
||||||
|
mPaint.setColorFilter(cf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicHeight() {
|
||||||
|
return mSize * 6 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicWidth() {
|
||||||
|
return mSize * 6 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set, canvas is flipped when progress reached to end and going back to start.
|
||||||
|
*/
|
||||||
|
protected void setVerticalMirror(boolean verticalMirror) {
|
||||||
|
mVerticalMirror = verticalMirror;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutoUpdateMirror(boolean autoUpdateMirror) {
|
||||||
|
mAutoUpdateMirror = autoUpdateMirror;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public float getProgress() {
|
||||||
|
return mProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public void setProgress(float progress) {
|
||||||
|
if (mAutoUpdateMirror) {
|
||||||
|
if (progress == 1f) {
|
||||||
|
setVerticalMirror(true);
|
||||||
|
} else if (progress == 0f) {
|
||||||
|
setVerticalMirror(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mProgress = progress;
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdd(long duration) {
|
||||||
|
setShape(false, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDelete(long duration) {
|
||||||
|
setShape(true, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShape(boolean delete, long duration) {
|
||||||
|
if (!((!delete && mProgress == 0f) || (delete && mProgress == 1f))) {
|
||||||
|
float endProgress = delete ? 1f : 0f;
|
||||||
|
if (duration <= 0) {
|
||||||
|
setProgress(endProgress);
|
||||||
|
} else {
|
||||||
|
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", endProgress);
|
||||||
|
oa.setDuration(duration);
|
||||||
|
oa.setAutoCancel(true);
|
||||||
|
oa.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
return PixelFormat.TRANSLUCENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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.drawable
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.DrawableWrapper
|
||||||
|
import androidx.core.graphics.withClip
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a part of the original drawable
|
||||||
|
*/
|
||||||
|
class PreciselyClipDrawable(
|
||||||
|
drawable: Drawable,
|
||||||
|
offsetX: Int,
|
||||||
|
offsetY: Int,
|
||||||
|
width: Int,
|
||||||
|
height: Int
|
||||||
|
) : DrawableWrapper(drawable) {
|
||||||
|
private val mScale: RectF
|
||||||
|
private val mTemp = Rect()
|
||||||
|
|
||||||
|
init {
|
||||||
|
val originWidth = drawable.intrinsicWidth.toFloat()
|
||||||
|
val originHeight = drawable.intrinsicHeight.toFloat()
|
||||||
|
mScale = RectF(
|
||||||
|
(offsetX / originWidth).coerceIn(0.0f, 1.0f),
|
||||||
|
(offsetY / originHeight).coerceIn(0.0f, 1.0f),
|
||||||
|
((offsetX + width) / originWidth).coerceIn(0.0f, 1.0f),
|
||||||
|
((offsetY + height) / originHeight).coerceIn(0.0f, 1.0f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBoundsChange(bounds: Rect) {
|
||||||
|
mTemp.left = ((mScale.left * bounds.right - mScale.right * bounds.left) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
|
||||||
|
mTemp.right = (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
|
||||||
|
mTemp.top = ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
|
||||||
|
mTemp.bottom = (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
|
||||||
|
super.onBoundsChange(mTemp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntrinsicWidth(): Int {
|
||||||
|
return (super.getIntrinsicWidth() * mScale.width()).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntrinsicHeight(): Int {
|
||||||
|
return (super.getIntrinsicHeight() * mScale.height()).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
canvas.withClip(bounds) { super.draw(canvas) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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.drawable;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
public class TriangleDrawable extends Drawable {
|
||||||
|
|
||||||
|
private final Paint mPaint;
|
||||||
|
private final Path mPath;
|
||||||
|
|
||||||
|
public TriangleDrawable(int color) {
|
||||||
|
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
|
||||||
|
mPaint.setColor(color);
|
||||||
|
mPath = new Path();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColor(int color) {
|
||||||
|
mPaint.setColor(color);
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBoundsChange(Rect bounds) {
|
||||||
|
super.onBoundsChange(bounds);
|
||||||
|
mPath.reset();
|
||||||
|
mPath.moveTo(bounds.left, bounds.top);
|
||||||
|
mPath.lineTo(bounds.right, bounds.top);
|
||||||
|
mPath.lineTo(bounds.right, bounds.bottom);
|
||||||
|
mPath.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(Canvas canvas) {
|
||||||
|
canvas.drawPath(mPath, mPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
mPaint.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(ColorFilter colorFilter) {
|
||||||
|
mPaint.setColorFilter(colorFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
return PixelFormat.OPAQUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 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.drawable;
|
||||||
|
|
||||||
|
import android.graphics.drawable.AnimatedImageDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import com.hippo.widget.ObservedTextView;
|
||||||
|
|
||||||
|
import coil.Coil;
|
||||||
|
import coil.request.Disposable;
|
||||||
|
import coil.request.ImageRequest;
|
||||||
|
|
||||||
|
public class UnikeryDrawable extends WrapDrawable implements ObservedTextView.OnWindowAttachListener {
|
||||||
|
|
||||||
|
private static final String TAG = UnikeryDrawable.class.getSimpleName();
|
||||||
|
private final ObservedTextView mTextView;
|
||||||
|
private String mUrl;
|
||||||
|
private Disposable task;
|
||||||
|
|
||||||
|
public UnikeryDrawable(ObservedTextView textView) {
|
||||||
|
mTextView = textView;
|
||||||
|
mTextView.setOnWindowAttachListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttachedToWindow() {
|
||||||
|
load(mUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetachedFromWindow() {
|
||||||
|
if (task != null && !task.isDisposed())
|
||||||
|
task.dispose();
|
||||||
|
clearDrawable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load(String url) {
|
||||||
|
if (url != null) {
|
||||||
|
mUrl = url;
|
||||||
|
var imageLoader = Coil.imageLoader(mTextView.getContext());
|
||||||
|
var request = new ImageRequest.Builder(mTextView.getContext()).data(url).memoryCacheKey(url).diskCacheKey(url).target(
|
||||||
|
drawable -> null,
|
||||||
|
drawable -> null,
|
||||||
|
drawable -> {
|
||||||
|
onGetValue(drawable);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
).build();
|
||||||
|
task = imageLoader.enqueue(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearDrawable() {
|
||||||
|
setDrawable(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDrawable(Drawable drawable) {
|
||||||
|
// Remove old callback
|
||||||
|
Drawable oldDrawable = getDrawable();
|
||||||
|
if (oldDrawable != null) {
|
||||||
|
oldDrawable.setCallback(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.setDrawable(drawable);
|
||||||
|
|
||||||
|
if (drawable != null) {
|
||||||
|
drawable.setCallback(mTextView);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBounds();
|
||||||
|
if (drawable != null) {
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateSelf() {
|
||||||
|
CharSequence cs = mTextView.getText();
|
||||||
|
mTextView.setText(cs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onGetValue(Drawable drawable) {
|
||||||
|
clearDrawable();
|
||||||
|
setDrawable(drawable);
|
||||||
|
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable)
|
||||||
|
animatedImageDrawable.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 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.drawable;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
public class WrapDrawable extends Drawable {
|
||||||
|
|
||||||
|
private Drawable mDrawable;
|
||||||
|
|
||||||
|
public Drawable getDrawable() {
|
||||||
|
return mDrawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDrawable(Drawable drawable) {
|
||||||
|
mDrawable = drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateBounds() {
|
||||||
|
setBounds(0, 0, getIntrinsicWidth(), getIntrinsicHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(Canvas canvas) {
|
||||||
|
if (mDrawable != null) {
|
||||||
|
mDrawable.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBounds(int left, int top, int right, int bottom) {
|
||||||
|
super.setBounds(left, top, right, bottom);
|
||||||
|
|
||||||
|
if (mDrawable != null) {
|
||||||
|
mDrawable.setBounds(left, top, right, bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBounds(Rect bounds) {
|
||||||
|
super.setBounds(bounds);
|
||||||
|
|
||||||
|
if (mDrawable != null) {
|
||||||
|
mDrawable.setBounds(bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getChangingConfigurations() {
|
||||||
|
if (mDrawable != null) {
|
||||||
|
return mDrawable.getChangingConfigurations();
|
||||||
|
} else {
|
||||||
|
return super.getChangingConfigurations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setChangingConfigurations(int configs) {
|
||||||
|
super.setChangingConfigurations(configs);
|
||||||
|
|
||||||
|
if (mDrawable != null) {
|
||||||
|
mDrawable.setChangingConfigurations(configs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDither(boolean dither) {
|
||||||
|
super.setDither(dither);
|
||||||
|
|
||||||
|
if (mDrawable != null) {
|
||||||
|
mDrawable.setDither(dither);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFilterBitmap(boolean filter) {
|
||||||
|
super.setFilterBitmap(filter);
|
||||||
|
|
||||||
|
if (mDrawable != null) {
|
||||||
|
mDrawable.setFilterBitmap(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
if (mDrawable != null) {
|
||||||
|
mDrawable.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(ColorFilter cf) {
|
||||||
|
if (mDrawable != null) {
|
||||||
|
mDrawable.setColorFilter(cf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
if (mDrawable != null) {
|
||||||
|
return mDrawable.getOpacity();
|
||||||
|
} else {
|
||||||
|
return PixelFormat.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicWidth() {
|
||||||
|
if (mDrawable != null) {
|
||||||
|
return mDrawable.getIntrinsicWidth();
|
||||||
|
} else {
|
||||||
|
return super.getIntrinsicWidth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicHeight() {
|
||||||
|
if (mDrawable != null) {
|
||||||
|
return mDrawable.getIntrinsicHeight();
|
||||||
|
} else {
|
||||||
|
return super.getIntrinsicHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,675 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 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.easyrecyclerview;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.SparseBooleanArray;
|
||||||
|
import android.view.ActionMode;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Checkable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.collection.LongSparseArray;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.hippo.yorozuya.NumberUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add setChoiceMode for RecyclerView
|
||||||
|
*/
|
||||||
|
// Get some code from twoway-view and AbsListView.
|
||||||
|
public class EasyRecyclerView extends RecyclerView {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normal list that does not indicate choices
|
||||||
|
*/
|
||||||
|
public static final int CHOICE_MODE_NONE = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list allows up to one choice
|
||||||
|
*/
|
||||||
|
public static final int CHOICE_MODE_SINGLE = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list allows multiple choices
|
||||||
|
*/
|
||||||
|
public static final int CHOICE_MODE_MULTIPLE = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list allows multiple choices in a modal selection mode
|
||||||
|
*/
|
||||||
|
public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list allows multiple choices in custom action
|
||||||
|
*/
|
||||||
|
public static final int CHOICE_MODE_MULTIPLE_CUSTOM = 4;
|
||||||
|
// Fling friction
|
||||||
|
public float pageScrollThreshold;
|
||||||
|
/**
|
||||||
|
* Wrapper for the multiple choice mode callback; AbsListView needs to perform
|
||||||
|
* a few extra actions around what application code does.
|
||||||
|
*/
|
||||||
|
MultiChoiceModeWrapper mMultiChoiceModeCallback;
|
||||||
|
/**
|
||||||
|
* Controls if/how the user may choose/check items in the list
|
||||||
|
*/
|
||||||
|
private int mChoiceMode = CHOICE_MODE_NONE;
|
||||||
|
/**
|
||||||
|
* Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive.
|
||||||
|
*/
|
||||||
|
private ActionMode mChoiceActionMode;
|
||||||
|
/**
|
||||||
|
* Listener for custom multiple choices
|
||||||
|
*/
|
||||||
|
private CustomChoiceListener mCustomChoiceListener;
|
||||||
|
|
||||||
|
private boolean mCustomChoice = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lock, avoid OutOfCustomChoiceMode when doing OutOfCustomChoiceMode
|
||||||
|
*/
|
||||||
|
private boolean mOutOfCustomChoiceModing = false;
|
||||||
|
|
||||||
|
private SparseBooleanArray mTempCheckStates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Running count of how many items are currently checked
|
||||||
|
*/
|
||||||
|
private int mCheckedItemCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Running state of which positions are currently checked
|
||||||
|
*/
|
||||||
|
private SparseBooleanArray mCheckStates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Running state of which IDs are currently checked.
|
||||||
|
* If there is a value for a given key, the checked state for that ID is true
|
||||||
|
* and the value holds the last known position in the adapter for that id.
|
||||||
|
*/
|
||||||
|
private LongSparseArray<Integer> mCheckedIdStates;
|
||||||
|
|
||||||
|
private Adapter<?> mAdapter;
|
||||||
|
|
||||||
|
public EasyRecyclerView(Context context) {
|
||||||
|
super(context);
|
||||||
|
EasyRecyclerView.this.init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EasyRecyclerView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
EasyRecyclerView.this.init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EasyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
EasyRecyclerView.this.init(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setViewChecked(View view, boolean checked) {
|
||||||
|
if (view instanceof Checkable) {
|
||||||
|
((Checkable) view).setChecked(checked);
|
||||||
|
} else {
|
||||||
|
view.setActivated(checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setAdapter(RecyclerView.Adapter adapter) {
|
||||||
|
|
||||||
|
super.setAdapter(adapter);
|
||||||
|
|
||||||
|
mAdapter = adapter;
|
||||||
|
if (adapter != null) {
|
||||||
|
if (mChoiceMode != CHOICE_MODE_NONE && adapter.hasStableIds() &&
|
||||||
|
mCheckedIdStates == null) {
|
||||||
|
mCheckedIdStates = new LongSparseArray<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCheckStates != null) {
|
||||||
|
mCheckStates.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCheckedIdStates != null) {
|
||||||
|
mCheckedIdStates.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChildAttachedToWindow(@NonNull View child) {
|
||||||
|
super.onChildAttachedToWindow(child);
|
||||||
|
|
||||||
|
if (mCheckStates != null) {
|
||||||
|
int position = getChildAdapterPosition(child);
|
||||||
|
if (position >= 0) {
|
||||||
|
setViewChecked(child, mCheckStates.get(position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int getCheckedItemCount() {
|
||||||
|
return mCheckedItemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the checked state of the specified position. The result is only
|
||||||
|
* valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE}
|
||||||
|
* or {@link #CHOICE_MODE_MULTIPLE}.
|
||||||
|
*
|
||||||
|
* @param position The item whose checked state to return
|
||||||
|
* @return The item's checked state or <code>false</code> if choice mode
|
||||||
|
* is invalid
|
||||||
|
* @see #setChoiceMode(int)
|
||||||
|
*/
|
||||||
|
public boolean isItemChecked(int position) {
|
||||||
|
//noinspection SimplifiableIfStatement
|
||||||
|
if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
|
||||||
|
return mCheckStates.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the set of checked items in the list. The result is only valid if
|
||||||
|
* the choice mode has not been set to {@link #CHOICE_MODE_NONE}.
|
||||||
|
*
|
||||||
|
* @return A SparseBooleanArray which will return true for each call to
|
||||||
|
* get(int position) where position is a checked position in the
|
||||||
|
* list and false otherwise, or <code>null</code> if the choice
|
||||||
|
* mode is set to {@link #CHOICE_MODE_NONE}.
|
||||||
|
*/
|
||||||
|
public SparseBooleanArray getCheckedItemPositions() {
|
||||||
|
if (mChoiceMode != CHOICE_MODE_NONE) {
|
||||||
|
return mCheckStates;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInCustomChoice() {
|
||||||
|
return mCustomChoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void intoCustomChoiceMode() {
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && !mCustomChoice) {
|
||||||
|
mCustomChoice = true;
|
||||||
|
|
||||||
|
mCustomChoiceListener.onIntoCustomChoice(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void outOfCustomChoiceMode() {
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && mCustomChoice && !mOutOfCustomChoiceModing) {
|
||||||
|
mOutOfCustomChoiceModing = true;
|
||||||
|
|
||||||
|
// Copy mCheckStates
|
||||||
|
mTempCheckStates.clear();
|
||||||
|
for (int i = 0, n = mCheckStates.size(); i < n; i++) {
|
||||||
|
mTempCheckStates.put(mCheckStates.keyAt(i), mCheckStates.valueAt(i));
|
||||||
|
}
|
||||||
|
// Uncheck remain checked items
|
||||||
|
for (int i = 0, n = mTempCheckStates.size(); i < n; i++) {
|
||||||
|
if (mTempCheckStates.valueAt(i)) {
|
||||||
|
setItemChecked(mTempCheckStates.keyAt(i), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mCustomChoice = false;
|
||||||
|
mCustomChoiceListener.onOutOfCustomChoice(this);
|
||||||
|
|
||||||
|
mOutOfCustomChoiceModing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear any choices previously set
|
||||||
|
*/
|
||||||
|
private void clearChoices() {
|
||||||
|
if (mCheckStates != null) {
|
||||||
|
mCheckStates.clear();
|
||||||
|
}
|
||||||
|
if (mCheckedIdStates != null) {
|
||||||
|
mCheckedIdStates.clear();
|
||||||
|
}
|
||||||
|
mCheckedItemCount = 0;
|
||||||
|
updateOnScreenCheckedViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all items checked.
|
||||||
|
*/
|
||||||
|
public void checkAll() {
|
||||||
|
if (mChoiceMode == CHOICE_MODE_NONE || mChoiceMode == CHOICE_MODE_SINGLE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check is intoCheckMode
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && !mCustomChoice) {
|
||||||
|
throw new IllegalStateException("Call intoCheckMode first");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start selection mode if needed. We don't need to if we're unchecking something.
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
|
||||||
|
if (mMultiChoiceModeCallback == null ||
|
||||||
|
!mMultiChoiceModeCallback.hasWrappedCallback()) {
|
||||||
|
throw new IllegalStateException("EasyRecyclerView: attempted to start selection mode " +
|
||||||
|
"for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " +
|
||||||
|
"supplied. Call setMultiChoiceModeListener to set a callback.");
|
||||||
|
}
|
||||||
|
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0, n = mAdapter.getItemCount(); i < n; i++) {
|
||||||
|
boolean oldValue = mCheckStates.get(i);
|
||||||
|
mCheckStates.put(i, true);
|
||||||
|
if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
|
||||||
|
mCheckedIdStates.put(mAdapter.getItemId(i), i);
|
||||||
|
}
|
||||||
|
if (!oldValue) {
|
||||||
|
mCheckedItemCount++;
|
||||||
|
}
|
||||||
|
if (mChoiceActionMode != null) {
|
||||||
|
final long id = mAdapter.getItemId(i);
|
||||||
|
mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, i, id, true);
|
||||||
|
}
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {
|
||||||
|
final long id = mAdapter.getItemId(i);
|
||||||
|
mCustomChoiceListener.onItemCheckedStateChanged(this, i, id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOnScreenCheckedViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleItemChecked(int position) {
|
||||||
|
if (mCheckStates != null) {
|
||||||
|
setItemChecked(position, !mCheckStates.get(position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the checked state of the specified position. The is only valid if
|
||||||
|
* the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or
|
||||||
|
* {@link #CHOICE_MODE_MULTIPLE}.
|
||||||
|
*
|
||||||
|
* @param position The item whose checked state is to be checked
|
||||||
|
* @param value The new checked state for the item
|
||||||
|
*/
|
||||||
|
public void setItemChecked(int position, boolean value) {
|
||||||
|
if (mChoiceMode == CHOICE_MODE_NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check is intoCheckMode
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && !mCustomChoice) {
|
||||||
|
throw new IllegalStateException("Call intoCheckMode first");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start selection mode if needed. We don't need to if we're unchecking something.
|
||||||
|
if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
|
||||||
|
if (mMultiChoiceModeCallback == null ||
|
||||||
|
!mMultiChoiceModeCallback.hasWrappedCallback()) {
|
||||||
|
throw new IllegalStateException("EasyRecyclerView: attempted to start selection mode " +
|
||||||
|
"for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " +
|
||||||
|
"supplied. Call setMultiChoiceModeListener to set a callback.");
|
||||||
|
}
|
||||||
|
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL || mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {
|
||||||
|
boolean oldValue = mCheckStates.get(position);
|
||||||
|
mCheckStates.put(position, value);
|
||||||
|
if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
|
||||||
|
if (value) {
|
||||||
|
mCheckedIdStates.put(mAdapter.getItemId(position), position);
|
||||||
|
} else {
|
||||||
|
mCheckedIdStates.remove(mAdapter.getItemId(position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldValue != value) {
|
||||||
|
if (value) {
|
||||||
|
mCheckedItemCount++;
|
||||||
|
} else {
|
||||||
|
mCheckedItemCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mChoiceActionMode != null) {
|
||||||
|
final long id = mAdapter.getItemId(position);
|
||||||
|
mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
|
||||||
|
position, id, value);
|
||||||
|
}
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {
|
||||||
|
final long id = mAdapter.getItemId(position);
|
||||||
|
mCustomChoiceListener.onItemCheckedStateChanged(this, position, id, value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds();
|
||||||
|
// Clear all values if we're checking something, or unchecking the currently
|
||||||
|
// selected item
|
||||||
|
if (value || isItemChecked(position)) {
|
||||||
|
mCheckStates.clear();
|
||||||
|
if (updateIds) {
|
||||||
|
mCheckedIdStates.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// this may end up selecting the value we just cleared but this way
|
||||||
|
// we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
|
||||||
|
if (value) {
|
||||||
|
mCheckStates.put(position, true);
|
||||||
|
if (updateIds) {
|
||||||
|
mCheckedIdStates.put(mAdapter.getItemId(position), position);
|
||||||
|
}
|
||||||
|
mCheckedItemCount = 1;
|
||||||
|
} else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
|
||||||
|
mCheckedItemCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOnScreenCheckedViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the choice behavior for the List. By default, Lists do not have any choice behavior
|
||||||
|
* ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the
|
||||||
|
* List allows up to one item to be in a chosen state. By setting the choiceMode to
|
||||||
|
* {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen.
|
||||||
|
*
|
||||||
|
* @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or
|
||||||
|
* {@link #CHOICE_MODE_MULTIPLE}
|
||||||
|
*/
|
||||||
|
public void setChoiceMode(int choiceMode) {
|
||||||
|
mChoiceMode = choiceMode;
|
||||||
|
if (mChoiceActionMode != null) {
|
||||||
|
mChoiceActionMode.finish();
|
||||||
|
mChoiceActionMode = null;
|
||||||
|
}
|
||||||
|
if (mChoiceMode != CHOICE_MODE_NONE) {
|
||||||
|
if (mCheckStates == null) {
|
||||||
|
mCheckStates = new SparseBooleanArray(0);
|
||||||
|
}
|
||||||
|
if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
|
||||||
|
mCheckedIdStates = new LongSparseArray<>(0);
|
||||||
|
}
|
||||||
|
// Modal multi-choice mode only has choices when the mode is active. Clear them.
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
|
||||||
|
clearChoices();
|
||||||
|
setLongClickable(true);
|
||||||
|
} else if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {
|
||||||
|
if (mTempCheckStates == null) {
|
||||||
|
mTempCheckStates = new SparseBooleanArray(0);
|
||||||
|
}
|
||||||
|
clearChoices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomCheckedListener(CustomChoiceListener listener) {
|
||||||
|
mCustomChoiceListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a quick, in-place update of the checked or activated state
|
||||||
|
* on all visible item views. This should only be called when a valid
|
||||||
|
* choice mode is active.
|
||||||
|
*/
|
||||||
|
private void updateOnScreenCheckedViews() {
|
||||||
|
final int count = getChildCount();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
final View child = getChildAt(i);
|
||||||
|
final int position = getChildAdapterPosition(child);
|
||||||
|
setViewChecked(child, mCheckStates.get(position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context) {
|
||||||
|
float scale = context.getResources().getDisplayMetrics().density;
|
||||||
|
// g (m/s^2)
|
||||||
|
// inch/meter
|
||||||
|
// A context-specific coefficient adjusted to physical values.
|
||||||
|
// look and feel tuning
|
||||||
|
pageScrollThreshold = scale * 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Parcelable onSaveInstanceState() {
|
||||||
|
final SavedState ss = new SavedState(super.onSaveInstanceState());
|
||||||
|
|
||||||
|
ss.choiceMode = mChoiceMode;
|
||||||
|
ss.customChoice = mCustomChoice;
|
||||||
|
ss.checkedItemCount = mCheckedItemCount;
|
||||||
|
ss.checkState = mCheckStates;
|
||||||
|
ss.checkIdState = mCheckedIdStates;
|
||||||
|
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRestoreInstanceState(Parcelable state) {
|
||||||
|
SavedState ss = (SavedState) state;
|
||||||
|
super.onRestoreInstanceState(ss.getSuperState());
|
||||||
|
|
||||||
|
setChoiceMode(ss.choiceMode);
|
||||||
|
mCustomChoice = ss.customChoice;
|
||||||
|
mCheckedItemCount = ss.checkedItemCount;
|
||||||
|
if (ss.checkState != null) {
|
||||||
|
mCheckStates = ss.checkState;
|
||||||
|
}
|
||||||
|
if (ss.checkIdState != null) {
|
||||||
|
mCheckedIdStates = ss.checkIdState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mCheckedItemCount > 0) {
|
||||||
|
mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
|
||||||
|
}
|
||||||
|
updateOnScreenCheckedViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A MultiChoiceModeListener receives events for {@link android.widget.AbsListView#CHOICE_MODE_MULTIPLE_MODAL}.
|
||||||
|
* It acts as the {@link android.view.ActionMode.Callback} for the selection mode and also receives
|
||||||
|
* {@link #onItemCheckedStateChanged(android.view.ActionMode, int, long, boolean)} events when the user
|
||||||
|
* selects and deselects list items.
|
||||||
|
*/
|
||||||
|
public interface MultiChoiceModeListener extends ActionMode.Callback {
|
||||||
|
/**
|
||||||
|
* Called when an item is checked or unchecked during selection mode.
|
||||||
|
*
|
||||||
|
* @param mode The {@link android.view.ActionMode} providing the selection mode
|
||||||
|
* @param position Adapter position of the item that was checked or unchecked
|
||||||
|
* @param id Adapter ID of the item that was checked or unchecked
|
||||||
|
* @param checked <code>true</code> if the item is now checked, <code>false</code>
|
||||||
|
* if the item is now unchecked.
|
||||||
|
*/
|
||||||
|
void onItemCheckedStateChanged(ActionMode mode,
|
||||||
|
int position, long id, boolean checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom checked
|
||||||
|
*/
|
||||||
|
public interface CustomChoiceListener {
|
||||||
|
|
||||||
|
void onIntoCustomChoice(EasyRecyclerView view);
|
||||||
|
|
||||||
|
void onOutOfCustomChoice(EasyRecyclerView view);
|
||||||
|
|
||||||
|
void onItemCheckedStateChanged(EasyRecyclerView view, int position, long id, boolean checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This saved state class is a Parcelable and should not extend
|
||||||
|
* {@link android.view.View.BaseSavedState} nor {@link android.view.AbsSavedState}
|
||||||
|
* because its super class AbsSavedState's constructor
|
||||||
|
* currently passes null
|
||||||
|
* as a class loader to read its superstate from Parcelable.
|
||||||
|
* This causes {@link android.os.BadParcelableException} when restoring saved states.
|
||||||
|
* <p/>
|
||||||
|
* The super class "RecyclerView" is a part of the support library,
|
||||||
|
* and restoring its saved state requires the class loader that loaded the RecyclerView.
|
||||||
|
* It seems that the class loader is not required when restoring from RecyclerView itself,
|
||||||
|
* but it is required when restoring from RecyclerView's subclasses.
|
||||||
|
*/
|
||||||
|
static class SavedState implements Parcelable {
|
||||||
|
|
||||||
|
public static final SavedState EMPTY_STATE = new SavedState() {
|
||||||
|
};
|
||||||
|
public static final Parcelable.Creator<SavedState> CREATOR
|
||||||
|
= new Parcelable.Creator<SavedState>() {
|
||||||
|
@Override
|
||||||
|
public SavedState createFromParcel(Parcel in) {
|
||||||
|
return new SavedState(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SavedState[] newArray(int size) {
|
||||||
|
return new SavedState[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
int choiceMode;
|
||||||
|
boolean customChoice;
|
||||||
|
int checkedItemCount;
|
||||||
|
SparseBooleanArray checkState;
|
||||||
|
LongSparseArray<Integer> checkIdState;
|
||||||
|
// This keeps the parent(RecyclerView)'s state
|
||||||
|
Parcelable mSuperState;
|
||||||
|
|
||||||
|
SavedState() {
|
||||||
|
mSuperState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor called from {@link #onSaveInstanceState()}
|
||||||
|
*/
|
||||||
|
SavedState(Parcelable superState) {
|
||||||
|
mSuperState = superState != EMPTY_STATE ? superState : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor called from {@link #CREATOR}
|
||||||
|
*/
|
||||||
|
private SavedState(Parcel in) {
|
||||||
|
// Parcel 'in' has its parent(RecyclerView)'s saved state.
|
||||||
|
// To restore it, class loader that loaded RecyclerView is required.
|
||||||
|
Parcelable superState = in.readParcelable(RecyclerView.class.getClassLoader());
|
||||||
|
mSuperState = superState != null ? superState : EMPTY_STATE;
|
||||||
|
|
||||||
|
choiceMode = in.readInt();
|
||||||
|
customChoice = NumberUtils.int2boolean(in.readInt());
|
||||||
|
checkedItemCount = in.readInt();
|
||||||
|
checkState = in.readSparseBooleanArray();
|
||||||
|
final int N = in.readInt();
|
||||||
|
if (N > 0) {
|
||||||
|
checkIdState = new LongSparseArray<>();
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
final long key = in.readLong();
|
||||||
|
final int value = in.readInt();
|
||||||
|
checkIdState.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel out, int flags) {
|
||||||
|
out.writeParcelable(mSuperState, flags);
|
||||||
|
|
||||||
|
out.writeInt(choiceMode);
|
||||||
|
out.writeInt(NumberUtils.boolean2int(customChoice));
|
||||||
|
out.writeInt(checkedItemCount);
|
||||||
|
out.writeSparseBooleanArray(checkState);
|
||||||
|
final int N = checkIdState != null ? checkIdState.size() : 0;
|
||||||
|
out.writeInt(N);
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
out.writeLong(checkIdState.keyAt(i));
|
||||||
|
out.writeInt(checkIdState.valueAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Parcelable getSuperState() {
|
||||||
|
return mSuperState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiChoiceModeWrapper implements MultiChoiceModeListener {
|
||||||
|
private MultiChoiceModeListener mWrapped;
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||||
|
public boolean hasWrappedCallback() {
|
||||||
|
return mWrapped != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||||
|
if (mWrapped.onCreateActionMode(mode, menu)) {
|
||||||
|
// Initialize checked graphic state?
|
||||||
|
setLongClickable(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||||
|
return mWrapped.onPrepareActionMode(mode, menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||||
|
return mWrapped.onActionItemClicked(mode, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode) {
|
||||||
|
mWrapped.onDestroyActionMode(mode);
|
||||||
|
mChoiceActionMode = null;
|
||||||
|
|
||||||
|
// Ending selection mode means deselecting everything.
|
||||||
|
clearChoices();
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
|
||||||
|
setLongClickable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemCheckedStateChanged(ActionMode mode,
|
||||||
|
int position, long id, boolean checked) {
|
||||||
|
mWrapped.onItemCheckedStateChanged(mode, position, id, checked);
|
||||||
|
|
||||||
|
// If there are no items selected we no longer need the selection mode.
|
||||||
|
if (getCheckedItemCount() == 0) {
|
||||||
|
mode.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 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.easyrecyclerview;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewConfiguration;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.R;
|
||||||
|
import com.hippo.yorozuya.AnimationUtils;
|
||||||
|
import com.hippo.yorozuya.LayoutUtils;
|
||||||
|
import com.hippo.yorozuya.MathUtils;
|
||||||
|
import com.hippo.yorozuya.SimpleAnimatorListener;
|
||||||
|
import com.hippo.yorozuya.SimpleHandler;
|
||||||
|
|
||||||
|
public class FastScroller extends View {
|
||||||
|
|
||||||
|
private static final int INVALID = -1;
|
||||||
|
|
||||||
|
private static final int SCROLL_BAR_FADE_DURATION = 500;
|
||||||
|
private static final int SCROLL_BAR_DELAY = 1000;
|
||||||
|
|
||||||
|
private static final int MIN_HANDLER_HEIGHT_DP = 32;
|
||||||
|
|
||||||
|
private Handler mSimpleHandler;
|
||||||
|
|
||||||
|
private boolean mDraggable;
|
||||||
|
|
||||||
|
private int mMinHandlerHeight;
|
||||||
|
|
||||||
|
private RecyclerView mRecyclerView;
|
||||||
|
private RecyclerView.OnScrollListener mOnScrollChangeListener;
|
||||||
|
private RecyclerView.Adapter<?> mAdapter;
|
||||||
|
private RecyclerView.AdapterDataObserver mAdapterDataObserver;
|
||||||
|
|
||||||
|
private Drawable mHandler;
|
||||||
|
private int mHandlerOffset = INVALID;
|
||||||
|
private int mHandlerHeight = INVALID;
|
||||||
|
|
||||||
|
private float mDownX = INVALID;
|
||||||
|
private float mDownY = INVALID;
|
||||||
|
|
||||||
|
private float mLastMotionY = INVALID;
|
||||||
|
|
||||||
|
private boolean mDragged = false;
|
||||||
|
|
||||||
|
private boolean mCantDrag = false;
|
||||||
|
|
||||||
|
private int mTouchSlop;
|
||||||
|
|
||||||
|
private OnDragHandlerListener mListener;
|
||||||
|
|
||||||
|
private ObjectAnimator mShowAnimator;
|
||||||
|
private ObjectAnimator mHideAnimator;
|
||||||
|
|
||||||
|
private final Runnable mHideRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mHideAnimator.start();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public FastScroller(Context context) {
|
||||||
|
super(context);
|
||||||
|
init(context, null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FastScroller(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FastScroller(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
mSimpleHandler = SimpleHandler.getInstance();
|
||||||
|
|
||||||
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, defStyleAttr, 0);
|
||||||
|
mHandler = a.getDrawable(R.styleable.FastScroller_handler);
|
||||||
|
mDraggable = a.getBoolean(R.styleable.FastScroller_draggable, true);
|
||||||
|
a.recycle();
|
||||||
|
|
||||||
|
setAlpha(0.0f);
|
||||||
|
setVisibility(INVISIBLE);
|
||||||
|
|
||||||
|
mMinHandlerHeight = LayoutUtils.dp2pix(context, MIN_HANDLER_HEIGHT_DP);
|
||||||
|
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||||
|
|
||||||
|
mShowAnimator = ObjectAnimator.ofFloat(this, "alpha", 1.0f);
|
||||||
|
mShowAnimator.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);
|
||||||
|
mShowAnimator.setDuration(SCROLL_BAR_FADE_DURATION);
|
||||||
|
|
||||||
|
mHideAnimator = ObjectAnimator.ofFloat(this, "alpha", 0.0f);
|
||||||
|
mHideAnimator.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR);
|
||||||
|
mHideAnimator.setDuration(SCROLL_BAR_FADE_DURATION);
|
||||||
|
mHideAnimator.addListener(new SimpleAnimatorListener() {
|
||||||
|
|
||||||
|
private boolean mCancel;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationCancel(Animator animation) {
|
||||||
|
mCancel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
if (mCancel) {
|
||||||
|
mCancel = false;
|
||||||
|
} else {
|
||||||
|
setVisibility(INVISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnDragHandlerListener(OnDragHandlerListener listener) {
|
||||||
|
mListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePosition(boolean show) {
|
||||||
|
if (mRecyclerView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int paddingTop = getPaddingTop();
|
||||||
|
int paddingBottom = getPaddingBottom();
|
||||||
|
int height = getHeight() - paddingTop - paddingBottom;
|
||||||
|
int offset = mRecyclerView.computeVerticalScrollOffset();
|
||||||
|
int extent = mRecyclerView.computeVerticalScrollExtent();
|
||||||
|
int range = mRecyclerView.computeVerticalScrollRange();
|
||||||
|
|
||||||
|
if (height <= 0 || extent >= range || extent <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int endOffset = height * offset / range;
|
||||||
|
int endHeight = height * extent / range;
|
||||||
|
|
||||||
|
endHeight = Math.max(endHeight, mMinHandlerHeight);
|
||||||
|
endOffset = Math.min(endOffset, height - endHeight);
|
||||||
|
|
||||||
|
mHandlerOffset = endOffset + paddingTop;
|
||||||
|
mHandlerHeight = endHeight;
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
if (mHideAnimator.isRunning()) {
|
||||||
|
mHideAnimator.cancel();
|
||||||
|
mShowAnimator.start();
|
||||||
|
} else if (getVisibility() != VISIBLE && !mShowAnimator.isRunning()) {
|
||||||
|
setVisibility(VISIBLE);
|
||||||
|
mShowAnimator.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler handler = mSimpleHandler;
|
||||||
|
handler.removeCallbacks(mHideRunnable);
|
||||||
|
|
||||||
|
if (!mDragged) {
|
||||||
|
handler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHandlerDrawable(Drawable drawable) {
|
||||||
|
mHandler = drawable;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDraggable() {
|
||||||
|
return mDraggable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDraggable(boolean draggable) {
|
||||||
|
mDraggable = draggable;
|
||||||
|
if (mDragged) {
|
||||||
|
mDragged = false;
|
||||||
|
}
|
||||||
|
mSimpleHandler.removeCallbacks(mHideRunnable);
|
||||||
|
mHideRunnable.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAttached() {
|
||||||
|
return mRecyclerView != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void attachToRecyclerView(RecyclerView recyclerView) {
|
||||||
|
if (recyclerView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mRecyclerView != null) {
|
||||||
|
throw new IllegalStateException("The FastScroller is already attached to a RecyclerView, " +
|
||||||
|
"call detachedFromRecyclerView first");
|
||||||
|
}
|
||||||
|
|
||||||
|
mRecyclerView = recyclerView;
|
||||||
|
mOnScrollChangeListener = new RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
updatePosition(true);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
recyclerView.addOnScrollListener(mOnScrollChangeListener);
|
||||||
|
|
||||||
|
mAdapter = recyclerView.getAdapter();
|
||||||
|
if (mAdapter != null) {
|
||||||
|
mAdapterDataObserver = new RecyclerView.AdapterDataObserver() {
|
||||||
|
@Override
|
||||||
|
public void onItemRangeChanged(int positionStart, int itemCount) {
|
||||||
|
super.onItemRangeChanged(positionStart, itemCount);
|
||||||
|
updatePosition(false);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
|
||||||
|
super.onItemRangeChanged(positionStart, itemCount, payload);
|
||||||
|
updatePosition(false);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||||
|
super.onItemRangeInserted(positionStart, itemCount);
|
||||||
|
updatePosition(false);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||||
|
super.onItemRangeRemoved(positionStart, itemCount);
|
||||||
|
updatePosition(false);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||||
|
super.onItemRangeMoved(fromPosition, toPosition, itemCount);
|
||||||
|
updatePosition(false);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mAdapter.registerAdapterDataObserver(mAdapterDataObserver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void detachedFromRecyclerView() {
|
||||||
|
if (mRecyclerView != null && mOnScrollChangeListener != null) {
|
||||||
|
mRecyclerView.removeOnScrollListener(mOnScrollChangeListener);
|
||||||
|
}
|
||||||
|
mRecyclerView = null;
|
||||||
|
mOnScrollChangeListener = null;
|
||||||
|
|
||||||
|
if (mAdapter != null && mAdapterDataObserver != null) {
|
||||||
|
mAdapter.unregisterAdapterDataObserver(mAdapterDataObserver);
|
||||||
|
}
|
||||||
|
mAdapter = null;
|
||||||
|
mAdapterDataObserver = null;
|
||||||
|
|
||||||
|
setAlpha(0.0f);
|
||||||
|
setVisibility(INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
if (mRecyclerView == null || mHandler == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mHandlerHeight == INVALID) {
|
||||||
|
updatePosition(false);
|
||||||
|
}
|
||||||
|
if (mHandlerHeight == INVALID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int paddingLeft = getPaddingLeft();
|
||||||
|
int saved = canvas.save();
|
||||||
|
canvas.translate(paddingLeft, mHandlerOffset);
|
||||||
|
mHandler.setBounds(0, 0, getWidth() - paddingLeft - getPaddingRight(), mHandlerHeight);
|
||||||
|
mHandler.draw(canvas);
|
||||||
|
canvas.restoreToCount(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
if (!mDraggable || getVisibility() != VISIBLE || mRecyclerView == null || mHandlerHeight == INVALID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int action = event.getAction();
|
||||||
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
|
mCantDrag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCantDrag) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_DOWN: {
|
||||||
|
mDragged = false;
|
||||||
|
mDownX = event.getX();
|
||||||
|
mDownY = event.getY();
|
||||||
|
if (mDownY < mHandlerOffset || mDownY > mHandlerOffset + mHandlerHeight) {
|
||||||
|
mCantDrag = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
if (!mDragged) {
|
||||||
|
float x = event.getX();
|
||||||
|
float y = event.getY();
|
||||||
|
// Check touch slop
|
||||||
|
if (MathUtils.dist(x, y, mDownX, mDownY) < mTouchSlop) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Math.abs(x - mDownX) > Math.abs(y - mDownY) ||
|
||||||
|
y < mHandlerOffset || y > mHandlerOffset + mHandlerHeight) {
|
||||||
|
mCantDrag = true;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
mDragged = true;
|
||||||
|
mSimpleHandler.removeCallbacks(mHideRunnable);
|
||||||
|
// Update mLastMotionY
|
||||||
|
if (mDownY < mHandlerOffset || mDownY >= mHandlerOffset + mHandlerHeight) {
|
||||||
|
// the point out of handler, make the point in handler center
|
||||||
|
mLastMotionY = mHandlerOffset + mHandlerHeight / 2;
|
||||||
|
} else {
|
||||||
|
mLastMotionY = mDownY;
|
||||||
|
}
|
||||||
|
// Notify
|
||||||
|
if (mListener != null) {
|
||||||
|
mListener.onStartDragHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int range = mRecyclerView.computeVerticalScrollRange();
|
||||||
|
if (range <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
float y = event.getY();
|
||||||
|
int scroll = (int) (range * (y - mLastMotionY) / (getHeight() - getPaddingTop() - getPaddingBottom()));
|
||||||
|
mRecyclerView.scrollBy(0, scroll);
|
||||||
|
mLastMotionY = y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
// Notify
|
||||||
|
if (mDragged && mListener != null) {
|
||||||
|
mListener.onEndDragHandler();
|
||||||
|
}
|
||||||
|
mDragged = false;
|
||||||
|
mSimpleHandler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnDragHandlerListener {
|
||||||
|
void onStartDragHandler();
|
||||||
|
|
||||||
|
void onEndDragHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 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.easyrecyclerview;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class HandlerDrawable extends Drawable {
|
||||||
|
|
||||||
|
private final Paint mPaint;
|
||||||
|
private final RectF mTemp = new RectF();
|
||||||
|
private int mColor = Color.BLACK;
|
||||||
|
|
||||||
|
public HandlerDrawable() {
|
||||||
|
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
mPaint.setColor(mColor);
|
||||||
|
mPaint.setStyle(Paint.Style.FILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull Canvas canvas) {
|
||||||
|
int width = getBounds().width();
|
||||||
|
int height = getBounds().height();
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
canvas.drawRect(0, 0, width, height, mPaint);
|
||||||
|
} else {
|
||||||
|
mTemp.set(0, 0, width, width);
|
||||||
|
canvas.drawArc(mTemp, -180, 180, true, mPaint);
|
||||||
|
mTemp.set(0, height - width, width, height);
|
||||||
|
canvas.drawArc(mTemp, 0, 180, true, mPaint);
|
||||||
|
float halfWidth = (float) width / 2.0f;
|
||||||
|
canvas.drawRect(0, halfWidth, width, height - halfWidth, mPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColor(int color) {
|
||||||
|
if (mColor != color) {
|
||||||
|
mColor = color;
|
||||||
|
mPaint.setColor(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
mPaint.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(ColorFilter colorFilter) {
|
||||||
|
mPaint.setColorFilter(colorFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
int alpha = Color.alpha(mColor);
|
||||||
|
if (alpha == 0xff) {
|
||||||
|
return PixelFormat.OPAQUE;
|
||||||
|
} else if (alpha == 0x00) {
|
||||||
|
return PixelFormat.TRANSPARENT;
|
||||||
|
} else {
|
||||||
|
return PixelFormat.TRANSLUCENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 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.easyrecyclerview;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.PointF;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||||
|
|
||||||
|
import com.hippo.yorozuya.MathUtils;
|
||||||
|
import com.hippo.yorozuya.SimpleHandler;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public final class LayoutManagerUtils {
|
||||||
|
|
||||||
|
private static Method sCsdfp = null;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
sCsdfp = StaggeredGridLayoutManager.class.getDeclaredMethod("calculateScrollDirectionForPosition", int.class);
|
||||||
|
sCsdfp.setAccessible(true);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
// Ignore
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void scrollToPositionWithOffset(
|
||||||
|
RecyclerView.LayoutManager layoutManager, int position, int offset) {
|
||||||
|
if (layoutManager instanceof LinearLayoutManager) {
|
||||||
|
((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(position, offset);
|
||||||
|
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||||
|
((StaggeredGridLayoutManager) layoutManager).scrollToPositionWithOffset(position, offset);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Can't do scrollToPositionWithOffset for " +
|
||||||
|
layoutManager.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void smoothScrollToPosition(
|
||||||
|
RecyclerView.LayoutManager layoutManager, Context context, int position) {
|
||||||
|
smoothScrollToPosition(layoutManager, context, position, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void smoothScrollToPosition(
|
||||||
|
RecyclerView.LayoutManager layoutManager, Context context, int position,
|
||||||
|
int millisecondsPerInch) {
|
||||||
|
SimpleSmoothScroller smoothScroller;
|
||||||
|
if (layoutManager instanceof final LinearLayoutManager linearLayoutManager) {
|
||||||
|
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
|
||||||
|
@Override
|
||||||
|
public PointF computeScrollVectorForPosition(int targetPosition) {
|
||||||
|
return linearLayoutManager.computeScrollVectorForPosition(targetPosition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} else if (layoutManager instanceof final StaggeredGridLayoutManager staggeredGridLayoutManager) {
|
||||||
|
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
|
||||||
|
@Override
|
||||||
|
public PointF computeScrollVectorForPosition(int targetPosition) {
|
||||||
|
int direction = 0;
|
||||||
|
try {
|
||||||
|
direction = (Integer) sCsdfp.invoke(staggeredGridLayoutManager, targetPosition);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (staggeredGridLayoutManager.getOrientation() == StaggeredGridLayoutManager.HORIZONTAL) {
|
||||||
|
return new PointF(direction, 0);
|
||||||
|
} else {
|
||||||
|
return new PointF(0, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Can't do smoothScrollToPosition for " +
|
||||||
|
layoutManager.getClass().getName());
|
||||||
|
}
|
||||||
|
smoothScroller.setTargetPosition(position);
|
||||||
|
layoutManager.startSmoothScroll(smoothScroller);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void scrollToPositionProperly(final RecyclerView.LayoutManager layoutManager,
|
||||||
|
final Context context, final int position, final OnScrollToPositionListener listener) {
|
||||||
|
SimpleHandler.getInstance().postDelayed(() -> {
|
||||||
|
int first = getFirstVisibleItemPosition(layoutManager);
|
||||||
|
int last = getLastVisibleItemPosition(layoutManager);
|
||||||
|
int offset = Math.abs(position - first);
|
||||||
|
int max = last - first;
|
||||||
|
if (offset < max && max > 0) {
|
||||||
|
smoothScrollToPosition(layoutManager, context, position,
|
||||||
|
MathUtils.lerp(100, 25, (offset / max)));
|
||||||
|
} else {
|
||||||
|
scrollToPositionWithOffset(layoutManager, position, 0);
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onScrollToPosition(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getFirstVisibleItemPosition(RecyclerView.LayoutManager layoutManager) {
|
||||||
|
if (layoutManager instanceof LinearLayoutManager) {
|
||||||
|
return ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
|
||||||
|
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||||
|
int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null);
|
||||||
|
return MathUtils.min(positions);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Can't do getFirstVisibleItemPosition for " +
|
||||||
|
layoutManager.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLastVisibleItemPosition(RecyclerView.LayoutManager layoutManager) {
|
||||||
|
if (layoutManager instanceof LinearLayoutManager) {
|
||||||
|
return ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
|
||||||
|
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||||
|
int[] positions = ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(null);
|
||||||
|
return MathUtils.max(positions);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Can't do getLastVisibleItemPosition for " +
|
||||||
|
layoutManager.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnScrollToPositionListener {
|
||||||
|
void onScrollToPosition(int position);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 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.easyrecyclerview;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
public class MarginItemDecoration extends RecyclerView.ItemDecoration {
|
||||||
|
|
||||||
|
private int mMargin;
|
||||||
|
private int mPaddingLeft;
|
||||||
|
private int mPaddingTop;
|
||||||
|
private int mPaddingRight;
|
||||||
|
private int mPaddingBottom;
|
||||||
|
|
||||||
|
public MarginItemDecoration(int margin, int paddingLeft, int paddingTop,
|
||||||
|
int paddingRight, int paddingBottom) {
|
||||||
|
setMargin(margin, paddingLeft, paddingTop, paddingRight, paddingBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getItemOffsets(Rect outRect, View view,
|
||||||
|
RecyclerView parent, RecyclerView.State state) {
|
||||||
|
outRect.set(mMargin, mMargin, mMargin, mMargin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param margin gap between two item
|
||||||
|
* @param paddingLeft gap between RecyclerView left and left item left
|
||||||
|
* @param paddingTop gap between RecyclerView top and top item top
|
||||||
|
* @param paddingRight gap between RecyclerView right and right item right
|
||||||
|
* @param paddingBottom gap between RecyclerView bottom and bottom item bottom
|
||||||
|
*/
|
||||||
|
public void setMargin(int margin, int paddingLeft, int paddingTop,
|
||||||
|
int paddingRight, int paddingBottom) {
|
||||||
|
int halfMargin = margin / 2;
|
||||||
|
mMargin = halfMargin;
|
||||||
|
mPaddingLeft = paddingLeft - halfMargin;
|
||||||
|
mPaddingTop = paddingTop - halfMargin;
|
||||||
|
mPaddingRight = paddingRight - halfMargin;
|
||||||
|
mPaddingBottom = paddingBottom - halfMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyPaddings(View view) {
|
||||||
|
view.setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 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.easyrecyclerview;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
public class SimpleHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
public SimpleHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 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.easyrecyclerview;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||||
|
|
||||||
|
public abstract class SimpleSmoothScroller extends LinearSmoothScroller {
|
||||||
|
|
||||||
|
private final float mMillisecondsPerPx;
|
||||||
|
|
||||||
|
public SimpleSmoothScroller(Context context, float millisecondsPerInch) {
|
||||||
|
super(context);
|
||||||
|
mMillisecondsPerPx = millisecondsPerInch / context.getResources().getDisplayMetrics().densityDpi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int calculateTimeForScrolling(int dx) {
|
||||||
|
if (mMillisecondsPerPx <= 0) {
|
||||||
|
return super.calculateTimeForScrolling(dx);
|
||||||
|
} else {
|
||||||
|
return (int) Math.ceil(Math.abs(dx) * mMillisecondsPerPx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Environment;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.client.exception.ParseException;
|
||||||
|
import com.hippo.util.ReadableTime;
|
||||||
|
import com.hippo.yorozuya.FileUtils;
|
||||||
|
import com.hippo.yorozuya.IOUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class AppConfig {
|
||||||
|
|
||||||
|
public static final String APP_DIRNAME = "EhViewer";
|
||||||
|
|
||||||
|
private static final String DOWNLOAD = "download";
|
||||||
|
private static final String TEMP = "temp";
|
||||||
|
private static final String PARSE_ERROR = "parse_error";
|
||||||
|
private static final String CRASH = "crash";
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private static Context sContext;
|
||||||
|
|
||||||
|
public static void initialize(Context context) {
|
||||||
|
sContext = context.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getExternalAppDir() {
|
||||||
|
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
|
||||||
|
File dir = sContext.getExternalFilesDir(null);
|
||||||
|
return FileUtils.ensureDirectory(dir) ? dir : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mkdirs and get
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static File getDirInExternalAppDir(String filename) {
|
||||||
|
File appFolder = getExternalAppDir();
|
||||||
|
if (appFolder != null) {
|
||||||
|
File dir = new File(appFolder, filename);
|
||||||
|
return FileUtils.ensureDirectory(dir) ? dir : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getFileInExternalAppDir(String filename) {
|
||||||
|
File appFolder = getExternalAppDir();
|
||||||
|
if (appFolder != null) {
|
||||||
|
File file = new File(appFolder, filename);
|
||||||
|
return FileUtils.ensureFile(file) ? file : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getDefaultDownloadDir() {
|
||||||
|
return getDirInExternalAppDir(DOWNLOAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getExternalTempDir() {
|
||||||
|
File dir = sContext.getExternalCacheDir();
|
||||||
|
File file;
|
||||||
|
if (null != dir && FileUtils.ensureDirectory(file = new File(dir, TEMP))) {
|
||||||
|
return file;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getExternalCopyTempDir() {
|
||||||
|
File dir = sContext.getExternalCacheDir();
|
||||||
|
File file;
|
||||||
|
if (null != dir && FileUtils.ensureDirectory(file = new File(dir, "copy"))) {
|
||||||
|
return file;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getExternalParseErrorDir() {
|
||||||
|
return getDirInExternalAppDir(PARSE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getExternalCrashDir() {
|
||||||
|
return getDirInExternalAppDir(CRASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getTempDir() {
|
||||||
|
File dir = sContext.getCacheDir();
|
||||||
|
File file;
|
||||||
|
if (null != dir && FileUtils.ensureDirectory(file = new File(dir, TEMP))) {
|
||||||
|
return file;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File createTempFile() {
|
||||||
|
return FileUtils.createTempFile(getTempDir(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveParseErrorBody(ParseException e) {
|
||||||
|
File dir = getExternalParseErrorDir();
|
||||||
|
if (null == dir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = new File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt");
|
||||||
|
OutputStream os = null;
|
||||||
|
try {
|
||||||
|
os = new FileOutputStream(file);
|
||||||
|
String message = e.getMessage();
|
||||||
|
String body = e.getBody();
|
||||||
|
if (null != message) {
|
||||||
|
os.write(message.getBytes(StandardCharsets.UTF_8));
|
||||||
|
os.write('\n');
|
||||||
|
}
|
||||||
|
if (null != body) {
|
||||||
|
os.write(body.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
os.flush();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
// Ignore
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getFilesDir(String name) {
|
||||||
|
File dir = sContext.getFilesDir();
|
||||||
|
if (dir == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = new File(dir, name);
|
||||||
|
if (dir.isDirectory() || dir.mkdirs()) {
|
||||||
|
return dir;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Debug
|
||||||
|
import com.hippo.util.ReadableTime
|
||||||
|
import com.hippo.yorozuya.FileUtils
|
||||||
|
import com.hippo.yorozuya.IOUtils
|
||||||
|
import com.hippo.yorozuya.OSUtils
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileWriter
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.util.Arrays
|
||||||
|
|
||||||
|
object Crash {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun collectInfo(fw: FileWriter) {
|
||||||
|
fw.write("======== PackageInfo ========\r\n")
|
||||||
|
fw.write("PackageName=")
|
||||||
|
fw.write(BuildConfig.APPLICATION_ID)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("VersionName=")
|
||||||
|
fw.write(BuildConfig.VERSION_NAME)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("VersionCode=")
|
||||||
|
fw.write(BuildConfig.VERSION_CODE)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("CommitSha=")
|
||||||
|
fw.write(BuildConfig.COMMIT_SHA)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("BuildTime=")
|
||||||
|
fw.write(BuildConfig.BUILD_TIME)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("\r\n")
|
||||||
|
|
||||||
|
// Runtime
|
||||||
|
val topActivityClazzName = EhApplication.application.topActivity?.javaClass?.name
|
||||||
|
fw.write("======== Runtime ========\r\n")
|
||||||
|
fw.write("TopActivity=")
|
||||||
|
fw.write(topActivityClazzName ?: "null")
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("\r\n")
|
||||||
|
|
||||||
|
// Device info
|
||||||
|
fw.write("======== DeviceInfo ========\r\n")
|
||||||
|
fw.write("BOARD=")
|
||||||
|
fw.write(Build.BOARD)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("BOOTLOADER=")
|
||||||
|
fw.write(Build.BOOTLOADER)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("SUPPORTED_ABIS=")
|
||||||
|
fw.write(Arrays.toString(Build.SUPPORTED_ABIS))
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("DEVICE=")
|
||||||
|
fw.write(Build.DEVICE)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("DISPLAY=")
|
||||||
|
fw.write(Build.DISPLAY)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("FINGERPRINT=")
|
||||||
|
fw.write(Build.FINGERPRINT)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("HARDWARE=")
|
||||||
|
fw.write(Build.HARDWARE)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("HOST=")
|
||||||
|
fw.write(Build.HOST)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("ID=")
|
||||||
|
fw.write(Build.ID)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("MANUFACTURER=")
|
||||||
|
fw.write(Build.MANUFACTURER)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("MODEL=")
|
||||||
|
fw.write(Build.MODEL)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("PRODUCT=")
|
||||||
|
fw.write(Build.PRODUCT)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("RADIO=")
|
||||||
|
fw.write(Build.getRadioVersion())
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("TAGS=")
|
||||||
|
fw.write(Build.TAGS)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("TYPE=")
|
||||||
|
fw.write(Build.TYPE)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("USER=")
|
||||||
|
fw.write(Build.USER)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("CODENAME=")
|
||||||
|
fw.write(Build.VERSION.CODENAME)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("INCREMENTAL=")
|
||||||
|
fw.write(Build.VERSION.INCREMENTAL)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("RELEASE=")
|
||||||
|
fw.write(Build.VERSION.RELEASE)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("SDK=")
|
||||||
|
fw.write(Build.VERSION.SDK_INT.toString())
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("MEMORY=")
|
||||||
|
fw.write(
|
||||||
|
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false)
|
||||||
|
)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("MEMORY_NATIVE=")
|
||||||
|
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false))
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("MEMORY_MAX=")
|
||||||
|
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false))
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("MEMORY_TOTAL=")
|
||||||
|
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false))
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getThrowableInfo(t: Throwable, fw: FileWriter) {
|
||||||
|
val printWriter = PrintWriter(fw)
|
||||||
|
t.printStackTrace(printWriter)
|
||||||
|
var cause = t.cause
|
||||||
|
while (cause != null) {
|
||||||
|
cause.printStackTrace(printWriter)
|
||||||
|
cause = cause.cause
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCrashLog(t: Throwable) {
|
||||||
|
val dir = AppConfig.getExternalCrashDir() ?: return
|
||||||
|
val nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis())
|
||||||
|
val fileName = "crash-$nowString.log"
|
||||||
|
val file = File(dir, fileName)
|
||||||
|
var fw: FileWriter? = null
|
||||||
|
try {
|
||||||
|
fw = FileWriter(file)
|
||||||
|
fw.write("TIME=")
|
||||||
|
fw.write(nowString)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.write("\r\n")
|
||||||
|
collectInfo(fw)
|
||||||
|
fw.write("======== CrashInfo ========\r\n")
|
||||||
|
getThrowableInfo(t, fw)
|
||||||
|
fw.write("\r\n")
|
||||||
|
fw.flush()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
file.delete()
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(fw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ComponentCallbacks2
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.collection.LruCache
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.ImageLoaderFactory
|
||||||
|
import coil.util.DebugLogger
|
||||||
|
import com.hippo.Native
|
||||||
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
|
import com.hippo.ehviewer.client.EhDns
|
||||||
|
import com.hippo.ehviewer.client.EhSSLSocketFactory
|
||||||
|
import com.hippo.ehviewer.client.EhTagDatabase
|
||||||
|
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||||
|
import com.hippo.ehviewer.coil.MergeInterceptor
|
||||||
|
import com.hippo.ehviewer.dao.buildMainDB
|
||||||
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
|
import com.hippo.ehviewer.ui.CommonOperations
|
||||||
|
import com.hippo.ehviewer.ui.EhActivity
|
||||||
|
import com.hippo.util.ExceptionUtils
|
||||||
|
import com.hippo.util.ReadableTime
|
||||||
|
import com.hippo.yorozuya.FileUtils
|
||||||
|
import com.hippo.yorozuya.IntIdGenerator
|
||||||
|
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||||
|
private val mIdGenerator = IntIdGenerator()
|
||||||
|
private val mGlobalStuffMap = HashMap<Int, Any>()
|
||||||
|
private val mActivityList = ArrayList<Activity>()
|
||||||
|
val topActivity: EhActivity?
|
||||||
|
get() = if (mActivityList.isNotEmpty()) {
|
||||||
|
mActivityList[mActivityList.size - 1] as EhActivity
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun recreateAllActivity() {
|
||||||
|
mActivityList.forEach { it.recreate() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
override fun onCreate() {
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
application = this
|
||||||
|
val handler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { t, e ->
|
||||||
|
try {
|
||||||
|
if (Settings.getSaveCrashLog()) {
|
||||||
|
Crash.saveCrashLog(e)
|
||||||
|
}
|
||||||
|
} catch (ignored: Throwable) {
|
||||||
|
}
|
||||||
|
handler?.uncaughtException(t, e)
|
||||||
|
}
|
||||||
|
super<Application>.onCreate()
|
||||||
|
Native.initialize()
|
||||||
|
GetText.initialize(this)
|
||||||
|
Settings.initialize()
|
||||||
|
ReadableTime.initialize(this)
|
||||||
|
AppConfig.initialize(this)
|
||||||
|
EhTagDatabase.update()
|
||||||
|
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
|
||||||
|
launchIO {
|
||||||
|
launchIO {
|
||||||
|
ehDatabase
|
||||||
|
}
|
||||||
|
launchIO {
|
||||||
|
DownloadManager.isIdle
|
||||||
|
}
|
||||||
|
launchIO {
|
||||||
|
cleanupDownload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanupDownload() {
|
||||||
|
try {
|
||||||
|
val downloadLocation = Settings.getDownloadLocation()
|
||||||
|
if (Settings.getMediaScan()) {
|
||||||
|
CommonOperations.removeNoMediaFile(downloadLocation)
|
||||||
|
} else {
|
||||||
|
CommonOperations.ensureNoMediaFile(downloadLocation)
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
t.printStackTrace()
|
||||||
|
ExceptionUtils.throwIfFatal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear temp files
|
||||||
|
try {
|
||||||
|
clearTempDir()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
t.printStackTrace()
|
||||||
|
ExceptionUtils.throwIfFatal(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearTempDir() {
|
||||||
|
var dir = AppConfig.getTempDir()
|
||||||
|
if (null != dir) {
|
||||||
|
FileUtils.deleteContent(dir)
|
||||||
|
}
|
||||||
|
dir = AppConfig.getExternalTempDir()
|
||||||
|
if (null != dir) {
|
||||||
|
FileUtils.deleteContent(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTrimMemory(level: Int) {
|
||||||
|
super.onTrimMemory(level)
|
||||||
|
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
|
||||||
|
galleryDetailCache.evictAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putGlobalStuff(o: Any): Int {
|
||||||
|
val id = mIdGenerator.nextId()
|
||||||
|
mGlobalStuffMap[id] = o
|
||||||
|
Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId())
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containGlobalStuff(id: Int): Boolean {
|
||||||
|
return mGlobalStuffMap.containsKey(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getGlobalStuff(id: Int): Any? {
|
||||||
|
return mGlobalStuffMap[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeGlobalStuff(id: Int): Any? {
|
||||||
|
return mGlobalStuffMap.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeGlobalStuff(o: Any) {
|
||||||
|
mGlobalStuffMap.values.removeAll(setOf(o))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerActivity(activity: Activity) {
|
||||||
|
mActivityList.add(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterActivity(activity: Activity) {
|
||||||
|
mActivityList.remove(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause(owner: LifecycleOwner) {
|
||||||
|
if (!locked)
|
||||||
|
locked_last_leave_time = System.currentTimeMillis() / 1000
|
||||||
|
locked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(): ImageLoader {
|
||||||
|
return ImageLoader.Builder(this).apply {
|
||||||
|
okHttpClient(okHttpClient)
|
||||||
|
components { add(MergeInterceptor) }
|
||||||
|
crossfade(300)
|
||||||
|
if (BuildConfig.DEBUG) logger(DebugLogger())
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id"
|
||||||
|
var locked = true
|
||||||
|
var locked_last_leave_time: Long = 0
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
lateinit var application: EhApplication
|
||||||
|
private set
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val ehCookieStore by lazy { EhCookieStore }
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val ehProxySelector by lazy { EhProxySelector() }
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val okHttpClient by lazy {
|
||||||
|
val builder = OkHttpClient.Builder()
|
||||||
|
.cookieJar(ehCookieStore)
|
||||||
|
.dns(EhDns)
|
||||||
|
.proxySelector(ehProxySelector)
|
||||||
|
|
||||||
|
if (Settings.getDF()) {
|
||||||
|
val factory =
|
||||||
|
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())!!
|
||||||
|
factory.init(null as KeyStore?)
|
||||||
|
val manager = factory.trustManagers!!
|
||||||
|
|
||||||
|
val trustManager = manager.filterIsInstance<X509TrustManager>().first()
|
||||||
|
builder.sslSocketFactory(EhSSLSocketFactory, trustManager)
|
||||||
|
builder.proxy(Proxy.NO_PROXY)
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val galleryDetailCache by lazy {
|
||||||
|
LruCache<Long, GalleryDetail>(25).also {
|
||||||
|
favouriteStatusRouter.addListener { gid, slot ->
|
||||||
|
it[gid]?.favoriteSlot = slot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val hosts by lazy { Hosts(application, "hosts.db") }
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val favouriteStatusRouter by lazy { FavouriteStatusRouter() }
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val readerPreferences by lazy { ReaderPreferences(AndroidPreferenceStore(application)) }
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val ehDatabase by lazy { buildMainDB(application) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.room.Room.databaseBuilder
|
||||||
|
import com.hippo.Native
|
||||||
|
import com.hippo.ehviewer.EhApplication.Companion.ehDatabase
|
||||||
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
|
import com.hippo.ehviewer.dao.BasicDao
|
||||||
|
import com.hippo.ehviewer.dao.DownloadDirname
|
||||||
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
|
import com.hippo.ehviewer.dao.DownloadLabel
|
||||||
|
import com.hippo.ehviewer.dao.EhDatabase
|
||||||
|
import com.hippo.ehviewer.dao.Filter
|
||||||
|
import com.hippo.ehviewer.dao.HistoryInfo
|
||||||
|
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||||
|
import com.hippo.ehviewer.dao.QuickSearch
|
||||||
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
|
|
||||||
|
object EhDB {
|
||||||
|
private const val CUR_DB_VER = 4
|
||||||
|
private val db = ehDatabase
|
||||||
|
|
||||||
|
// Fix state
|
||||||
|
@get:Synchronized
|
||||||
|
val allDownloadInfo: List<DownloadInfo>
|
||||||
|
get() = db.downloadsDao().list().onEach {
|
||||||
|
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
|
||||||
|
it.state = DownloadInfo.STATE_NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun updateDownloadInfo(downloadInfos: List<DownloadInfo>) {
|
||||||
|
val dao = db.downloadsDao()
|
||||||
|
dao.update(downloadInfos)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putDownloadInfo(downloadInfo: DownloadInfo) {
|
||||||
|
db.downloadsDao().run {
|
||||||
|
if (load(downloadInfo.gid) != null) {
|
||||||
|
update(downloadInfo)
|
||||||
|
} else {
|
||||||
|
insert(downloadInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun removeDownloadInfo(downloadInfo: DownloadInfo) {
|
||||||
|
db.downloadsDao().delete(downloadInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getDownloadDirname(gid: Long): String? {
|
||||||
|
val dao = db.downloadDirnameDao()
|
||||||
|
val raw = dao.load(gid)
|
||||||
|
return raw?.dirname
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putDownloadDirname(gid: Long, dirname: String?) {
|
||||||
|
val dao = db.downloadDirnameDao()
|
||||||
|
var raw = dao.load(gid)
|
||||||
|
if (raw != null) {
|
||||||
|
raw.dirname = dirname
|
||||||
|
dao.update(raw)
|
||||||
|
} else {
|
||||||
|
raw = DownloadDirname(gid, dirname)
|
||||||
|
dao.insert(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun removeDownloadDirname(gid: Long) {
|
||||||
|
val dao = db.downloadDirnameDao()
|
||||||
|
dao.deleteByKey(gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun clearDownloadDirname() {
|
||||||
|
val dao = db.downloadDirnameDao()
|
||||||
|
dao.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
val allDownloadLabelList: List<DownloadLabel>
|
||||||
|
get() = db.downloadLabelDao().list()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun addDownloadLabel(label: String): DownloadLabel {
|
||||||
|
val dao = db.downloadLabelDao()
|
||||||
|
val raw = DownloadLabel()
|
||||||
|
raw.label = label
|
||||||
|
raw.time = System.currentTimeMillis()
|
||||||
|
raw.id = dao.insert(raw)
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
|
||||||
|
// Reset id
|
||||||
|
raw.id = null
|
||||||
|
val dao = db.downloadLabelDao()
|
||||||
|
raw.id = dao.insert(raw)
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun updateDownloadLabel(raw: DownloadLabel?) {
|
||||||
|
val dao = db.downloadLabelDao()
|
||||||
|
dao.update(raw!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun moveDownloadLabel(fromPosition: Int, toPosition: Int) {
|
||||||
|
if (fromPosition == toPosition) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val reverse = fromPosition > toPosition
|
||||||
|
val offset = if (reverse) toPosition else fromPosition
|
||||||
|
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
|
||||||
|
val dao = db.downloadLabelDao()
|
||||||
|
val list = dao.list(offset, limit)
|
||||||
|
val step = if (reverse) 1 else -1
|
||||||
|
val start = if (reverse) limit - 1 else 0
|
||||||
|
val end = if (reverse) 0 else limit - 1
|
||||||
|
val toTime = list[end].time
|
||||||
|
var i = end
|
||||||
|
while (if (reverse) i < start else i > start) {
|
||||||
|
list[i].time = list[i + step].time
|
||||||
|
i += step
|
||||||
|
}
|
||||||
|
list[start].time = toTime
|
||||||
|
dao.update(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun removeDownloadLabel(raw: DownloadLabel?) {
|
||||||
|
val dao = db.downloadLabelDao()
|
||||||
|
dao.delete(raw!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
val allLocalFavorites: List<GalleryInfo>
|
||||||
|
get() {
|
||||||
|
val dao = db.localFavoritesDao()
|
||||||
|
val list = dao.list()
|
||||||
|
return ArrayList<GalleryInfo>(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun searchLocalFavorites(query: String): List<GalleryInfo> {
|
||||||
|
val dao = db.localFavoritesDao()
|
||||||
|
val list = dao.list("%$query%")
|
||||||
|
return ArrayList<GalleryInfo>(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun removeLocalFavorites(gid: Long) {
|
||||||
|
db.localFavoritesDao().deleteByKey(gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun removeLocalFavorites(gidArray: LongArray) {
|
||||||
|
val dao = db.localFavoritesDao()
|
||||||
|
for (gid in gidArray) {
|
||||||
|
dao.deleteByKey(gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Synchronized
|
||||||
|
fun containLocalFavorites(gid: Long): Boolean {
|
||||||
|
val dao = db.localFavoritesDao()
|
||||||
|
return null != dao.load(gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putLocalFavorites(galleryInfo: GalleryInfo) {
|
||||||
|
val dao = db.localFavoritesDao()
|
||||||
|
if (null == dao.load(galleryInfo.gid)) {
|
||||||
|
val info: LocalFavoriteInfo
|
||||||
|
if (galleryInfo is LocalFavoriteInfo) {
|
||||||
|
info = galleryInfo
|
||||||
|
} else {
|
||||||
|
info = LocalFavoriteInfo(galleryInfo)
|
||||||
|
info.time = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
dao.insert(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putLocalFavorites(galleryInfoList: List<GalleryInfo>) {
|
||||||
|
for (gi in galleryInfoList) {
|
||||||
|
putLocalFavorites(gi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
val allQuickSearch: List<QuickSearch>
|
||||||
|
get() {
|
||||||
|
val dao = db.quickSearchDao()
|
||||||
|
return dao.list()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun insertQuickSearch(quickSearch: QuickSearch) {
|
||||||
|
val dao = db.quickSearchDao()
|
||||||
|
quickSearch.id = null
|
||||||
|
quickSearch.time = System.currentTimeMillis()
|
||||||
|
quickSearch.id = dao.insert(quickSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun importQuickSearch(quickSearchList: List<QuickSearch?>) {
|
||||||
|
val dao = db.quickSearchDao()
|
||||||
|
for (quickSearch in quickSearchList) {
|
||||||
|
dao.insert(quickSearch!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deleteQuickSearch(quickSearch: QuickSearch?) {
|
||||||
|
val dao = db.quickSearchDao()
|
||||||
|
dao.delete(quickSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun moveQuickSearch(fromPosition: Int, toPosition: Int) {
|
||||||
|
if (fromPosition == toPosition) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val reverse = fromPosition > toPosition
|
||||||
|
val offset = if (reverse) toPosition else fromPosition
|
||||||
|
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
|
||||||
|
val dao = db.quickSearchDao()
|
||||||
|
val list = dao.list(offset, limit)
|
||||||
|
val step = if (reverse) 1 else -1
|
||||||
|
val start = if (reverse) limit - 1 else 0
|
||||||
|
val end = if (reverse) 0 else limit - 1
|
||||||
|
val toTime = list[end].time
|
||||||
|
var i = end
|
||||||
|
while (if (reverse) i < start else i > start) {
|
||||||
|
list[i].time = list[i + step].time
|
||||||
|
i += step
|
||||||
|
}
|
||||||
|
list[start].time = toTime
|
||||||
|
dao.update(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
val historyLazyList: PagingSource<Int, HistoryInfo>
|
||||||
|
get() = db.historyDao().listLazy()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putHistoryInfo(galleryInfo: GalleryInfo?) {
|
||||||
|
val dao = db.historyDao()
|
||||||
|
val info: HistoryInfo = if (galleryInfo is HistoryInfo) {
|
||||||
|
galleryInfo
|
||||||
|
} else {
|
||||||
|
HistoryInfo(galleryInfo!!)
|
||||||
|
}
|
||||||
|
info.time = System.currentTimeMillis()
|
||||||
|
if (null != dao.load(info.gid)) {
|
||||||
|
dao.update(info)
|
||||||
|
} else {
|
||||||
|
dao.insert(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putHistoryInfoNonRefresh(info: GalleryInfo) {
|
||||||
|
val dao = db.historyDao()
|
||||||
|
val i = dao.load(info.gid)
|
||||||
|
if (null != i) {
|
||||||
|
val historyInfo: HistoryInfo
|
||||||
|
if (info is HistoryInfo) {
|
||||||
|
historyInfo = info
|
||||||
|
} else {
|
||||||
|
historyInfo = HistoryInfo(info)
|
||||||
|
historyInfo.time = i.time
|
||||||
|
}
|
||||||
|
dao.update(historyInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun putHistoryInfo(historyInfoList: List<HistoryInfo>) {
|
||||||
|
val dao = db.historyDao()
|
||||||
|
for (info in historyInfoList) {
|
||||||
|
if (null == dao.load(info.gid)) {
|
||||||
|
dao.insert(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deleteHistoryInfo(info: HistoryInfo?) {
|
||||||
|
val dao = db.historyDao()
|
||||||
|
dao.delete(info!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun clearHistoryInfo() {
|
||||||
|
val dao = db.historyDao()
|
||||||
|
dao.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
val allFilter: List<Filter>
|
||||||
|
get() = db.filterDao().list()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun addFilter(filter: Filter): Boolean {
|
||||||
|
val existFilter: Filter? = try {
|
||||||
|
db.filterDao().load(filter.text!!, filter.mode)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return if (existFilter == null) {
|
||||||
|
filter.id = null
|
||||||
|
filter.id = db.filterDao().insert(filter)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deleteFilter(filter: Filter) {
|
||||||
|
db.filterDao().delete(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun triggerFilter(filter: Filter) {
|
||||||
|
filter.enable = filter.enable?.not() ?: false
|
||||||
|
db.filterDao().update(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> copyDao(from: BasicDao<T>, to: BasicDao<T>) {
|
||||||
|
val list = from.list()
|
||||||
|
for (item in list) to.insert(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun exportDB(context: Context, uri: Uri): Boolean {
|
||||||
|
val ehExportName = "eh.export.db"
|
||||||
|
runCatching {
|
||||||
|
context.deleteDatabase(ehExportName)
|
||||||
|
val newDb = databaseBuilder(context, EhDatabase::class.java, ehExportName).build()
|
||||||
|
copyDao(db.downloadsDao(), newDb.downloadsDao())
|
||||||
|
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao())
|
||||||
|
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao())
|
||||||
|
copyDao(db.historyDao(), newDb.historyDao())
|
||||||
|
copyDao(db.quickSearchDao(), newDb.quickSearchDao())
|
||||||
|
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao())
|
||||||
|
copyDao(db.filterDao(), newDb.filterDao())
|
||||||
|
val dbFile = context.getDatabasePath(ehExportName)
|
||||||
|
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
|
||||||
|
ParcelFileDescriptor.open(dbFile, MODE_READ_ONLY).use { fromFd ->
|
||||||
|
Native.sendfile(fromFd.fd, toFd.fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return error string, null for no error
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun importDB(context: Context, uri: Uri): String? {
|
||||||
|
runCatching {
|
||||||
|
val oldDB = databaseBuilder(context, EhDatabase::class.java, "tmp.db")
|
||||||
|
.createFromInputStream { context.contentResolver.openInputStream(uri) }.build()
|
||||||
|
|
||||||
|
// Download label
|
||||||
|
val manager = DownloadManager
|
||||||
|
runCatching {
|
||||||
|
val downloadLabelList = oldDB.downloadLabelDao().list()
|
||||||
|
manager.addDownloadLabel(downloadLabelList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downloads
|
||||||
|
runCatching {
|
||||||
|
val downloadInfoList = oldDB.downloadsDao().list()
|
||||||
|
manager.addDownload(downloadInfoList, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download dirname
|
||||||
|
runCatching {
|
||||||
|
oldDB.downloadDirnameDao().list().forEach {
|
||||||
|
putDownloadDirname(it.gid, it.dirname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// History
|
||||||
|
runCatching {
|
||||||
|
val historyInfoList = oldDB.historyDao().list()
|
||||||
|
putHistoryInfo(historyInfoList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuickSearch
|
||||||
|
runCatching {
|
||||||
|
val quickSearchList = oldDB.quickSearchDao().list()
|
||||||
|
val currentQuickSearchList = db.quickSearchDao().list()
|
||||||
|
val importList = quickSearchList.mapNotNull { newQS ->
|
||||||
|
newQS.takeIf { currentQuickSearchList.find { it.name == newQS.name } == null }
|
||||||
|
}
|
||||||
|
importQuickSearch(importList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalFavorites
|
||||||
|
runCatching {
|
||||||
|
oldDB.localFavoritesDao().list().forEach {
|
||||||
|
putLocalFavorites(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
runCatching {
|
||||||
|
val filterList = oldDB.filterDao().list()
|
||||||
|
val currentFilterList = db.filterDao().list()
|
||||||
|
filterList.forEach {
|
||||||
|
if (it !in currentFilterList) addFilter(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oldDB.close()
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
return context.getString(R.string.cant_read_the_file)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.hippo.network.InetValidator;
|
||||||
|
import com.hippo.util.ExceptionUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Proxy;
|
||||||
|
import java.net.ProxySelector;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class EhProxySelector extends ProxySelector {
|
||||||
|
|
||||||
|
public static final int TYPE_DIRECT = 0;
|
||||||
|
public static final int TYPE_SYSTEM = 1;
|
||||||
|
public static final int TYPE_HTTP = 2;
|
||||||
|
public static final int TYPE_SOCKS = 3;
|
||||||
|
|
||||||
|
private ProxySelector delegation;
|
||||||
|
private ProxySelector alternative;
|
||||||
|
|
||||||
|
EhProxySelector() {
|
||||||
|
alternative = ProxySelector.getDefault();
|
||||||
|
if (alternative == null) {
|
||||||
|
alternative = new NullProxySelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateProxy() {
|
||||||
|
switch (Settings.getProxyType()) {
|
||||||
|
case TYPE_DIRECT:
|
||||||
|
delegation = new NullProxySelector();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case TYPE_SYSTEM:
|
||||||
|
delegation = alternative;
|
||||||
|
break;
|
||||||
|
case TYPE_HTTP:
|
||||||
|
case TYPE_SOCKS:
|
||||||
|
delegation = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Proxy> select(URI uri) {
|
||||||
|
int type = Settings.getProxyType();
|
||||||
|
if (type == TYPE_HTTP || type == TYPE_SOCKS) {
|
||||||
|
try {
|
||||||
|
String ip = Settings.getProxyIp();
|
||||||
|
int port = Settings.getProxyPort();
|
||||||
|
if (!TextUtils.isEmpty(ip) && InetValidator.isValidInetPort(port)) {
|
||||||
|
InetAddress inetAddress = InetAddress.getByName(ip);
|
||||||
|
SocketAddress socketAddress = new InetSocketAddress(inetAddress, port);
|
||||||
|
return Collections.singletonList(new Proxy(type == TYPE_HTTP ? Proxy.Type.HTTP : Proxy.Type.SOCKS, socketAddress));
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
ExceptionUtils.throwIfFatal(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delegation != null) {
|
||||||
|
return delegation.select(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return alternative.select(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||||
|
if (delegation != null) {
|
||||||
|
delegation.select(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NullProxySelector extends ProxySelector {
|
||||||
|
@Override
|
||||||
|
public List<Proxy> select(URI uri) {
|
||||||
|
return Collections.singletonList(Proxy.NO_PROXY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||||
|
import com.hippo.yorozuya.IntIdGenerator;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class FavouriteStatusRouter {
|
||||||
|
|
||||||
|
private static final String KEY_DATA_MAP_NEXT_ID = "data_map_next_id";
|
||||||
|
|
||||||
|
private final IntIdGenerator idGenerator = new IntIdGenerator(Settings.getInt(KEY_DATA_MAP_NEXT_ID, 0));
|
||||||
|
@SuppressLint("UseSparseArrays")
|
||||||
|
private final HashMap<Integer, Map<Long, GalleryInfo>> maps = new HashMap<>();
|
||||||
|
|
||||||
|
private final List<Listener> listeners = new ArrayList<>();
|
||||||
|
|
||||||
|
public int saveDataMap(Map<Long, GalleryInfo> map) {
|
||||||
|
int id = idGenerator.nextId();
|
||||||
|
maps.put(id, map);
|
||||||
|
Settings.putInt(KEY_DATA_MAP_NEXT_ID, idGenerator.nextId());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Long, GalleryInfo> restoreDataMap(int id) {
|
||||||
|
return maps.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void modifyFavourites(long gid, int slot) {
|
||||||
|
for (Map<Long, GalleryInfo> map : maps.values()) {
|
||||||
|
GalleryInfo info = map.get(gid);
|
||||||
|
if (info != null) {
|
||||||
|
info.setFavoriteSlot(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Listener listener : listeners) {
|
||||||
|
listener.onModifyFavourites(gid, slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(Listener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(Listener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void onModifyFavourites(long gid, int slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
public class GetText {
|
||||||
|
|
||||||
|
private static Resources sResources;
|
||||||
|
|
||||||
|
public static void initialize(Context context) {
|
||||||
|
sResources = context.getApplicationContext().getResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String getString(@StringRes int id) {
|
||||||
|
return sResources.getString(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String getString(@StringRes int id, Object... formatArgs) {
|
||||||
|
return sResources.getString(id, formatArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 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;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by Hippo on 2018/3/21.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.hippo.database.MSQLiteBuilder;
|
||||||
|
import com.hippo.util.SqlUtils;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Hosts {
|
||||||
|
|
||||||
|
private static final int VERSION_1 = 1;
|
||||||
|
private static final String TABLE_HOSTS = "HOSTS";
|
||||||
|
private static final String COLUMN_HOST = "HOST";
|
||||||
|
private static final String COLUMN_IP = "IP";
|
||||||
|
|
||||||
|
private static final int DB_VERSION = VERSION_1;
|
||||||
|
|
||||||
|
private final SQLiteDatabase db;
|
||||||
|
|
||||||
|
public Hosts(Context context, String name) {
|
||||||
|
SQLiteOpenHelper helper = new MSQLiteBuilder()
|
||||||
|
.version(VERSION_1)
|
||||||
|
.createTable(TABLE_HOSTS)
|
||||||
|
.insertColumn(TABLE_HOSTS, COLUMN_HOST, String.class)
|
||||||
|
.insertColumn(TABLE_HOSTS, COLUMN_IP, String.class)
|
||||||
|
.build(context, name, DB_VERSION);
|
||||||
|
db = helper.getWritableDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static InetAddress toInetAddress(String host, String ip) {
|
||||||
|
if (!isValidHost(host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = parseV4(ip);
|
||||||
|
if (bytes == null) {
|
||||||
|
bytes = parseV6(ip);
|
||||||
|
}
|
||||||
|
if (bytes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return InetAddress.getByAddress(host, bytes);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the host is valid.
|
||||||
|
*/
|
||||||
|
public static boolean isValidHost(String host) {
|
||||||
|
if (host == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.length() > 253) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int labelLength = 0;
|
||||||
|
for (int i = 0, n = host.length(); i < n; i++) {
|
||||||
|
char ch = host.charAt(i);
|
||||||
|
|
||||||
|
if (ch == '.') {
|
||||||
|
if (labelLength < 1 || labelLength > 63) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
labelLength = 0;
|
||||||
|
} else {
|
||||||
|
labelLength++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-' && ch != '.') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelLength >= 1 && labelLength <= 63;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if ips are valid.
|
||||||
|
*/
|
||||||
|
public static boolean isValidIp(String ips) {
|
||||||
|
if (ips == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String[] list = ips.split("\\+");
|
||||||
|
for (String ip : list) {
|
||||||
|
if (parseV4(ip) == null && parseV6(ip) == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// org.xbill.DNS.Address.parseV4
|
||||||
|
@Nullable
|
||||||
|
private static byte[] parseV4(String s) {
|
||||||
|
int numDigits;
|
||||||
|
int currentOctet;
|
||||||
|
byte[] values = new byte[4];
|
||||||
|
int currentValue;
|
||||||
|
int length = s.length();
|
||||||
|
|
||||||
|
currentOctet = 0;
|
||||||
|
currentValue = 0;
|
||||||
|
numDigits = 0;
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
|
/* Can't have more than 3 digits per octet. */
|
||||||
|
if (numDigits == 3)
|
||||||
|
return null;
|
||||||
|
/* Octets shouldn't start with 0, unless they are 0. */
|
||||||
|
if (numDigits > 0 && currentValue == 0)
|
||||||
|
return null;
|
||||||
|
numDigits++;
|
||||||
|
currentValue *= 10;
|
||||||
|
currentValue += (c - '0');
|
||||||
|
/* 255 is the maximum value for an octet. */
|
||||||
|
if (currentValue > 255)
|
||||||
|
return null;
|
||||||
|
} else if (c == '.') {
|
||||||
|
/* Can't have more than 3 dots. */
|
||||||
|
if (currentOctet == 3)
|
||||||
|
return null;
|
||||||
|
/* Two consecutive dots are bad. */
|
||||||
|
if (numDigits == 0)
|
||||||
|
return null;
|
||||||
|
values[currentOctet++] = (byte) currentValue;
|
||||||
|
currentValue = 0;
|
||||||
|
numDigits = 0;
|
||||||
|
} else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/* Must have 4 octets. */
|
||||||
|
if (currentOctet != 3)
|
||||||
|
return null;
|
||||||
|
/* The fourth octet can't be empty. */
|
||||||
|
if (numDigits == 0)
|
||||||
|
return null;
|
||||||
|
values[currentOctet] = (byte) currentValue;
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
// org.xbill.DNS.Address.parseV6
|
||||||
|
@Nullable
|
||||||
|
private static byte[] parseV6(String s) {
|
||||||
|
int range = -1;
|
||||||
|
byte[] data = new byte[16];
|
||||||
|
|
||||||
|
String[] tokens = s.split(":", -1);
|
||||||
|
|
||||||
|
int first = 0;
|
||||||
|
int last = tokens.length - 1;
|
||||||
|
|
||||||
|
if (tokens[0].length() == 0) {
|
||||||
|
// If the first two tokens are empty, it means the string
|
||||||
|
// started with ::, which is fine. If only the first is
|
||||||
|
// empty, the string started with :, which is bad.
|
||||||
|
if (last - first > 0 && tokens[1].length() == 0)
|
||||||
|
first++;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens[last].length() == 0) {
|
||||||
|
// If the last two tokens are empty, it means the string
|
||||||
|
// ended with ::, which is fine. If only the last is
|
||||||
|
// empty, the string ended with :, which is bad.
|
||||||
|
if (last - first > 0 && tokens[last - 1].length() == 0)
|
||||||
|
last--;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last - first + 1 > 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int i, j;
|
||||||
|
for (i = first, j = 0; i <= last; i++) {
|
||||||
|
if (tokens[i].length() == 0) {
|
||||||
|
if (range >= 0)
|
||||||
|
return null;
|
||||||
|
range = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens[i].indexOf('.') >= 0) {
|
||||||
|
// An IPv4 address must be the last component
|
||||||
|
if (i < last)
|
||||||
|
return null;
|
||||||
|
// There can't have been more than 6 components.
|
||||||
|
if (i > 6)
|
||||||
|
return null;
|
||||||
|
byte[] v4addr = parseV4(tokens[i]);
|
||||||
|
if (v4addr == null)
|
||||||
|
return null;
|
||||||
|
for (int k = 0; k < 4; k++)
|
||||||
|
data[j++] = v4addr[k];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int k = 0; k < tokens[i].length(); k++) {
|
||||||
|
char c = tokens[i].charAt(k);
|
||||||
|
if (Character.digit(c, 16) < 0)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int x = Integer.parseInt(tokens[i], 16);
|
||||||
|
if (x > 0xFFFF || x < 0)
|
||||||
|
return null;
|
||||||
|
data[j++] = (byte) (x >>> 8);
|
||||||
|
data[j++] = (byte) (x & 0xFF);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j < 16 && range < 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (range >= 0) {
|
||||||
|
int empty = 16 - j;
|
||||||
|
System.arraycopy(data, range, data, range + empty, j - range);
|
||||||
|
for (i = range; i < range + empty; i++)
|
||||||
|
data[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a InetAddress with the host.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public List<InetAddress> get(String host) {
|
||||||
|
if (!isValidHost(host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[]{host})) {
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
String[] ips = SqlUtils.getString(cursor, COLUMN_IP, null).split("\\+");
|
||||||
|
List<InetAddress> addresses = new ArrayList<>();
|
||||||
|
for (String ip : ips) {
|
||||||
|
addresses.add(toInetAddress(host, ip));
|
||||||
|
}
|
||||||
|
return addresses;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(String host) {
|
||||||
|
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + " WHERE " + COLUMN_HOST + " = ?;", new String[]{host})) {
|
||||||
|
return cursor.moveToNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts the host-ip pair into this hosts.
|
||||||
|
*/
|
||||||
|
public boolean put(String host, String ip) {
|
||||||
|
if (!isValidHost(host) || !isValidIp(ip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COLUMN_HOST, host);
|
||||||
|
values.put(COLUMN_IP, ip);
|
||||||
|
|
||||||
|
if (contains(host)) {
|
||||||
|
db.update(TABLE_HOSTS, values, COLUMN_HOST + " = ?", new String[]{host});
|
||||||
|
} else {
|
||||||
|
db.insert(TABLE_HOSTS, null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts delete the entry with the host.
|
||||||
|
*/
|
||||||
|
public void delete(String host) {
|
||||||
|
db.delete(TABLE_HOSTS, COLUMN_HOST + " = ?", new String[]{host});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all data from this host.
|
||||||
|
*/
|
||||||
|
public List<Pair<String, String>> getAll() {
|
||||||
|
List<Pair<String, String>> result = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Cursor cursor = db.rawQuery("SELECT * FROM " + TABLE_HOSTS + ";", null)) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
String host = SqlUtils.getString(cursor, COLUMN_HOST, null);
|
||||||
|
String ip = SqlUtils.getString(cursor, COLUMN_IP, null);
|
||||||
|
result.add(new Pair<>(host, ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
+756
@@ -0,0 +1,756 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.DimenRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.client.EhConfig;
|
||||||
|
import com.hippo.ehviewer.client.data.FavListUrlBuilder;
|
||||||
|
import com.hippo.ehviewer.ui.CommonOperations;
|
||||||
|
import com.hippo.ehviewer.ui.scene.GalleryListScene;
|
||||||
|
import com.hippo.unifile.UniFile;
|
||||||
|
import com.hippo.yorozuya.AssertUtilsKt;
|
||||||
|
import com.hippo.yorozuya.NumberUtils;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class Settings {
|
||||||
|
|
||||||
|
/********************
|
||||||
|
****** Eh
|
||||||
|
********************/
|
||||||
|
|
||||||
|
public static final String KEY_THEME = "theme";
|
||||||
|
|
||||||
|
public static final String KEY_ACCOUNT = "account";
|
||||||
|
public static final String KEY_IMAGE_LIMITS = "image_limits";
|
||||||
|
public static final String KEY_U_CONFIG = "uconfig";
|
||||||
|
public static final String KEY_MY_TAGS = "mytags";
|
||||||
|
public static final String KEY_BLACK_DARK_THEME = "black_dark_theme";
|
||||||
|
public static final int THEME_SYSTEM = -1;
|
||||||
|
public static final String KEY_GALLERY_SITE = "gallery_site";
|
||||||
|
public static final String KEY_LIST_MODE = "list_mode";
|
||||||
|
public static final String KEY_DETAIL_SIZE = "detail_size";
|
||||||
|
public static final String KEY_THUMB_SIZE = "thumb_size_";
|
||||||
|
public static final String KEY_THUMB_RESOLUTION = "thumb_resolution";
|
||||||
|
public static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
|
||||||
|
public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations";
|
||||||
|
public static final String KEY_TAG_TRANSLATIONS_SOURCE = "tag_translations_source";
|
||||||
|
public static final String KEY_DEFAULT_CATEGORIES = "default_categories";
|
||||||
|
public static final int DEFAULT_DEFAULT_CATEGORIES = EhConfig.ALL_CATEGORY;
|
||||||
|
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
|
||||||
|
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
|
||||||
|
public static final String KEY_REQUEST_NEWS = "request_news";
|
||||||
|
public static final String KEY_REQUEST_NEWS_TIMER = "request_news_timer";
|
||||||
|
public static final String KEY_REQUEST_NEWS_TIMER_HOUR = "request_news_timer_hour";
|
||||||
|
public static final String KEY_REQUEST_NEWS_TIMER_MINUTE = "request_news_timer_minute";
|
||||||
|
public static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
|
||||||
|
public static final String[] SIGN_IN_REQUIRED = {
|
||||||
|
KEY_IMAGE_LIMITS,
|
||||||
|
KEY_GALLERY_SITE,
|
||||||
|
KEY_U_CONFIG,
|
||||||
|
KEY_MY_TAGS,
|
||||||
|
KEY_SHOW_JPN_TITLE,
|
||||||
|
KEY_REQUEST_NEWS,
|
||||||
|
KEY_REQUEST_NEWS_TIMER,
|
||||||
|
KEY_HIDE_HV_EVENTS
|
||||||
|
};
|
||||||
|
/********************
|
||||||
|
****** Privacy and Security
|
||||||
|
********************/
|
||||||
|
public static final String KEY_SEC_SECURITY = "enable_secure";
|
||||||
|
public static final boolean VALUE_SEC_SECURITY = false;
|
||||||
|
/********************
|
||||||
|
****** Download
|
||||||
|
********************/
|
||||||
|
public static final String KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme";
|
||||||
|
public static final String KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority";
|
||||||
|
public static final String KEY_DOWNLOAD_SAVE_PATH = "image_path";
|
||||||
|
public static final String KEY_NOTIFICATION_REQUIRED = "notification_required";
|
||||||
|
public static final String KEY_DOWNLOAD_SAVE_QUERY = "image_query";
|
||||||
|
public static final String KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment";
|
||||||
|
public static final String KEY_MEDIA_SCAN = "media_scan";
|
||||||
|
public static final String KEY_IMAGE_RESOLUTION = "image_size";
|
||||||
|
public static final String DEFAULT_IMAGE_RESOLUTION = EhConfig.IMAGE_SIZE_AUTO;
|
||||||
|
public static final int INVALID_DEFAULT_FAV_SLOT = -2;
|
||||||
|
/********************
|
||||||
|
****** Advanced
|
||||||
|
********************/
|
||||||
|
public static final String KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body";
|
||||||
|
public static final String KEY_SECURITY = "require_unlock";
|
||||||
|
public static final String KEY_SECURITY_DELAY = "require_unlock_delay";
|
||||||
|
public static final String KEY_READ_CACHE_SIZE = "read_cache_size";
|
||||||
|
public static final int DEFAULT_READ_CACHE_SIZE = 320;
|
||||||
|
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
|
||||||
|
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
|
||||||
|
public static final String KEY_BYPASS_VPN = "bypass_vpn";
|
||||||
|
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
|
||||||
|
private static final boolean DEFAULT_HIDE_HV_EVENTS = false;
|
||||||
|
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
|
||||||
|
private static final boolean DEFAULT_SHOW_COMMENTS = true;
|
||||||
|
private static final String TAG = Settings.class.getSimpleName();
|
||||||
|
private static final String KEY_DISPLAY_NAME = "display_name";
|
||||||
|
private static final String DEFAULT_DISPLAY_NAME = null;
|
||||||
|
private static final int DEFAULT_LIST_THUMB_SIZE = 40;
|
||||||
|
private static final String KEY_AVATAR = "avatar";
|
||||||
|
private static final String KEY_REMOVE_IMAGE_FILES = "include_pic";
|
||||||
|
private static final boolean DEFAULT_REMOVE_IMAGE_FILES = true;
|
||||||
|
private static final String KEY_NEED_SIGN_IN = "need_sign_in";
|
||||||
|
private static final boolean DEFAULT_NEED_SIGN_IN = true;
|
||||||
|
private static final String KEY_SELECT_SITE = "select_site";
|
||||||
|
private static final boolean DEFAULT_SELECT_SITE = true;
|
||||||
|
private static final int DEFAULT_THEME = THEME_SYSTEM;
|
||||||
|
private static final int DEFAULT_GALLERY_SITE = 1;
|
||||||
|
private static final String KEY_LAUNCH_PAGE = "launch_page";
|
||||||
|
private static final int DEFAULT_LAUNCH_PAGE = 0;
|
||||||
|
private static final int DEFAULT_LIST_MODE = 0;
|
||||||
|
private static final int DEFAULT_DETAIL_SIZE = 0;
|
||||||
|
private static final int DEFAULT_THUMB_SIZE = 120;
|
||||||
|
private static final int DEFAULT_THUMB_RESOLUTION = 0;
|
||||||
|
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
|
||||||
|
private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages";
|
||||||
|
private static final boolean DEFAULT_SHOW_GALLERY_PAGES = false;
|
||||||
|
private static final boolean DEFAULT_SHOW_TAG_TRANSLATIONS = false;
|
||||||
|
private static final int DEFAULT_EXCLUDED_TAG_NAMESPACES = 0;
|
||||||
|
private static final String DEFAULT_EXCLUDED_LANGUAGES = null;
|
||||||
|
private static final String KEY_METERED_NETWORK_WARNING = "cellular_network_warning";
|
||||||
|
private static final boolean DEFAULT_METERED_NETWORK_WARNING = false;
|
||||||
|
private static final String KEY_APP_LINK_VERIFY_TIP = "app_link_verify_tip";
|
||||||
|
private static final boolean DEFAULT_APP_LINK_VERIFY_TIP = false;
|
||||||
|
private static final boolean DEFAULT_MEDIA_SCAN = false;
|
||||||
|
private static final String KEY_RECENT_DOWNLOAD_LABEL = "recent_download_label";
|
||||||
|
private static final String DEFAULT_RECENT_DOWNLOAD_LABEL = null;
|
||||||
|
private static final String KEY_HAS_DEFAULT_DOWNLOAD_LABEL = "has_default_download_label";
|
||||||
|
private static final boolean DEFAULT_HAS_DOWNLOAD_LABEL = false;
|
||||||
|
private static final String KEY_DEFAULT_DOWNLOAD_LABEL = "default_download_label";
|
||||||
|
private static final String DEFAULT_DOWNLOAD_LABEL = null;
|
||||||
|
private static final String KEY_MULTI_THREAD_DOWNLOAD = "download_thread";
|
||||||
|
private static final int DEFAULT_MULTI_THREAD_DOWNLOAD = 3;
|
||||||
|
private static final String KEY_PRELOAD_IMAGE = "preload_image";
|
||||||
|
private static final int DEFAULT_PRELOAD_IMAGE = 5;
|
||||||
|
private static final String KEY_DOWNLOAD_ORIGIN_IMAGE = "download_origin_image";
|
||||||
|
private static final boolean DEFAULT_DOWNLOAD_ORIGIN_IMAGE = false;
|
||||||
|
/********************
|
||||||
|
****** Favorites
|
||||||
|
********************/
|
||||||
|
private static final String KEY_FAV_CAT_0 = "fav_cat_0";
|
||||||
|
private static final String KEY_FAV_CAT_1 = "fav_cat_1";
|
||||||
|
private static final String KEY_FAV_CAT_2 = "fav_cat_2";
|
||||||
|
private static final String KEY_FAV_CAT_3 = "fav_cat_3";
|
||||||
|
private static final String KEY_FAV_CAT_4 = "fav_cat_4";
|
||||||
|
private static final String KEY_FAV_CAT_5 = "fav_cat_5";
|
||||||
|
private static final String KEY_FAV_CAT_6 = "fav_cat_6";
|
||||||
|
private static final String KEY_FAV_CAT_7 = "fav_cat_7";
|
||||||
|
private static final String KEY_FAV_CAT_8 = "fav_cat_8";
|
||||||
|
private static final String KEY_FAV_CAT_9 = "fav_cat_9";
|
||||||
|
private static final String DEFAULT_FAV_CAT_0 = "Favorites 0";
|
||||||
|
private static final String DEFAULT_FAV_CAT_1 = "Favorites 1";
|
||||||
|
private static final String DEFAULT_FAV_CAT_2 = "Favorites 2";
|
||||||
|
private static final String DEFAULT_FAV_CAT_3 = "Favorites 3";
|
||||||
|
private static final String DEFAULT_FAV_CAT_4 = "Favorites 4";
|
||||||
|
private static final String DEFAULT_FAV_CAT_5 = "Favorites 5";
|
||||||
|
private static final String DEFAULT_FAV_CAT_6 = "Favorites 6";
|
||||||
|
private static final String DEFAULT_FAV_CAT_7 = "Favorites 7";
|
||||||
|
private static final String DEFAULT_FAV_CAT_8 = "Favorites 8";
|
||||||
|
private static final String DEFAULT_FAV_CAT_9 = "Favorites 9";
|
||||||
|
private static final String KEY_FAV_COUNT_0 = "fav_count_0";
|
||||||
|
private static final String KEY_FAV_COUNT_1 = "fav_count_1";
|
||||||
|
private static final String KEY_FAV_COUNT_2 = "fav_count_2";
|
||||||
|
private static final String KEY_FAV_COUNT_3 = "fav_count_3";
|
||||||
|
private static final String KEY_FAV_COUNT_4 = "fav_count_4";
|
||||||
|
private static final String KEY_FAV_COUNT_5 = "fav_count_5";
|
||||||
|
private static final String KEY_FAV_COUNT_6 = "fav_count_6";
|
||||||
|
private static final String KEY_FAV_COUNT_7 = "fav_count_7";
|
||||||
|
private static final String KEY_FAV_COUNT_8 = "fav_count_8";
|
||||||
|
private static final String KEY_FAV_COUNT_9 = "fav_count_9";
|
||||||
|
private static final String KEY_FAV_LOCAL = "fav_local";
|
||||||
|
private static final String KEY_FAV_CLOUD = "fav_cloud";
|
||||||
|
private static final int DEFAULT_FAV_COUNT = 0;
|
||||||
|
private static final String KEY_RECENT_FAV_CAT = "recent_fav_cat";
|
||||||
|
private static final int DEFAULT_RECENT_FAV_CAT = FavListUrlBuilder.FAV_CAT_ALL;
|
||||||
|
// -1 for local, 0 - 9 for cloud favorite, other for no default fav slot
|
||||||
|
private static final String KEY_DEFAULT_FAV_SLOT = "default_favorite_2";
|
||||||
|
private static final int DEFAULT_DEFAULT_FAV_SLOT = INVALID_DEFAULT_FAV_SLOT;
|
||||||
|
private static final boolean DEFAULT_SAVE_PARSE_ERROR_BODY = true;
|
||||||
|
private static final String KEY_SAVE_CRASH_LOG = "save_crash_log";
|
||||||
|
private static final boolean DEFAULT_SAVE_CRASH_LOG = true;
|
||||||
|
private static final boolean DEFAULT_BUILT_IN_HOSTS = false;
|
||||||
|
private static final boolean DEFAULT_FRONTING = false;
|
||||||
|
private static final boolean DEFAULT_BYPASS_VPN = true;
|
||||||
|
private static final String KEY_PROXY_TYPE = "proxy_type";
|
||||||
|
private static final int DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM;
|
||||||
|
private static final String KEY_PROXY_IP = "proxy_ip";
|
||||||
|
private static final String DEFAULT_PROXY_IP = null;
|
||||||
|
private static final String KEY_PROXY_PORT = "proxy_port";
|
||||||
|
private static final int DEFAULT_PROXY_PORT = -1;
|
||||||
|
private static final String KEY_CLIPBOARD_TEXT_HASH_CODE = "clipboard_text_hash_code";
|
||||||
|
private static final int DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0;
|
||||||
|
private static final String KEY_DOWNLOAD_DELAY = "download_delay";
|
||||||
|
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
|
||||||
|
private static final boolean DEFAULT_REQUEST_NEWS = false;
|
||||||
|
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
|
||||||
|
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
|
||||||
|
private static final boolean DEFAULT_QS_SAVE_PROGRESS = true;
|
||||||
|
public static boolean LIST_THUMB_SIZE_INITED = false;
|
||||||
|
public static SharedPreferences sSettingsPre;
|
||||||
|
private static int LIST_THUMB_SIZE = 40;
|
||||||
|
private static EhConfig sEhConfig;
|
||||||
|
|
||||||
|
public static void initialize() {
|
||||||
|
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(EhApplication.getApplication());
|
||||||
|
sEhConfig = loadEhConfig();
|
||||||
|
fixDefaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fixDefaultValue() {
|
||||||
|
if ("CN".equals(Locale.getDefault().getCountry())) {
|
||||||
|
// Enable domain fronting if the country is CN
|
||||||
|
if (!sSettingsPre.contains(KEY_BUILT_IN_HOSTS)) {
|
||||||
|
putBuiltInHosts(true);
|
||||||
|
}
|
||||||
|
if (!sSettingsPre.contains(KEY_DOMAIN_FRONTING)) {
|
||||||
|
putDF(true);
|
||||||
|
}
|
||||||
|
// Enable show tag translations if the country is CN
|
||||||
|
if (!sSettingsPre.contains(KEY_SHOW_TAG_TRANSLATIONS)) {
|
||||||
|
putShowTagTranslations(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EhConfig loadEhConfig() {
|
||||||
|
EhConfig ehConfig = new EhConfig();
|
||||||
|
ehConfig.imageSize = getImageResolution();
|
||||||
|
ehConfig.excludedLanguages = getExcludedLanguages();
|
||||||
|
ehConfig.defaultCategories = getDefaultCategories();
|
||||||
|
ehConfig.excludedNamespaces = getExcludedTagNamespaces();
|
||||||
|
ehConfig.setDirty();
|
||||||
|
return ehConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getBoolean(String key, boolean defValue) {
|
||||||
|
try {
|
||||||
|
return sSettingsPre.getBoolean(key, defValue);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putBoolean(String key, boolean value) {
|
||||||
|
sSettingsPre.edit().putBoolean(key, value).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getInt(String key, int defValue) {
|
||||||
|
try {
|
||||||
|
return sSettingsPre.getInt(key, defValue);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putInt(String key, int value) {
|
||||||
|
sSettingsPre.edit().putInt(key, value).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getString(String key, String defValue) {
|
||||||
|
try {
|
||||||
|
return sSettingsPre.getString(key, defValue);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putString(String key, String value) {
|
||||||
|
sSettingsPre.edit().putString(key, value).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> getStringSet(String key) {
|
||||||
|
return sSettingsPre.getStringSet(key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putStringToStringSet(String key, String value) {
|
||||||
|
Set<String> set = getStringSet(key);
|
||||||
|
if (set == null)
|
||||||
|
set = Set.of(value);
|
||||||
|
else if (set.contains(value))
|
||||||
|
return;
|
||||||
|
else
|
||||||
|
set.add(value);
|
||||||
|
sSettingsPre.edit().putStringSet(key, set).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getIntFromStr(String key, int defValue) {
|
||||||
|
try {
|
||||||
|
return NumberUtils.parseIntSafely(sSettingsPre.getString(key, Integer.toString(defValue)), defValue);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
|
||||||
|
return defValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putIntToStr(String key, int value) {
|
||||||
|
sSettingsPre.edit().putString(key, Integer.toString(value)).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static String getDisplayName() {
|
||||||
|
return getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putDisplayName(String value) {
|
||||||
|
putString(KEY_DISPLAY_NAME, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putAvatar(String value) {
|
||||||
|
putString(KEY_AVATAR, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getRemoveImageFiles() {
|
||||||
|
return getBoolean(KEY_REMOVE_IMAGE_FILES, DEFAULT_REMOVE_IMAGE_FILES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putRemoveImageFiles(boolean value) {
|
||||||
|
putBoolean(KEY_REMOVE_IMAGE_FILES, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EhConfig getEhConfig() {
|
||||||
|
return sEhConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getNeedSignIn() {
|
||||||
|
return getBoolean(KEY_NEED_SIGN_IN, DEFAULT_NEED_SIGN_IN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putNeedSignIn(boolean value) {
|
||||||
|
putBoolean(KEY_NEED_SIGN_IN, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getSelectSite() {
|
||||||
|
return getBoolean(KEY_SELECT_SITE, DEFAULT_SELECT_SITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putSelectSite(boolean value) {
|
||||||
|
putBoolean(KEY_SELECT_SITE, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getTheme() {
|
||||||
|
return getIntFromStr(KEY_THEME, DEFAULT_THEME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getGallerySite() {
|
||||||
|
return getIntFromStr(KEY_GALLERY_SITE, DEFAULT_GALLERY_SITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putGallerySite(int value) {
|
||||||
|
putIntToStr(KEY_GALLERY_SITE, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getLaunchPageGalleryListSceneAction() {
|
||||||
|
int value = getIntFromStr(KEY_LAUNCH_PAGE, DEFAULT_LAUNCH_PAGE);
|
||||||
|
return switch (value) {
|
||||||
|
case 0 -> GalleryListScene.ACTION_HOMEPAGE;
|
||||||
|
case 1 -> GalleryListScene.ACTION_SUBSCRIPTION;
|
||||||
|
case 2 -> GalleryListScene.ACTION_WHATS_HOT;
|
||||||
|
case 3 -> GalleryListScene.ACTION_TOP_LIST;
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getListMode() {
|
||||||
|
return getIntFromStr(KEY_LIST_MODE, DEFAULT_LIST_MODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getDetailSize() {
|
||||||
|
return getIntFromStr(KEY_DETAIL_SIZE, DEFAULT_DETAIL_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DimenRes
|
||||||
|
public static int getDetailSizeResId() {
|
||||||
|
return switch (getDetailSize()) {
|
||||||
|
case 0 -> R.dimen.gallery_list_column_width_long;
|
||||||
|
case 1 -> R.dimen.gallery_list_column_width_short;
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + getDetailSize());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getThumbSize() {
|
||||||
|
return dip2px(getInt(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int dip2px(int dpValue) {
|
||||||
|
final float scale = EhApplication.getApplication().getResources().getDisplayMetrics().density;
|
||||||
|
return (int) (dpValue * scale + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getThumbResolution() {
|
||||||
|
return getIntFromStr(KEY_THUMB_RESOLUTION, DEFAULT_THUMB_RESOLUTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getShowComments() {
|
||||||
|
return getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getRequestNews() {
|
||||||
|
return getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getHideHvEvents() {
|
||||||
|
return getBoolean(KEY_HIDE_HV_EVENTS, DEFAULT_HIDE_HV_EVENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getShowJpnTitle() {
|
||||||
|
return getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getShowGalleryPages() {
|
||||||
|
return getBoolean(KEY_SHOW_GALLERY_PAGES, DEFAULT_SHOW_GALLERY_PAGES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getShowTagTranslations() {
|
||||||
|
return getBoolean(KEY_SHOW_TAG_TRANSLATIONS, DEFAULT_SHOW_TAG_TRANSLATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putShowTagTranslations(boolean value) {
|
||||||
|
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getDefaultCategories() {
|
||||||
|
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getExcludedTagNamespaces() {
|
||||||
|
return getInt(KEY_EXCLUDED_TAG_NAMESPACES, DEFAULT_EXCLUDED_TAG_NAMESPACES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getExcludedLanguages() {
|
||||||
|
return getString(KEY_EXCLUDED_LANGUAGES, DEFAULT_EXCLUDED_LANGUAGES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getMeteredNetworkWarning() {
|
||||||
|
return getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getAppLinkVerifyTip() {
|
||||||
|
return getBoolean(KEY_APP_LINK_VERIFY_TIP, DEFAULT_APP_LINK_VERIFY_TIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putAppLinkVerifyTip(boolean value) {
|
||||||
|
putBoolean(KEY_APP_LINK_VERIFY_TIP, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getEnabledSecurity() {
|
||||||
|
return getBoolean(KEY_SEC_SECURITY, VALUE_SEC_SECURITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static UniFile getDownloadLocation() {
|
||||||
|
UniFile dir;
|
||||||
|
Uri.Builder builder = new Uri.Builder();
|
||||||
|
builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null));
|
||||||
|
builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null));
|
||||||
|
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null));
|
||||||
|
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null));
|
||||||
|
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null));
|
||||||
|
dir = UniFile.fromUri(EhApplication.getApplication(), builder.build());
|
||||||
|
return dir != null ? dir : UniFile.fromFile(AppConfig.getDefaultDownloadDir());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putDownloadLocation(@NonNull UniFile location) {
|
||||||
|
Uri uri = location.getUri();
|
||||||
|
putString(KEY_DOWNLOAD_SAVE_SCHEME, uri.getScheme());
|
||||||
|
putString(KEY_DOWNLOAD_SAVE_AUTHORITY, uri.getEncodedAuthority());
|
||||||
|
putString(KEY_DOWNLOAD_SAVE_PATH, uri.getEncodedPath());
|
||||||
|
putString(KEY_DOWNLOAD_SAVE_QUERY, uri.getEncodedQuery());
|
||||||
|
putString(KEY_DOWNLOAD_SAVE_FRAGMENT, uri.getEncodedFragment());
|
||||||
|
|
||||||
|
if (getMediaScan()) {
|
||||||
|
CommonOperations.removeNoMediaFile(location);
|
||||||
|
} else {
|
||||||
|
CommonOperations.ensureNoMediaFile(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getMediaScan() {
|
||||||
|
return getBoolean(KEY_MEDIA_SCAN, DEFAULT_MEDIA_SCAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getRecentDownloadLabel() {
|
||||||
|
return getString(KEY_RECENT_DOWNLOAD_LABEL, DEFAULT_RECENT_DOWNLOAD_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putRecentDownloadLabel(String value) {
|
||||||
|
putString(KEY_RECENT_DOWNLOAD_LABEL, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getHasDefaultDownloadLabel() {
|
||||||
|
return getBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, DEFAULT_HAS_DOWNLOAD_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putHasDefaultDownloadLabel(boolean hasDefaultDownloadLabel) {
|
||||||
|
putBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, hasDefaultDownloadLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDefaultDownloadLabel() {
|
||||||
|
return getString(KEY_DEFAULT_DOWNLOAD_LABEL, DEFAULT_DOWNLOAD_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putDefaultDownloadLabel(String value) {
|
||||||
|
putString(KEY_DEFAULT_DOWNLOAD_LABEL, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getMultiThreadDownload() {
|
||||||
|
return getIntFromStr(KEY_MULTI_THREAD_DOWNLOAD, DEFAULT_MULTI_THREAD_DOWNLOAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getDownloadDelay() {
|
||||||
|
return getIntFromStr(KEY_DOWNLOAD_DELAY, DEFAULT_DOWNLOAD_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getPreloadImage() {
|
||||||
|
return getIntFromStr(KEY_PRELOAD_IMAGE, DEFAULT_PRELOAD_IMAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getImageResolution() {
|
||||||
|
return getString(KEY_IMAGE_RESOLUTION, DEFAULT_IMAGE_RESOLUTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putImageResolution(String value) {
|
||||||
|
sEhConfig.imageSize = value;
|
||||||
|
sEhConfig.setDirty();
|
||||||
|
putString(KEY_IMAGE_RESOLUTION, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getDownloadOriginImage() {
|
||||||
|
return getBoolean(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] getFavCat() {
|
||||||
|
String[] favCat = new String[10];
|
||||||
|
favCat[0] = sSettingsPre.getString(KEY_FAV_CAT_0, DEFAULT_FAV_CAT_0);
|
||||||
|
favCat[1] = sSettingsPre.getString(KEY_FAV_CAT_1, DEFAULT_FAV_CAT_1);
|
||||||
|
favCat[2] = sSettingsPre.getString(KEY_FAV_CAT_2, DEFAULT_FAV_CAT_2);
|
||||||
|
favCat[3] = sSettingsPre.getString(KEY_FAV_CAT_3, DEFAULT_FAV_CAT_3);
|
||||||
|
favCat[4] = sSettingsPre.getString(KEY_FAV_CAT_4, DEFAULT_FAV_CAT_4);
|
||||||
|
favCat[5] = sSettingsPre.getString(KEY_FAV_CAT_5, DEFAULT_FAV_CAT_5);
|
||||||
|
favCat[6] = sSettingsPre.getString(KEY_FAV_CAT_6, DEFAULT_FAV_CAT_6);
|
||||||
|
favCat[7] = sSettingsPre.getString(KEY_FAV_CAT_7, DEFAULT_FAV_CAT_7);
|
||||||
|
favCat[8] = sSettingsPre.getString(KEY_FAV_CAT_8, DEFAULT_FAV_CAT_8);
|
||||||
|
favCat[9] = sSettingsPre.getString(KEY_FAV_CAT_9, DEFAULT_FAV_CAT_9);
|
||||||
|
return favCat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putFavCat(String[] value) {
|
||||||
|
AssertUtilsKt.assertEquals(10, value.length);
|
||||||
|
sSettingsPre.edit()
|
||||||
|
.putString(KEY_FAV_CAT_0, value[0])
|
||||||
|
.putString(KEY_FAV_CAT_1, value[1])
|
||||||
|
.putString(KEY_FAV_CAT_2, value[2])
|
||||||
|
.putString(KEY_FAV_CAT_3, value[3])
|
||||||
|
.putString(KEY_FAV_CAT_4, value[4])
|
||||||
|
.putString(KEY_FAV_CAT_5, value[5])
|
||||||
|
.putString(KEY_FAV_CAT_6, value[6])
|
||||||
|
.putString(KEY_FAV_CAT_7, value[7])
|
||||||
|
.putString(KEY_FAV_CAT_8, value[8])
|
||||||
|
.putString(KEY_FAV_CAT_9, value[9])
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int[] getFavCount() {
|
||||||
|
int[] favCount = new int[10];
|
||||||
|
favCount[0] = sSettingsPre.getInt(KEY_FAV_COUNT_0, DEFAULT_FAV_COUNT);
|
||||||
|
favCount[1] = sSettingsPre.getInt(KEY_FAV_COUNT_1, DEFAULT_FAV_COUNT);
|
||||||
|
favCount[2] = sSettingsPre.getInt(KEY_FAV_COUNT_2, DEFAULT_FAV_COUNT);
|
||||||
|
favCount[3] = sSettingsPre.getInt(KEY_FAV_COUNT_3, DEFAULT_FAV_COUNT);
|
||||||
|
favCount[4] = sSettingsPre.getInt(KEY_FAV_COUNT_4, DEFAULT_FAV_COUNT);
|
||||||
|
favCount[5] = sSettingsPre.getInt(KEY_FAV_COUNT_5, DEFAULT_FAV_COUNT);
|
||||||
|
favCount[6] = sSettingsPre.getInt(KEY_FAV_COUNT_6, DEFAULT_FAV_COUNT);
|
||||||
|
favCount[7] = sSettingsPre.getInt(KEY_FAV_COUNT_7, DEFAULT_FAV_COUNT);
|
||||||
|
favCount[8] = sSettingsPre.getInt(KEY_FAV_COUNT_8, DEFAULT_FAV_COUNT);
|
||||||
|
favCount[9] = sSettingsPre.getInt(KEY_FAV_COUNT_9, DEFAULT_FAV_COUNT);
|
||||||
|
return favCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putFavCount(int[] count) {
|
||||||
|
AssertUtilsKt.assertEquals(10, count.length);
|
||||||
|
sSettingsPre.edit()
|
||||||
|
.putInt(KEY_FAV_COUNT_0, count[0])
|
||||||
|
.putInt(KEY_FAV_COUNT_1, count[1])
|
||||||
|
.putInt(KEY_FAV_COUNT_2, count[2])
|
||||||
|
.putInt(KEY_FAV_COUNT_3, count[3])
|
||||||
|
.putInt(KEY_FAV_COUNT_4, count[4])
|
||||||
|
.putInt(KEY_FAV_COUNT_5, count[5])
|
||||||
|
.putInt(KEY_FAV_COUNT_6, count[6])
|
||||||
|
.putInt(KEY_FAV_COUNT_7, count[7])
|
||||||
|
.putInt(KEY_FAV_COUNT_8, count[8])
|
||||||
|
.putInt(KEY_FAV_COUNT_9, count[9])
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getFavLocalCount() {
|
||||||
|
return sSettingsPre.getInt(KEY_FAV_LOCAL, DEFAULT_FAV_COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putFavLocalCount(int count) {
|
||||||
|
sSettingsPre.edit().putInt(KEY_FAV_LOCAL, count).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getFavCloudCount() {
|
||||||
|
return sSettingsPre.getInt(KEY_FAV_CLOUD, DEFAULT_FAV_COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putFavCloudCount(int count) {
|
||||||
|
sSettingsPre.edit().putInt(KEY_FAV_CLOUD, count).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getRecentFavCat() {
|
||||||
|
return getInt(KEY_RECENT_FAV_CAT, DEFAULT_RECENT_FAV_CAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putRecentFavCat(int value) {
|
||||||
|
putInt(KEY_RECENT_FAV_CAT, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getDefaultFavSlot() {
|
||||||
|
return getInt(KEY_DEFAULT_FAV_SLOT, DEFAULT_DEFAULT_FAV_SLOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putDefaultFavSlot(int value) {
|
||||||
|
putInt(KEY_DEFAULT_FAV_SLOT, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getQSSaveProgress() {
|
||||||
|
return getBoolean(KEY_QS_SAVE_PROGRESS, DEFAULT_QS_SAVE_PROGRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putQSSaveProgress(boolean value) {
|
||||||
|
putBoolean(KEY_QS_SAVE_PROGRESS, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getSaveParseErrorBody() {
|
||||||
|
return getBoolean(KEY_SAVE_PARSE_ERROR_BODY, DEFAULT_SAVE_PARSE_ERROR_BODY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getSaveCrashLog() {
|
||||||
|
return getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getSecurity() {
|
||||||
|
return getBoolean(KEY_SECURITY, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putSecurity(boolean value) {
|
||||||
|
putBoolean(KEY_SECURITY, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getSecurityDelay() {
|
||||||
|
return getInt(KEY_SECURITY_DELAY, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getReadCacheSize() {
|
||||||
|
return getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getBuiltInHosts() {
|
||||||
|
return getBoolean(KEY_BUILT_IN_HOSTS, DEFAULT_BUILT_IN_HOSTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putBuiltInHosts(boolean value) {
|
||||||
|
putBoolean(KEY_BUILT_IN_HOSTS, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getDF() {
|
||||||
|
return getBoolean(KEY_DOMAIN_FRONTING, DEFAULT_FRONTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putDF(boolean value) {
|
||||||
|
putBoolean(KEY_DOMAIN_FRONTING, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getBypassVpn() {
|
||||||
|
return getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getProxyType() {
|
||||||
|
return getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putProxyType(int value) {
|
||||||
|
putInt(KEY_PROXY_TYPE, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getProxyIp() {
|
||||||
|
return getString(KEY_PROXY_IP, DEFAULT_PROXY_IP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putProxyIp(String value) {
|
||||||
|
putString(KEY_PROXY_IP, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getProxyPort() {
|
||||||
|
return getInt(KEY_PROXY_PORT, DEFAULT_PROXY_PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putProxyPort(int value) {
|
||||||
|
putInt(KEY_PROXY_PORT, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getClipboardTextHashCode() {
|
||||||
|
return getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putClipboardTextHashCode(int value) {
|
||||||
|
putInt(KEY_CLIPBOARD_TEXT_HASH_CODE, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> getArchivePasswds() {
|
||||||
|
return getStringSet(KEY_ARCHIVE_PASSWDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putPasswdToArchivePasswds(String value) {
|
||||||
|
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getListThumbSize() {
|
||||||
|
if (LIST_THUMB_SIZE_INITED) {
|
||||||
|
return LIST_THUMB_SIZE;
|
||||||
|
}
|
||||||
|
int size = 3 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE);
|
||||||
|
LIST_THUMB_SIZE = size;
|
||||||
|
LIST_THUMB_SIZE_INITED = true;
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean getNotificationRequired() {
|
||||||
|
return getBoolean(KEY_NOTIFICATION_REQUIRED, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putNotificationRequired() {
|
||||||
|
putBoolean(KEY_NOTIFICATION_REQUIRED, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent;
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.client.data.GalleryDetail;
|
||||||
|
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||||
|
|
||||||
|
public final class UrlOpener {
|
||||||
|
|
||||||
|
private UrlOpener() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openUrl(@NonNull Context context, String url, boolean ehUrl) {
|
||||||
|
openUrl(context, url, ehUrl, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openUrl(@NonNull Context context, String url, boolean ehUrl, GalleryDetail galleryDetail) {
|
||||||
|
if (TextUtils.isEmpty(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent intent;
|
||||||
|
Uri uri = Uri.parse(url);
|
||||||
|
|
||||||
|
if (ehUrl) {
|
||||||
|
if (galleryDetail != null) {
|
||||||
|
GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(url);
|
||||||
|
if (result != null) {
|
||||||
|
if (result.gid == galleryDetail.getGid()) {
|
||||||
|
intent = new Intent(context, ReaderActivity.class);
|
||||||
|
intent.setAction(ReaderActivity.ACTION_EH);
|
||||||
|
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail);
|
||||||
|
intent.putExtra(ReaderActivity.KEY_PAGE, result.page);
|
||||||
|
context.startActivity(intent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (url.startsWith("#c")) {
|
||||||
|
try {
|
||||||
|
intent = new Intent(context, ReaderActivity.class);
|
||||||
|
intent.setAction(ReaderActivity.ACTION_EH);
|
||||||
|
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail);
|
||||||
|
intent.putExtra(ReaderActivity.KEY_PAGE, Integer.parseInt(url.replace("#c", "")) - 1);
|
||||||
|
context.startActivity(intent);
|
||||||
|
return;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder();
|
||||||
|
customTabsIntent.setShowTitle(true);
|
||||||
|
try {
|
||||||
|
customTabsIntent.build().launchUrl(context, uri);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Toast.makeText(context, R.string.no_browser_installed, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Tarsin Norbin
|
||||||
|
*
|
||||||
|
* This file is part of EhViewer
|
||||||
|
*
|
||||||
|
* EhViewer is free software: you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* EhViewer is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.hippo.ehviewer;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.view.WindowInsetsAnimationCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
|
import com.hippo.yorozuya.MathUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WindowInsetsAnimationHelper extends WindowInsetsAnimationCompat.Callback {
|
||||||
|
private final View[] views;
|
||||||
|
private final HashMap<View, Integer> startPaddings = new HashMap<>();
|
||||||
|
private final HashMap<View, Integer> endPaddings = new HashMap<>();
|
||||||
|
WindowInsetsAnimationCompat animation;
|
||||||
|
|
||||||
|
public WindowInsetsAnimationHelper(int dispatchMode, View... views) {
|
||||||
|
super(dispatchMode);
|
||||||
|
this.views = views;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepare(@NonNull WindowInsetsAnimationCompat animation) {
|
||||||
|
super.onPrepare(animation);
|
||||||
|
this.animation = animation;
|
||||||
|
for (View view : views) {
|
||||||
|
if (view == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
startPaddings.put(view, view.getPaddingBottom());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public WindowInsetsAnimationCompat.BoundsCompat onStart(@NonNull WindowInsetsAnimationCompat animation, @NonNull WindowInsetsAnimationCompat.BoundsCompat bounds) {
|
||||||
|
this.animation = animation;
|
||||||
|
for (View view : views) {
|
||||||
|
if (view == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
endPaddings.put(view, view.getPaddingBottom());
|
||||||
|
int startPadding = startPaddings.containsKey(view) ? startPaddings.get(view) : 0;
|
||||||
|
view.setTranslationY(-(startPadding - view.getPaddingBottom()));
|
||||||
|
}
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets, @NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
|
||||||
|
if (animation == null) {
|
||||||
|
return insets;
|
||||||
|
}
|
||||||
|
WindowInsetsAnimationCompat imeAnimation = null;
|
||||||
|
for (WindowInsetsAnimationCompat animation : runningAnimations) {
|
||||||
|
if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
|
||||||
|
imeAnimation = animation;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imeAnimation != null) {
|
||||||
|
for (View view : views) {
|
||||||
|
if (view == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int startPadding = startPaddings.containsKey(view) ? startPaddings.get(view) : 0;
|
||||||
|
int endPadding = endPaddings.containsKey(view) ? endPaddings.get(view) : 0;
|
||||||
|
view.setTranslationY(MathUtils.lerp(endPadding - startPadding, 0, animation.getInterpolatedFraction()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return insets;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnd(@NonNull WindowInsetsAnimationCompat animation) {
|
||||||
|
super.onEnd(animation);
|
||||||
|
startPaddings.clear();
|
||||||
|
endPaddings.clear();
|
||||||
|
this.animation = null;
|
||||||
|
for (View view : views) {
|
||||||
|
if (view == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
view.setTranslationY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-8
@@ -13,12 +13,10 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.ehviewer.core.model
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
object EhCacheKeyFactory {
|
||||||
|
fun getImageKey(gid: Long, index: Int): String {
|
||||||
@Serializable
|
return "image:$gid:$index"
|
||||||
data class GalleryTagGroup(
|
}
|
||||||
val namespace: TagNamespace,
|
}
|
||||||
val tags: List<GalleryTag>,
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object EhClient {
|
||||||
|
internal fun enqueue(request: EhRequest, scope: CoroutineScope) {
|
||||||
|
check(!request.isActive) // Abort if attempt to execute an active request
|
||||||
|
request.job = scope.launchIO {
|
||||||
|
val callback: Callback<Any?>? = request.callback
|
||||||
|
try {
|
||||||
|
val result: Any? = request.run {
|
||||||
|
if (args == null)
|
||||||
|
execute(method)
|
||||||
|
else
|
||||||
|
execute(method, *args!!)
|
||||||
|
}
|
||||||
|
withUIContext { callback?.onSuccess(result) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException)
|
||||||
|
throw e // Don't catch coroutine CancellationException
|
||||||
|
e.printStackTrace()
|
||||||
|
withUIContext { callback?.onFailure(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun execute(method: Int, vararg params: Any?): Any? {
|
||||||
|
return when (method) {
|
||||||
|
METHOD_SIGN_IN -> EhEngine.signIn(
|
||||||
|
params[0] as String,
|
||||||
|
params[1] as String
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_GALLERY_LIST -> EhEngine.getGalleryList(
|
||||||
|
params[0] as String
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_GALLERY_DETAIL -> EhEngine.getGalleryDetail(
|
||||||
|
params[0] as String?
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_PREVIEW_SET -> EhEngine.getPreviewSet(
|
||||||
|
params[0] as String?
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_RATE_GALLERY -> EhEngine.rateGallery(
|
||||||
|
params[0] as Long,
|
||||||
|
params[1] as String?,
|
||||||
|
params[2] as Long,
|
||||||
|
params[3] as String?,
|
||||||
|
params[4] as Float
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_COMMENT_GALLERY -> EhEngine.commentGallery(
|
||||||
|
params[0] as String?,
|
||||||
|
params[1] as String,
|
||||||
|
params[2] as String?
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_GALLERY_TOKEN -> EhEngine.getGalleryToken(
|
||||||
|
params[0] as Long,
|
||||||
|
params[1] as String?,
|
||||||
|
params[2] as Int
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_FAVORITES -> EhEngine.getFavorites(
|
||||||
|
params[0] as String
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_ADD_FAVORITES -> EhEngine.addFavorites(
|
||||||
|
params[0] as Long,
|
||||||
|
params[1] as String?,
|
||||||
|
params[2] as Int,
|
||||||
|
params[3] as String?
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_ADD_FAVORITES_RANGE -> @Suppress("UNCHECKED_CAST") EhEngine.addFavoritesRange(
|
||||||
|
params[0] as LongArray,
|
||||||
|
params[1] as Array<String?>,
|
||||||
|
params[2] as Int
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_MODIFY_FAVORITES -> EhEngine.modifyFavorites(
|
||||||
|
params[0] as String,
|
||||||
|
params[1] as LongArray,
|
||||||
|
params[2] as Int
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_TORRENT_LIST -> EhEngine.getTorrentList(
|
||||||
|
params[0] as String?,
|
||||||
|
params[1] as Long,
|
||||||
|
params[2] as String?
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_PROFILE -> EhEngine.getProfile()
|
||||||
|
METHOD_VOTE_COMMENT -> EhEngine.voteComment(
|
||||||
|
params[0] as Long,
|
||||||
|
params[1] as String?,
|
||||||
|
params[2] as Long,
|
||||||
|
params[3] as String?,
|
||||||
|
params[4] as Long,
|
||||||
|
params[5] as Int
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_IMAGE_SEARCH -> EhEngine.imageSearch(
|
||||||
|
params[0] as File,
|
||||||
|
params[1] as Boolean,
|
||||||
|
params[2] as Boolean,
|
||||||
|
params[3] as Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_ARCHIVE_LIST -> EhEngine.getArchiveList(
|
||||||
|
params[0] as String?,
|
||||||
|
params[1] as Long,
|
||||||
|
params[2] as String?
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_DOWNLOAD_ARCHIVE -> EhEngine.downloadArchive(
|
||||||
|
params[0] as Long,
|
||||||
|
params[1] as String?,
|
||||||
|
params[2] as String?,
|
||||||
|
params[3] as String?,
|
||||||
|
params[4] as Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_VOTE_TAG -> EhEngine.voteTag(
|
||||||
|
params[0] as Long,
|
||||||
|
params[1] as String?,
|
||||||
|
params[2] as Long,
|
||||||
|
params[3] as String?,
|
||||||
|
params[4] as String?,
|
||||||
|
params[5] as Int
|
||||||
|
)
|
||||||
|
|
||||||
|
METHOD_GET_UCONFIG -> EhEngine.getUConfig()
|
||||||
|
else -> throw IllegalStateException("Can't detect method $method")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback<E> {
|
||||||
|
fun onSuccess(result: E)
|
||||||
|
fun onFailure(e: Exception)
|
||||||
|
fun onCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
const val METHOD_SIGN_IN = 0
|
||||||
|
const val METHOD_GET_GALLERY_LIST = 1
|
||||||
|
const val METHOD_GET_GALLERY_DETAIL = 3
|
||||||
|
const val METHOD_GET_PREVIEW_SET = 4
|
||||||
|
const val METHOD_GET_RATE_GALLERY = 5
|
||||||
|
const val METHOD_GET_COMMENT_GALLERY = 6
|
||||||
|
const val METHOD_GET_GALLERY_TOKEN = 7
|
||||||
|
const val METHOD_GET_FAVORITES = 8
|
||||||
|
const val METHOD_ADD_FAVORITES = 9
|
||||||
|
const val METHOD_ADD_FAVORITES_RANGE = 10
|
||||||
|
const val METHOD_MODIFY_FAVORITES = 11
|
||||||
|
const val METHOD_GET_TORRENT_LIST = 12
|
||||||
|
const val METHOD_GET_PROFILE = 14
|
||||||
|
const val METHOD_VOTE_COMMENT = 15
|
||||||
|
const val METHOD_IMAGE_SEARCH = 16
|
||||||
|
const val METHOD_ARCHIVE_LIST = 17
|
||||||
|
const val METHOD_DOWNLOAD_ARCHIVE = 18
|
||||||
|
const val METHOD_VOTE_TAG = 19
|
||||||
|
const val METHOD_GET_UCONFIG = 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,806 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 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;
|
||||||
|
|
||||||
|
import com.hippo.network.InetValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some configurable stuff about EH. It ends up cookie.
|
||||||
|
*/
|
||||||
|
public class EhConfig implements Cloneable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Cookie key of uconfig
|
||||||
|
*/
|
||||||
|
public static final String KEY_UCONFIG = "uconfig";
|
||||||
|
/**
|
||||||
|
* The Cookie key of lofi resolution
|
||||||
|
*/
|
||||||
|
public static final String KEY_LOFI_RESOLUTION = "xres";
|
||||||
|
/**
|
||||||
|
* The Cookie key of show warning
|
||||||
|
*/
|
||||||
|
public static final String KEY_CONTENT_WARNING = "nw";
|
||||||
|
/**
|
||||||
|
* load images through the Hentai@Home Network
|
||||||
|
*/
|
||||||
|
public static final String LOAD_FROM_HAH_YES = "y";
|
||||||
|
/**
|
||||||
|
* do not load images through the Hentai@Home Network
|
||||||
|
*/
|
||||||
|
public static final String LOAD_FROM_HAH_NO = "n";
|
||||||
|
/**
|
||||||
|
* Image Size Auto
|
||||||
|
*/
|
||||||
|
public static final String IMAGE_SIZE_AUTO = "a";
|
||||||
|
/**
|
||||||
|
* Image Size 780x
|
||||||
|
*/
|
||||||
|
public static final String IMAGE_SIZE_780X = "780";
|
||||||
|
/**
|
||||||
|
* Image Size 980x
|
||||||
|
*/
|
||||||
|
public static final String IMAGE_SIZE_980X = "980";
|
||||||
|
/**
|
||||||
|
* Image Size 1280x
|
||||||
|
*/
|
||||||
|
public static final String IMAGE_SIZE_1280X = "1280";
|
||||||
|
/**
|
||||||
|
* Image Size 1600x
|
||||||
|
*/
|
||||||
|
public static final String IMAGE_SIZE_1600X = "1600";
|
||||||
|
/**
|
||||||
|
* Image Size 2400x
|
||||||
|
*/
|
||||||
|
public static final String IMAGE_SIZE_2400X = "2400";
|
||||||
|
/**
|
||||||
|
* Manual Accept, Manual Start
|
||||||
|
*/
|
||||||
|
public static final String ARCHIVER_DOWNLOAD_MAMS = "0";
|
||||||
|
/**
|
||||||
|
* >Manual Accept, Auto Start
|
||||||
|
*/
|
||||||
|
public static final String ARCHIVER_DOWNLOAD_AAMS = "1";
|
||||||
|
/**
|
||||||
|
* Auto Accept, Manual Start
|
||||||
|
*/
|
||||||
|
public static final String ARCHIVER_DOWNLOAD_MAAS = "2";
|
||||||
|
/**
|
||||||
|
* Auto Accept, Auto Start
|
||||||
|
*/
|
||||||
|
public static final String ARCHIVER_DOWNLOAD_AAAS = "3";
|
||||||
|
/**
|
||||||
|
* List View on the front and search pages
|
||||||
|
*/
|
||||||
|
public static final String LAYOUT_MODE_LIST = "l";
|
||||||
|
/**
|
||||||
|
* Thumbnail View on the front and search pages
|
||||||
|
*/
|
||||||
|
public static final String LAYOUT_MODE_THUMB = "t";
|
||||||
|
public static final int MISC = 0x1;
|
||||||
|
public static final int DOUJINSHI = 0x2;
|
||||||
|
public static final int MANGA = 0x4;
|
||||||
|
public static final int ARTIST_CG = 0x8;
|
||||||
|
public static final int GAME_CG = 0x10;
|
||||||
|
public static final int IMAGE_SET = 0x20;
|
||||||
|
public static final int COSPLAY = 0x40;
|
||||||
|
public static final int ASIAN_PORN = 0x80;
|
||||||
|
public static final int NON_H = 0x100;
|
||||||
|
public static final int WESTERN = 0x200;
|
||||||
|
public static final int ALL_CATEGORY = 0x3ff;
|
||||||
|
public static final int NAMESPACES_RECLASS = 0x1;
|
||||||
|
public static final int NAMESPACES_LANGUAGE = 0x2;
|
||||||
|
public static final int NAMESPACES_PARODY = 0x4;
|
||||||
|
public static final int NAMESPACES_CHARACTER = 0x8;
|
||||||
|
public static final int NAMESPACES_GROUP = 0x10;
|
||||||
|
public static final int NAMESPACES_ARTIST = 0x20;
|
||||||
|
public static final int NAMESPACES_MALE = 0x40;
|
||||||
|
public static final int NAMESPACES_FEMALE = 0x80;
|
||||||
|
public static final String JAPANESE_ORIGINAL = "0";
|
||||||
|
public static final String JAPANESE_TRANSLATED = "1024";
|
||||||
|
public static final String JAPANESE_REWRITE = "2048";
|
||||||
|
public static final String ENGLISH_ORIGINAL = "1";
|
||||||
|
public static final String ENGLISH_TRANSLATED = "1025";
|
||||||
|
public static final String ENGLISH_REWRITE = "2049";
|
||||||
|
public static final String CHINESE_ORIGINAL = "10";
|
||||||
|
public static final String CHINESE_TRANSLATED = "1034";
|
||||||
|
public static final String CHINESE_REWRITE = "2058";
|
||||||
|
public static final String DUTCH_ORIGINAL = "20";
|
||||||
|
public static final String DUTCH_TRANSLATED = "1044";
|
||||||
|
public static final String DUTCH_REWRITE = "2068";
|
||||||
|
public static final String FRENCH_ORIGINAL = "30";
|
||||||
|
public static final String FRENCH_TRANSLATED = "1054";
|
||||||
|
public static final String FRENCH_REWRITE = "2078";
|
||||||
|
public static final String GERMAN_ORIGINAL = "40";
|
||||||
|
public static final String GERMAN_TRANSLATED = "1064";
|
||||||
|
public static final String GERMAN_REWRITE = "2088";
|
||||||
|
public static final String HUNGARIAN_ORIGINAL = "50";
|
||||||
|
public static final String HUNGARIAN_TRANSLATED = "1074";
|
||||||
|
public static final String HUNGARIAN_REWRITE = "2098";
|
||||||
|
public static final String ITALIAN_ORIGINAL = "60";
|
||||||
|
public static final String ITALIAN_TRANSLATED = "1084";
|
||||||
|
public static final String ITALIAN_REWRITE = "2108";
|
||||||
|
public static final String KOREAN_ORIGINAL = "70";
|
||||||
|
public static final String KOREAN_TRANSLATED = "1094";
|
||||||
|
public static final String KOREAN_REWRITE = "2118";
|
||||||
|
public static final String POLISH_ORIGINAL = "80";
|
||||||
|
public static final String POLISH_TRANSLATED = "1104";
|
||||||
|
public static final String POLISH_REWRITE = "2128";
|
||||||
|
public static final String PORTUGUESE_ORIGINAL = "90";
|
||||||
|
public static final String PORTUGUESE_TRANSLATED = "1114";
|
||||||
|
public static final String PORTUGUESE_REWRITE = "2138";
|
||||||
|
public static final String RUSSIAN_ORIGINAL = "100";
|
||||||
|
public static final String RUSSIAN_TRANSLATED = "1124";
|
||||||
|
public static final String RUSSIAN_REWRITE = "2148";
|
||||||
|
public static final String SPANISH_ORIGINAL = "110";
|
||||||
|
public static final String SPANISH_TRANSLATED = "1134";
|
||||||
|
public static final String SPANISH_REWRITE = "2158";
|
||||||
|
public static final String THAI_ORIGINAL = "120";
|
||||||
|
public static final String THAI_TRANSLATED = "1144";
|
||||||
|
public static final String THAI_REWRITE = "2168";
|
||||||
|
public static final String VIETNAMESE_ORIGINAL = "130";
|
||||||
|
public static final String VIETNAMESE_TRANSLATED = "1154";
|
||||||
|
public static final String VIETNAMESE_REWRITE = "2178";
|
||||||
|
public static final String NA_ORIGINAL = "254";
|
||||||
|
public static final String NA_TRANSLATED = "1278";
|
||||||
|
public static final String NA_REWRITE = "2302";
|
||||||
|
public static final String OTHER_ORIGINAL = "255";
|
||||||
|
public static final String OTHER_TRANSLATED = "1279";
|
||||||
|
public static final String OTHER_REWRITE = "2303";
|
||||||
|
/**
|
||||||
|
* 25 results per page for the index/search page and torrent search pages
|
||||||
|
*/
|
||||||
|
public static final String RESULT_COUNT_25 = "0";
|
||||||
|
/**
|
||||||
|
* 50 results per page for the index/search page and torrent search pages
|
||||||
|
*/
|
||||||
|
public static final String RESULT_COUNT_50 = "1";
|
||||||
|
/**
|
||||||
|
* 100 results per page for the index/search page and torrent search pages
|
||||||
|
*/
|
||||||
|
public static final String RESULT_COUNT_100 = "2";
|
||||||
|
/**
|
||||||
|
* 200 results per page for the index/search page and torrent search pages
|
||||||
|
*/
|
||||||
|
public static final String RESULT_COUNT_200 = "3";
|
||||||
|
/**
|
||||||
|
* On mouse-over
|
||||||
|
*/
|
||||||
|
public static final String MOUSE_OVER_YES = "m";
|
||||||
|
/**
|
||||||
|
* On page load
|
||||||
|
*/
|
||||||
|
public static final String MOUSE_OVER_NO = "p";
|
||||||
|
/**
|
||||||
|
* Preview normal size
|
||||||
|
*/
|
||||||
|
public static final String PREVIEW_SIZE_NORMAL = "m";
|
||||||
|
/**
|
||||||
|
* Preview large size
|
||||||
|
*/
|
||||||
|
public static final String PREVIEW_SIZE_LARGE = "l";
|
||||||
|
/**
|
||||||
|
* 4 row preview per page
|
||||||
|
*/
|
||||||
|
public static final String PREVIEW_ROW_4 = "2";
|
||||||
|
/**
|
||||||
|
* 10 row preview per page
|
||||||
|
*/
|
||||||
|
public static final String PREVIEW_ROW_10 = "5";
|
||||||
|
/**
|
||||||
|
* 20 row preview per page
|
||||||
|
*/
|
||||||
|
public static final String PREVIEW_ROW_20 = "10";
|
||||||
|
/**
|
||||||
|
* 40 row preview per page
|
||||||
|
*/
|
||||||
|
public static final String PREVIEW_ROW_40 = "20";
|
||||||
|
/**
|
||||||
|
* Oldest comments first
|
||||||
|
*/
|
||||||
|
public static final String COMMENTS_SORT_OLDEST_FIRST = "a";
|
||||||
|
/**
|
||||||
|
* Recent comments first
|
||||||
|
*/
|
||||||
|
public static final String COMMENTS_SORT_RECENT_FIRST = "d";
|
||||||
|
/**
|
||||||
|
* By highest score
|
||||||
|
*/
|
||||||
|
public static final String COMMENTS_SORT_HIGHEST_SCORE_FIRST = "s";
|
||||||
|
/**
|
||||||
|
* Show gallery comment votes On score hover or click
|
||||||
|
*/
|
||||||
|
public static final String COMMENTS_VOTES_POP = "0";
|
||||||
|
/**
|
||||||
|
* Always show gallery comment votes
|
||||||
|
*/
|
||||||
|
public static final String COMMENTS_VOTES_ALWAYS = "1";
|
||||||
|
/**
|
||||||
|
* Sort order for gallery tags alphabetically
|
||||||
|
*/
|
||||||
|
public static final String TAGS_SORT_ALPHABETICAL = "a";
|
||||||
|
/**
|
||||||
|
* Sort order for gallery tags by tag power
|
||||||
|
*/
|
||||||
|
public static final String TAGS_SORT_POWER = "p";
|
||||||
|
/**
|
||||||
|
* Show gallery page numbers
|
||||||
|
*/
|
||||||
|
public static final String SHOW_GALLERY_INDEX_YES = "1";
|
||||||
|
/**
|
||||||
|
* Do not show gallery page numbers
|
||||||
|
*/
|
||||||
|
public static final String SHOW_GALLERY_INDEX_NO = "0";
|
||||||
|
/**
|
||||||
|
* Enable Tag Flagging
|
||||||
|
*/
|
||||||
|
public static final String ENABLE_TAG_FLAGGING_YES = "y";
|
||||||
|
/**
|
||||||
|
* Do not enable Tag Flagging
|
||||||
|
*/
|
||||||
|
public static final String ENABLE_TAG_FLAGGING_NO = "n";
|
||||||
|
/**
|
||||||
|
* Always display the original images
|
||||||
|
*/
|
||||||
|
public static final String ALWAYS_ORIGINAL_YES = "y";
|
||||||
|
/**
|
||||||
|
* Do not Always display the original images
|
||||||
|
*/
|
||||||
|
public static final String ALWAYS_ORIGINAL_NO = "n";
|
||||||
|
/**
|
||||||
|
* Enable the Multi-Page Viewe
|
||||||
|
*/
|
||||||
|
public static final String MULTI_PAGE_YES = "y";
|
||||||
|
/**
|
||||||
|
* Do not enable the Multi-Page Viewe
|
||||||
|
*/
|
||||||
|
public static final String MULTI_PAGE_NO = "n";
|
||||||
|
/**
|
||||||
|
* Align left, only scale if image is larger than browser width
|
||||||
|
*/
|
||||||
|
public static final String MULTI_PAGE_STYLE_N = "n";
|
||||||
|
/**
|
||||||
|
* Align center, only scale if image is larger than browser width
|
||||||
|
*/
|
||||||
|
public static final String MULTI_PAGE_STYLE_C = "c";
|
||||||
|
/**
|
||||||
|
* Align center, Always scale images to fit browser width
|
||||||
|
*/
|
||||||
|
public static final String MULTI_PAGE_STYLE_Y = "y";
|
||||||
|
/**
|
||||||
|
* Show Multi-Page Viewer Thumbnail Pane
|
||||||
|
*/
|
||||||
|
public static final String MULTI_PAGE_THUMB_SHOW = "n";
|
||||||
|
/**
|
||||||
|
* Hide Multi-Page Viewer Thumbnail Pane
|
||||||
|
*/
|
||||||
|
public static final String MULTI_PAGE_THUMB_HIDE = "y";
|
||||||
|
/**
|
||||||
|
* 460x for lofi resolution
|
||||||
|
*/
|
||||||
|
public static final String LOFI_RESOLUTION_460X = "1";
|
||||||
|
/**
|
||||||
|
* 780X for lofi resolution
|
||||||
|
*/
|
||||||
|
public static final String LOFI_RESOLUTION_780X = "2";
|
||||||
|
/**
|
||||||
|
* 980X for lofi resolution
|
||||||
|
*/
|
||||||
|
public static final String LOFI_RESOLUTION_980X = "3";
|
||||||
|
/**
|
||||||
|
* show warning
|
||||||
|
*/
|
||||||
|
public static final String CONTENT_WARNING_SHOW = "0";
|
||||||
|
/**
|
||||||
|
* not show warning
|
||||||
|
*/
|
||||||
|
public static final String CONTENT_WARNING_NOT_SHOW = "1";
|
||||||
|
/**
|
||||||
|
* The key of load images through the Hentai@Home Network
|
||||||
|
*
|
||||||
|
* @see #LOAD_FROM_HAH_YES
|
||||||
|
* @see #LOAD_FROM_HAH_NO
|
||||||
|
*/
|
||||||
|
private static final String KEY_LOAD_FROM_HAH = "uh";
|
||||||
|
/**
|
||||||
|
* The key of Image Size Settings
|
||||||
|
*
|
||||||
|
* @see #IMAGE_SIZE_AUTO
|
||||||
|
* @see #IMAGE_SIZE_780X
|
||||||
|
* @see #IMAGE_SIZE_980X
|
||||||
|
* @see #IMAGE_SIZE_1280X
|
||||||
|
* @see #IMAGE_SIZE_1600X
|
||||||
|
* @see #IMAGE_SIZE_2400X
|
||||||
|
*/
|
||||||
|
private static final String KEY_IMAGE_SIZE = "xr";
|
||||||
|
/**
|
||||||
|
* The key of scale images width
|
||||||
|
*/
|
||||||
|
private static final String KEY_SCALE_WIDTH = "rx";
|
||||||
|
/**
|
||||||
|
* The key of scale images height
|
||||||
|
*/
|
||||||
|
private static final String KEY_SCALE_HEIGHT = "ry";
|
||||||
|
/**
|
||||||
|
* The key of Gallery Name Display
|
||||||
|
*
|
||||||
|
* @see #GALLERY_TITLE_DEFAULT
|
||||||
|
* @see #GALLERY_TITLE_JAPANESE
|
||||||
|
*/
|
||||||
|
private static final String KEY_GALLERY_TITLE = "tl";
|
||||||
|
/**
|
||||||
|
* The key of the behavior for downloading archiver
|
||||||
|
*
|
||||||
|
* @see #ARCHIVER_DOWNLOAD_MAMS
|
||||||
|
* @see #ARCHIVER_DOWNLOAD_MAAS
|
||||||
|
* @see #ARCHIVER_DOWNLOAD_AAMS
|
||||||
|
* @see #ARCHIVER_DOWNLOAD_AAAS
|
||||||
|
*/
|
||||||
|
private static final String KEY_ARCHIVER_DOWNLOAD = "ar";
|
||||||
|
/**
|
||||||
|
* The key of display mode would you like to use on the front and search pages
|
||||||
|
*
|
||||||
|
* @see #LAYOUT_MODE_LIST
|
||||||
|
* @see #LAYOUT_MODE_THUMB
|
||||||
|
*/
|
||||||
|
private static final String KEY_LAYOUT_MODE = "dm";
|
||||||
|
/**
|
||||||
|
* The key for show popular
|
||||||
|
*
|
||||||
|
* @see #POPULAR_YES
|
||||||
|
* @see #POPULAR_NO
|
||||||
|
*/
|
||||||
|
private static final String KEY_POPULAR = "prn";
|
||||||
|
/**
|
||||||
|
* The key of categories would you like to view as default on the front page
|
||||||
|
*/
|
||||||
|
private static final String KEY_DEFAULT_CATEGORIES = "cats";
|
||||||
|
/**
|
||||||
|
* The key for favorites sort
|
||||||
|
*
|
||||||
|
* @see #FAVORITES_SORT_GALLERY_UPDATE_TIME
|
||||||
|
* @see #FAVORITES_SORT_FAVORITED_TIME
|
||||||
|
*/
|
||||||
|
private static final String KEY_FAVORITES_SORT = "fs";
|
||||||
|
/**
|
||||||
|
* The key of exclude certain namespaces from a default tag search
|
||||||
|
*/
|
||||||
|
private static final String KEY_EXCLUDED_NAMESPACES = "xns";
|
||||||
|
/**
|
||||||
|
* The key of hide galleries in certain languages from the gallery list and searches
|
||||||
|
*/
|
||||||
|
private static final String KEY_EXCLUDED_LANGUAGES = "xl";
|
||||||
|
/**
|
||||||
|
* The key of how many results would you like per page for the index/search page and torrent search pages
|
||||||
|
*
|
||||||
|
* @see #RESULT_COUNT_25
|
||||||
|
* @see #RESULT_COUNT_50
|
||||||
|
* @see #RESULT_COUNT_100
|
||||||
|
* @see #RESULT_COUNT_200
|
||||||
|
*/
|
||||||
|
private static final String KEY_RESULT_COUNT = "rc";
|
||||||
|
/**
|
||||||
|
* The key of mouse-over thumb
|
||||||
|
*
|
||||||
|
* @see #MOUSE_OVER_YES
|
||||||
|
* @see #MOUSE_OVER_NO
|
||||||
|
*/
|
||||||
|
private static final String KEY_MOUSE_OVER = "lt";
|
||||||
|
/**
|
||||||
|
* The key of preview size
|
||||||
|
*
|
||||||
|
* @see #PREVIEW_SIZE_NORMAL
|
||||||
|
* @see #PREVIEW_SIZE_LARGE
|
||||||
|
*/
|
||||||
|
private static final String KEY_PREVIEW_SIZE = "ts";
|
||||||
|
/**
|
||||||
|
* The key of preview row per page
|
||||||
|
*
|
||||||
|
* @see #PREVIEW_ROW_4
|
||||||
|
* @see #PREVIEW_ROW_10
|
||||||
|
* @see #PREVIEW_ROW_20
|
||||||
|
* @see #PREVIEW_ROW_40
|
||||||
|
*/
|
||||||
|
private static final String KEY_PREVIEW_ROW = "tr";
|
||||||
|
/**
|
||||||
|
* The key of sort order for gallery comments
|
||||||
|
*
|
||||||
|
* @see #COMMENTS_SORT_OLDEST_FIRST
|
||||||
|
* @see #COMMENTS_SORT_RECENT_FIRST
|
||||||
|
* @see #COMMENTS_SORT_HIGHEST_SCORE_FIRST
|
||||||
|
*/
|
||||||
|
private static final String KEY_COMMENTS_SORT = "cs";
|
||||||
|
/**
|
||||||
|
* The key of show gallery comment votes
|
||||||
|
*
|
||||||
|
* @see #COMMENTS_VOTES_POP
|
||||||
|
* @see #COMMENTS_VOTES_ALWAYS
|
||||||
|
*/
|
||||||
|
private static final String KEY_COMMENTS_VOTES = "sc";
|
||||||
|
/**
|
||||||
|
* The key of sort order for gallery tags
|
||||||
|
*
|
||||||
|
* @see #TAGS_SORT_ALPHABETICAL
|
||||||
|
* @see #TAGS_SORT_POWER
|
||||||
|
*/
|
||||||
|
private static final String KEY_TAGS_SORT = "to";
|
||||||
|
/**
|
||||||
|
* The key of show gallery page numbers
|
||||||
|
*
|
||||||
|
* @see #SHOW_GALLERY_INDEX_NO
|
||||||
|
* @see #SHOW_GALLERY_INDEX_YES
|
||||||
|
*/
|
||||||
|
private static final String KEY_SHOW_GALLERY_INDEX = "pn";
|
||||||
|
/**
|
||||||
|
* The key of the IP:Port of a proxy-enabled Hentai@Home Client
|
||||||
|
* to load all images
|
||||||
|
*/
|
||||||
|
private static final String KEY_HAH_CLIENT_IP_PORT = "hp";
|
||||||
|
/**
|
||||||
|
* The key of the passkey of a proxy-enabled Hentai@Home Client
|
||||||
|
* to load all images
|
||||||
|
*/
|
||||||
|
private static final String KEY_HAH_CLIENT_PASSKEY = "hk";
|
||||||
|
/**
|
||||||
|
* The key of enable Tag Flagging
|
||||||
|
*
|
||||||
|
* @see #ENABLE_TAG_FLAGGING_NO
|
||||||
|
* @see #ENABLE_TAG_FLAGGING_YES
|
||||||
|
*/
|
||||||
|
private static final String KEY_ENABLE_TAG_FLAGGING = "tf";
|
||||||
|
/**
|
||||||
|
* The key of always display the original images instead of the resampled versions
|
||||||
|
*
|
||||||
|
* @see #ALWAYS_ORIGINAL_NO
|
||||||
|
* @see #ALWAYS_ORIGINAL_YES
|
||||||
|
*/
|
||||||
|
private static final String KEY_ALWAYS_ORIGINAL = "oi";
|
||||||
|
/**
|
||||||
|
* The key of enable the multi-Page Viewer
|
||||||
|
*
|
||||||
|
* @see #MULTI_PAGE_NO
|
||||||
|
* @see #MULTI_PAGE_YES
|
||||||
|
*/
|
||||||
|
private static final String KEY_MULTI_PAGE = "qb";
|
||||||
|
/**
|
||||||
|
* The key of multi-Page Viewer Display Style
|
||||||
|
*
|
||||||
|
* @see #MULTI_PAGE_STYLE_C
|
||||||
|
* @see #MULTI_PAGE_STYLE_N
|
||||||
|
* @see #MULTI_PAGE_STYLE_Y
|
||||||
|
*/
|
||||||
|
private static final String KEY_MULTI_PAGE_STYLE = "ms";
|
||||||
|
/**
|
||||||
|
* The key of multi-Page Viewer Thumbnail Pane
|
||||||
|
*
|
||||||
|
* @see #MULTI_PAGE_THUMB_HIDE
|
||||||
|
* @see #MULTI_PAGE_THUMB_SHOW
|
||||||
|
*/
|
||||||
|
private static final String KEY_MULTI_PAGE_THUMB = "mt";
|
||||||
|
/**
|
||||||
|
* Default gallery title
|
||||||
|
*/
|
||||||
|
private static final String GALLERY_TITLE_DEFAULT = "r";
|
||||||
|
/**
|
||||||
|
* Japanese gallery title
|
||||||
|
*/
|
||||||
|
private static final String GALLERY_TITLE_JAPANESE = "j";
|
||||||
|
/**
|
||||||
|
* Show popular
|
||||||
|
*/
|
||||||
|
private static final String POPULAR_YES = "y";
|
||||||
|
/**
|
||||||
|
* Don't show popular
|
||||||
|
*/
|
||||||
|
private static final String POPULAR_NO = "n";
|
||||||
|
/**
|
||||||
|
* Sort favorites by last gallery update time
|
||||||
|
*/
|
||||||
|
private static final String FAVORITES_SORT_GALLERY_UPDATE_TIME = "p";
|
||||||
|
/**
|
||||||
|
* Sort favorites by favorited time
|
||||||
|
*/
|
||||||
|
private static final String FAVORITES_SORT_FAVORITED_TIME = "f";
|
||||||
|
/**
|
||||||
|
* Load images through the Hentai@Home Network<br/>
|
||||||
|
* key: {@link #KEY_LOAD_FROM_HAH}<br/>
|
||||||
|
* value: {@link #LOAD_FROM_HAH_YES}, {@link #LOAD_FROM_HAH_NO}
|
||||||
|
*/
|
||||||
|
public String loadFromHAH = LOAD_FROM_HAH_YES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image Size<br/>
|
||||||
|
* key: {@link #KEY_IMAGE_SIZE}<br/>
|
||||||
|
* value: {@link #IMAGE_SIZE_AUTO}, {@link #IMAGE_SIZE_780X}, {@link #IMAGE_SIZE_980X},
|
||||||
|
* {@link #IMAGE_SIZE_1280X}, {@link #IMAGE_SIZE_1600X}, {@link #IMAGE_SIZE_2400X}
|
||||||
|
*/
|
||||||
|
public String imageSize = IMAGE_SIZE_AUTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale width<br/>
|
||||||
|
* key: {@link #KEY_SCALE_WIDTH}<br/>
|
||||||
|
* value: 0 for no limit
|
||||||
|
*/
|
||||||
|
public int scaleWidth = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale height<br/>
|
||||||
|
* key: {@link #KEY_SCALE_HEIGHT}<br/>
|
||||||
|
* value: 0 for no limit
|
||||||
|
*/
|
||||||
|
public int scaleHeight = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gallery title<br/>
|
||||||
|
* key: {@link #KEY_GALLERY_TITLE}<br/>
|
||||||
|
* value: {@link #GALLERY_TITLE_DEFAULT}, {@link #GALLERY_TITLE_JAPANESE}
|
||||||
|
*/
|
||||||
|
public String galleryTitle = GALLERY_TITLE_DEFAULT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default behavior for downloading an archiver<br/>
|
||||||
|
* key: {@link #KEY_ARCHIVER_DOWNLOAD}<br/>
|
||||||
|
* value: {@link #ARCHIVER_DOWNLOAD_MAMS}, {@link #ARCHIVER_DOWNLOAD_AAMS},
|
||||||
|
* {@link #ARCHIVER_DOWNLOAD_MAAS}, {@link #ARCHIVER_DOWNLOAD_AAAS}
|
||||||
|
*/
|
||||||
|
public String archiverDownload = ARCHIVER_DOWNLOAD_MAMS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mode used on the front and search pages<br/>
|
||||||
|
* false for list, true for thumb<br/>
|
||||||
|
* key: {@link #KEY_LAYOUT_MODE}<br/>
|
||||||
|
* value: {@link #LAYOUT_MODE_LIST}, {@link #LAYOUT_MODE_THUMB}
|
||||||
|
*/
|
||||||
|
public String layoutMode = LAYOUT_MODE_LIST;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show popular or not<br/>
|
||||||
|
* key: {@link #KEY_POPULAR}<br/>
|
||||||
|
* value: {@link #POPULAR_YES}, {@link #POPULAR_NO}
|
||||||
|
*/
|
||||||
|
public String popular = POPULAR_YES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default categories on the front page<br/>
|
||||||
|
* key: {@link #KEY_DEFAULT_CATEGORIES}<br/>
|
||||||
|
* value: the value of categories, for multiple use & operation,
|
||||||
|
* 0 for none
|
||||||
|
*/
|
||||||
|
public int defaultCategories = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <br/>
|
||||||
|
* key: {@link #KEY_FAVORITES_SORT}<br/>
|
||||||
|
* value: {@link #FAVORITES_SORT_GALLERY_UPDATE_TIME}, {@link #FAVORITES_SORT_FAVORITED_TIME}
|
||||||
|
*/
|
||||||
|
public String favoritesSort = FAVORITES_SORT_FAVORITED_TIME;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certain namespaces excluded from a default tag search<br/>
|
||||||
|
* key: {@link #KEY_EXCLUDED_NAMESPACES}<br/>
|
||||||
|
* value: the value of namespaces, for multiple use & operation,
|
||||||
|
* 0 for none
|
||||||
|
*/
|
||||||
|
public int excludedNamespaces = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certain languages excluded from list and searches<br/>
|
||||||
|
* key: {@link #KEY_EXCLUDED_LANGUAGES}<br/>
|
||||||
|
* value: {@link #JAPANESE_TRANSLATED}, {@link #JAPANESE_REWRITE} ...
|
||||||
|
* For multiple languages, use <code>x<code/> to combine them, like 1x1024x2048
|
||||||
|
*/
|
||||||
|
public String excludedLanguages = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many results would you like per page for the index/search page
|
||||||
|
* and torrent search pages<br/>
|
||||||
|
* key: {@link #KEY_RESULT_COUNT}<br/>
|
||||||
|
* value: {@link #RESULT_COUNT_25}, {@link #RESULT_COUNT_50},
|
||||||
|
* {@link #RESULT_COUNT_100}, {@link #RESULT_COUNT_200}<br/>
|
||||||
|
* Require <code>Hath Perk:Paging Enlargement</code>
|
||||||
|
*/
|
||||||
|
public String resultCount = RESULT_COUNT_25;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mouse-over thumb<br/>
|
||||||
|
* key: {@link #KEY_MOUSE_OVER}<br/>
|
||||||
|
* value: {@link #MOUSE_OVER_YES}, {@link #MOUSE_OVER_NO}
|
||||||
|
*/
|
||||||
|
public String mouseOver = MOUSE_OVER_YES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default preview mode<br/>
|
||||||
|
* key: {@link #KEY_PREVIEW_SIZE}<br/>
|
||||||
|
* value: {@link #PREVIEW_SIZE_NORMAL}, {@link #PREVIEW_SIZE_LARGE}
|
||||||
|
*/
|
||||||
|
public String previewSize = PREVIEW_SIZE_LARGE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview row<br/>
|
||||||
|
* key: {@link #KEY_PREVIEW_ROW}<br/>
|
||||||
|
* value: {@link #PREVIEW_ROW_4}, {@link #PREVIEW_ROW_10},
|
||||||
|
* {@link #PREVIEW_ROW_20}, {@link #PREVIEW_ROW_40}
|
||||||
|
*/
|
||||||
|
public String previewRow = PREVIEW_ROW_4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort order for gallery comments<br/>
|
||||||
|
* key: {@link #KEY_COMMENTS_SORT}<br/>
|
||||||
|
* value: {@link #COMMENTS_SORT_OLDEST_FIRST}, {@link #COMMENTS_SORT_RECENT_FIRST},
|
||||||
|
* {@link #COMMENTS_SORT_HIGHEST_SCORE_FIRST}
|
||||||
|
*/
|
||||||
|
public String commentSort = COMMENTS_SORT_OLDEST_FIRST;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show gallery comment votes mode<br/>
|
||||||
|
* key: {@link #KEY_COMMENTS_VOTES}<br/>
|
||||||
|
* value: {@link #COMMENTS_VOTES_POP}, {@link #COMMENTS_VOTES_ALWAYS}
|
||||||
|
*/
|
||||||
|
public String commentVotes = COMMENTS_VOTES_POP;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort order for gallery tags<br/>
|
||||||
|
* key: {@link #KEY_TAGS_SORT}<br/>
|
||||||
|
* value: {@link #TAGS_SORT_ALPHABETICAL}, {@link #TAGS_SORT_POWER}
|
||||||
|
*/
|
||||||
|
public String tagSort = TAGS_SORT_ALPHABETICAL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show gallery page numbers<br/>
|
||||||
|
* key: {@link #KEY_SHOW_GALLERY_INDEX}<br/>
|
||||||
|
* value: {@link #SHOW_GALLERY_INDEX_YES}, {@link #SHOW_GALLERY_INDEX_NO}
|
||||||
|
*/
|
||||||
|
public String showGalleryIndex = SHOW_GALLERY_INDEX_YES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IP of a proxy-enabled Hentai@Home Client
|
||||||
|
* to load all images<br/>
|
||||||
|
* key: {@link #KEY_HAH_CLIENT_IP_PORT}<br/>
|
||||||
|
*/
|
||||||
|
public String hahClientIp = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PORT of a proxy-enabled Hentai@Home Client
|
||||||
|
* to load all images<br/>
|
||||||
|
* key: {@link #KEY_HAH_CLIENT_IP_PORT}<br/>
|
||||||
|
*/
|
||||||
|
public int hahClientPort = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The passkey of a proxy-enabled Hentai@Home Client
|
||||||
|
* to load all images<br/>
|
||||||
|
* key: {@link #KEY_HAH_CLIENT_PASSKEY}<br/>
|
||||||
|
*/
|
||||||
|
public String hahClientPasskey = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable tag flagging
|
||||||
|
* key: {@link #KEY_ENABLE_TAG_FLAGGING}<br/>
|
||||||
|
* value: {@link #ENABLE_TAG_FLAGGING_YES}, {@link #ENABLE_TAG_FLAGGING_NO}<br/>
|
||||||
|
* <code>Bronze Star</code> or <code>Hath Perk: Tag Flagging</code> Required
|
||||||
|
*/
|
||||||
|
public String enableTagFlagging = ENABLE_TAG_FLAGGING_NO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always display the original images instead of the resampled versions<br/>
|
||||||
|
* key: {@link #KEY_ALWAYS_ORIGINAL}<br/>
|
||||||
|
* value: {@link #ALWAYS_ORIGINAL_YES}, {@link #ALWAYS_ORIGINAL_NO}<br/>
|
||||||
|
* <code>Silver Star</code> or <code>Hath Perk: Source Nexus</code> Required
|
||||||
|
*/
|
||||||
|
public String alwaysOriginal = ALWAYS_ORIGINAL_NO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the multi-Page Viewer<br/>
|
||||||
|
* key: {@link #KEY_MULTI_PAGE}<br/>
|
||||||
|
* value: {@link #MULTI_PAGE_YES}, {@link #MULTI_PAGE_NO}<br/>
|
||||||
|
* <code>Gold Star</code> or <code>Hath Perk: Multi-Page Viewer</code> Required
|
||||||
|
*/
|
||||||
|
public String multiPage = MULTI_PAGE_NO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-Page Viewer Display Style<br/>
|
||||||
|
* key: {@link #KEY_MULTI_PAGE_STYLE}<br/>
|
||||||
|
* value: {@link #MULTI_PAGE_STYLE_N}, {@link #MULTI_PAGE_STYLE_C},
|
||||||
|
* {@link #MULTI_PAGE_STYLE_Y}<br/>
|
||||||
|
* <code>Gold Star</code> or <code>Hath Perk: Multi-Page Viewer</code> Required
|
||||||
|
*/
|
||||||
|
public String multiPageStyle = MULTI_PAGE_STYLE_N;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-Page Viewer Thumbnail Pane<br/>
|
||||||
|
* key: {@link #KEY_MULTI_PAGE_THUMB}<br/>
|
||||||
|
* value: {@link #MULTI_PAGE_THUMB_SHOW}, {@link #MULTI_PAGE_THUMB_HIDE}<br/>
|
||||||
|
* <code>Gold Star</code> or <code>Hath Perk: Multi-Page Viewer</code> Required
|
||||||
|
*/
|
||||||
|
public String multiPageThumb = MULTI_PAGE_THUMB_SHOW;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lofi resolution
|
||||||
|
* key: {@link #KEY_LOFI_RESOLUTION}<br/>
|
||||||
|
* value: {@link #LOFI_RESOLUTION_460X}, {@link #LOFI_RESOLUTION_780X},
|
||||||
|
* {@link #LOFI_RESOLUTION_980X}
|
||||||
|
*/
|
||||||
|
public String lofiResolution = LOFI_RESOLUTION_980X;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show content warning
|
||||||
|
* key: {@link #KEY_CONTENT_WARNING}<br/>
|
||||||
|
* value: {@link #CONTENT_WARNING_SHOW}, {@link #CONTENT_WARNING_NOT_SHOW}
|
||||||
|
*/
|
||||||
|
public String contentWarning = CONTENT_WARNING_NOT_SHOW;
|
||||||
|
|
||||||
|
private String mUconfig;
|
||||||
|
|
||||||
|
private boolean mDirty = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EhConfig clone() {
|
||||||
|
try {
|
||||||
|
return (EhConfig) super.clone();
|
||||||
|
} catch (CloneNotSupportedException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setDirty() {
|
||||||
|
mDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUconfig() {
|
||||||
|
String hahClientIpPort = (InetValidator.isValidInet4Address(hahClientIp) &&
|
||||||
|
InetValidator.isValidInetPort(hahClientPort)) ?
|
||||||
|
hahClientIp + "%3A" + hahClientPort : "";
|
||||||
|
String hahClientPasskey = null == this.hahClientPasskey ? "" : this.hahClientPasskey;
|
||||||
|
mUconfig = KEY_LOAD_FROM_HAH + "_" + loadFromHAH + "-" +
|
||||||
|
KEY_IMAGE_SIZE + "_" + imageSize + "-" +
|
||||||
|
KEY_SCALE_WIDTH + "_" + scaleWidth + "-" +
|
||||||
|
KEY_SCALE_HEIGHT + "_" + scaleHeight + "-" +
|
||||||
|
KEY_GALLERY_TITLE + "_" + galleryTitle + "-" +
|
||||||
|
KEY_ARCHIVER_DOWNLOAD + "_" + archiverDownload + "-" +
|
||||||
|
KEY_LAYOUT_MODE + "_" + layoutMode + "-" +
|
||||||
|
KEY_POPULAR + "_" + popular + "-" +
|
||||||
|
KEY_DEFAULT_CATEGORIES + "_" + defaultCategories + "-" +
|
||||||
|
KEY_FAVORITES_SORT + "_" + favoritesSort + "-" +
|
||||||
|
KEY_EXCLUDED_NAMESPACES + "_" + excludedNamespaces + "-" +
|
||||||
|
KEY_EXCLUDED_LANGUAGES + "_" + excludedLanguages + "-" +
|
||||||
|
KEY_RESULT_COUNT + "_" + resultCount + "-" +
|
||||||
|
KEY_MOUSE_OVER + "_" + mouseOver + "-" +
|
||||||
|
KEY_PREVIEW_SIZE + "_" + previewSize + "-" +
|
||||||
|
KEY_PREVIEW_ROW + "_" + previewRow + "-" +
|
||||||
|
KEY_COMMENTS_SORT + "_" + commentSort + "-" +
|
||||||
|
KEY_COMMENTS_VOTES + "_" + commentVotes + "-" +
|
||||||
|
KEY_TAGS_SORT + "_" + tagSort + "-" +
|
||||||
|
KEY_SHOW_GALLERY_INDEX + "_" + showGalleryIndex + "-" +
|
||||||
|
KEY_HAH_CLIENT_IP_PORT + "_" + hahClientIpPort + "-" +
|
||||||
|
KEY_HAH_CLIENT_PASSKEY + "_" + hahClientPasskey + "-" +
|
||||||
|
KEY_ENABLE_TAG_FLAGGING + "_" + enableTagFlagging + "-" +
|
||||||
|
KEY_ALWAYS_ORIGINAL + "_" + alwaysOriginal + "-" +
|
||||||
|
KEY_MULTI_PAGE + "_" + multiPage + "-" +
|
||||||
|
KEY_MULTI_PAGE_STYLE + "_" + multiPageStyle + "-" +
|
||||||
|
KEY_MULTI_PAGE_THUMB + "_" + multiPageThumb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String uconfig() {
|
||||||
|
if (mDirty) {
|
||||||
|
mDirty = false;
|
||||||
|
updateUconfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mUconfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.EhApplication
|
||||||
|
import com.hippo.network.CookieDatabase
|
||||||
|
import com.hippo.network.CookieSet
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
object EhCookieStore : CookieJar {
|
||||||
|
private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie.db")
|
||||||
|
private val map: MutableMap<String, CookieSet> = db.allCookies
|
||||||
|
|
||||||
|
fun signOut() {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasSignedIn(): Boolean {
|
||||||
|
val url = EhUrl.HOST_E.toHttpUrl()
|
||||||
|
return contains(url, KEY_IPB_MEMBER_ID) &&
|
||||||
|
contains(url, KEY_IPB_PASS_HASH)
|
||||||
|
}
|
||||||
|
|
||||||
|
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
||||||
|
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
||||||
|
const val KEY_IGNEOUS = "igneous"
|
||||||
|
private val sTipsCookie: Cookie = Cookie.Builder()
|
||||||
|
.name(EhConfig.KEY_CONTENT_WARNING)
|
||||||
|
.value(EhConfig.CONTENT_WARNING_NOT_SHOW)
|
||||||
|
.domain(EhUrl.DOMAIN_E)
|
||||||
|
.path("/")
|
||||||
|
.expiresAt(Long.MAX_VALUE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun newCookie(
|
||||||
|
cookie: Cookie, newDomain: String, forcePersistent: Boolean,
|
||||||
|
forceLongLive: Boolean, forceNotHostOnly: Boolean
|
||||||
|
): Cookie {
|
||||||
|
val builder = Cookie.Builder()
|
||||||
|
builder.name(cookie.name)
|
||||||
|
builder.value(cookie.value)
|
||||||
|
if (forceLongLive) {
|
||||||
|
builder.expiresAt(Long.MAX_VALUE)
|
||||||
|
} else if (cookie.persistent) {
|
||||||
|
builder.expiresAt(cookie.expiresAt)
|
||||||
|
} else if (forcePersistent) {
|
||||||
|
builder.expiresAt(Long.MAX_VALUE)
|
||||||
|
}
|
||||||
|
if (cookie.hostOnly && !forceNotHostOnly) {
|
||||||
|
builder.hostOnlyDomain(newDomain)
|
||||||
|
} else {
|
||||||
|
builder.domain(newDomain)
|
||||||
|
}
|
||||||
|
builder.path(cookie.path)
|
||||||
|
if (cookie.secure) {
|
||||||
|
builder.secure()
|
||||||
|
}
|
||||||
|
if (cookie.httpOnly) {
|
||||||
|
builder.httpOnly()
|
||||||
|
}
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun addCookie(cookie: Cookie) {
|
||||||
|
// For cookie database
|
||||||
|
var toAdd: Cookie? = null
|
||||||
|
var toUpdate: Cookie? = null
|
||||||
|
var toRemove: Cookie? = null
|
||||||
|
var set = map[cookie.domain]
|
||||||
|
if (set == null) {
|
||||||
|
set = CookieSet()
|
||||||
|
map[cookie.domain] = set
|
||||||
|
}
|
||||||
|
if (cookie.expiresAt <= System.currentTimeMillis()) {
|
||||||
|
toRemove = set.remove(cookie)
|
||||||
|
// If the cookie is not persistent, it's not in database
|
||||||
|
if (toRemove != null && !toRemove.persistent) {
|
||||||
|
toRemove = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toAdd = cookie
|
||||||
|
toUpdate = set.add(cookie)
|
||||||
|
// If the cookie is not persistent, it's not in database
|
||||||
|
if (!toAdd.persistent) toAdd = null
|
||||||
|
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
|
||||||
|
// Remove the cookie if it updates to null
|
||||||
|
if (toAdd == null && toUpdate != null) {
|
||||||
|
toRemove = toUpdate
|
||||||
|
toUpdate = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toRemove != null) {
|
||||||
|
db.remove(toRemove)
|
||||||
|
}
|
||||||
|
if (toAdd != null) {
|
||||||
|
if (toUpdate != null) {
|
||||||
|
db.update(toUpdate, toAdd)
|
||||||
|
} else {
|
||||||
|
db.add(toAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCookieHeader(url: HttpUrl): String {
|
||||||
|
val cookies = getCookies(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()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getCookies(url: HttpUrl): List<Cookie> {
|
||||||
|
val accepted: MutableList<Cookie> = ArrayList()
|
||||||
|
val expired: MutableList<Cookie> = ArrayList()
|
||||||
|
for ((domain, cookieSet) in map) {
|
||||||
|
if (domainMatch(url, domain)) {
|
||||||
|
cookieSet[url, accepted, expired]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (cookie in expired) {
|
||||||
|
if (cookie.persistent) {
|
||||||
|
db.remove(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 6265 Section-5.4 step 2, sort the cookie-list
|
||||||
|
// Cookies with longer paths are listed before cookies with shorter paths.
|
||||||
|
// Ignore creation-time, we don't store them.
|
||||||
|
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
|
||||||
|
return accepted
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contains(url: HttpUrl, name: String?): Boolean {
|
||||||
|
for (cookie in getCookies(url)) {
|
||||||
|
if (cookie.name == name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all cookies in this `CookieRepository`.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun clear() {
|
||||||
|
map.clear()
|
||||||
|
db.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun close() {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
for (cookie in cookies) {
|
||||||
|
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
||||||
|
if (cookie.name != "__utmp") {
|
||||||
|
addCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
|
||||||
|
* of Android's private InetAddress#isNumeric API.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* This matches IPv6 addresses as a hex string containing at least one colon, and possibly
|
||||||
|
* including dots after the first colon. It matches IPv4 addresses as strings containing only
|
||||||
|
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
|
||||||
|
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
|
||||||
|
* verification).
|
||||||
|
*/
|
||||||
|
private val VERIFY_AS_IP_ADDRESS =
|
||||||
|
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if `host` is not a host name and might be an IP address.
|
||||||
|
*/
|
||||||
|
private fun verifyAsIpAddress(host: String): Boolean {
|
||||||
|
return VERIFY_AS_IP_ADDRESS.matcher(host).matches()
|
||||||
|
}
|
||||||
|
|
||||||
|
// okhttp3.Cookie.domainMatch(HttpUrl, String)
|
||||||
|
@JvmStatic
|
||||||
|
fun domainMatch(url: HttpUrl, domain: String?): Boolean {
|
||||||
|
val urlHost = url.host
|
||||||
|
return if (urlHost == domain) {
|
||||||
|
true // As in 'example.com' matching 'example.com'.
|
||||||
|
} else urlHost.endsWith(domain!!)
|
||||||
|
&& urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(
|
||||||
|
urlHost
|
||||||
|
)
|
||||||
|
// As in 'example.com' matching 'www.example.com'.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
|
val cookies = getCookies(url)
|
||||||
|
val checkTips = domainMatch(url, EhUrl.DOMAIN_E)
|
||||||
|
return if (checkTips) {
|
||||||
|
val result: MutableList<Cookie> = ArrayList(cookies.size + 1)
|
||||||
|
// Add all but skip some
|
||||||
|
for (cookie in cookies) {
|
||||||
|
val name = cookie.name
|
||||||
|
if (EhConfig.KEY_CONTENT_WARNING == name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (EhConfig.KEY_UCONFIG == name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.add(cookie)
|
||||||
|
}
|
||||||
|
// Add some
|
||||||
|
result.add(sTipsCookie)
|
||||||
|
Collections.unmodifiableList(result)
|
||||||
|
} else {
|
||||||
|
cookies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 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
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.EhApplication
|
||||||
|
import com.hippo.ehviewer.Hosts
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
|
import okhttp3.Dns
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
object EhDns : Dns {
|
||||||
|
private val hosts = EhApplication.hosts
|
||||||
|
private val builtInHosts: MutableMap<String, List<InetAddress>> = mutableMapOf()
|
||||||
|
|
||||||
|
init {
|
||||||
|
/* Pair(ip: String!, blockedByCCP: Boolean!) */
|
||||||
|
val ehgtHosts = arrayOf(
|
||||||
|
Pair("37.48.89.44", false),
|
||||||
|
Pair("81.171.10.48", false),
|
||||||
|
Pair("178.162.139.24", false),
|
||||||
|
Pair("178.162.140.212", false),
|
||||||
|
Pair("2001:1af8:4700:a062:8::47de", false),
|
||||||
|
Pair("2001:1af8:4700:a062:9::47de", true),
|
||||||
|
Pair("2001:1af8:4700:a0c9:4::47de", false),
|
||||||
|
Pair("2001:1af8:4700:a0c9:3::47de", true),
|
||||||
|
)
|
||||||
|
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"e-hentai.org",
|
||||||
|
Pair("104.20.134.21", false),
|
||||||
|
Pair("104.20.135.21", false),
|
||||||
|
Pair("172.67.0.127", false),
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"exhentai.org",
|
||||||
|
Pair("178.175.128.252", false),
|
||||||
|
Pair("178.175.129.252", false),
|
||||||
|
Pair("178.175.129.254", false),
|
||||||
|
Pair("178.175.128.254", false),
|
||||||
|
Pair("178.175.132.20", false),
|
||||||
|
Pair("178.175.132.22", false),
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"repo.e-hentai.org",
|
||||||
|
Pair("94.100.28.57", true),
|
||||||
|
Pair("94.100.29.73", true),
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"forums.e-hentai.org",
|
||||||
|
Pair("94.100.18.243", false),
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"ehgt.org",
|
||||||
|
*ehgtHosts
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"gt0.ehgt.org",
|
||||||
|
*ehgtHosts
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"gt1.ehgt.org",
|
||||||
|
*ehgtHosts
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"gt2.ehgt.org",
|
||||||
|
*ehgtHosts
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"gt3.ehgt.org",
|
||||||
|
*ehgtHosts
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"ul.ehgt.org",
|
||||||
|
Pair("94.100.24.82", true),
|
||||||
|
Pair("94.100.24.72", true),
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
builtInHosts,
|
||||||
|
"raw.githubusercontent.com",
|
||||||
|
Pair("151.101.0.133", false),
|
||||||
|
Pair("151.101.64.133", false),
|
||||||
|
Pair("151.101.128.133", false),
|
||||||
|
Pair("151.101.192.133", false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun put(
|
||||||
|
map: MutableMap<String, List<InetAddress>>,
|
||||||
|
host: String,
|
||||||
|
vararg ips: Pair<String, Boolean>
|
||||||
|
) {
|
||||||
|
map[host] = ips.mapNotNull { pair ->
|
||||||
|
Hosts.toInetAddress(host, pair.first).takeUnless { Settings.getDF() && pair.second }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(UnknownHostException::class)
|
||||||
|
override fun lookup(hostname: String): List<InetAddress> {
|
||||||
|
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.getBuiltInHosts() }
|
||||||
|
?: Dns.SYSTEM.lookup(hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isInHosts(hostname: String): Boolean {
|
||||||
|
return hosts.contains(hostname) || (builtInHosts.contains(hostname) && Settings.getBuiltInHosts())
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,315 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.hippo.ehviewer.EhDB
|
||||||
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
|
import com.hippo.ehviewer.dao.Filter
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
object EhFilter {
|
||||||
|
private val mTitleFilterList: MutableList<Filter> = ArrayList()
|
||||||
|
private val mUploaderFilterList: MutableList<Filter> = ArrayList()
|
||||||
|
private val mTagFilterList: MutableList<Filter> = ArrayList()
|
||||||
|
private val mTagNamespaceFilterList: MutableList<Filter> = ArrayList()
|
||||||
|
private val mCommenterFilterList: MutableList<Filter> = ArrayList()
|
||||||
|
private val mCommentFilterList: MutableList<Filter> = ArrayList()
|
||||||
|
|
||||||
|
const val MODE_TITLE = 0
|
||||||
|
const val MODE_UPLOADER = 1
|
||||||
|
const val MODE_TAG = 2
|
||||||
|
const val MODE_TAG_NAMESPACE = 3
|
||||||
|
const val MODE_COMMENTER = 4
|
||||||
|
const val MODE_COMMENT = 5
|
||||||
|
private val TAG = EhFilter::class.java.simpleName
|
||||||
|
|
||||||
|
init {
|
||||||
|
val list = EhDB.allFilter
|
||||||
|
var i = 0
|
||||||
|
val n = list.size
|
||||||
|
while (i < n) {
|
||||||
|
val filter = list[i]
|
||||||
|
when (filter.mode) {
|
||||||
|
MODE_TITLE -> {
|
||||||
|
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||||
|
mTitleFilterList.add(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE_TAG -> {
|
||||||
|
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||||
|
mTagFilterList.add(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE_TAG_NAMESPACE -> {
|
||||||
|
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||||
|
mTagNamespaceFilterList.add(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE_UPLOADER -> mUploaderFilterList.add(filter)
|
||||||
|
MODE_COMMENTER -> mCommenterFilterList.add(filter)
|
||||||
|
MODE_COMMENT -> mCommentFilterList.add(filter)
|
||||||
|
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val titleFilterList: List<Filter>
|
||||||
|
get() = mTitleFilterList
|
||||||
|
val uploaderFilterList: List<Filter>
|
||||||
|
get() = mUploaderFilterList
|
||||||
|
val tagFilterList: List<Filter>
|
||||||
|
get() = mTagFilterList
|
||||||
|
val tagNamespaceFilterList: List<Filter>
|
||||||
|
get() = mTagNamespaceFilterList
|
||||||
|
val commenterFilterList: List<Filter>
|
||||||
|
get() = mCommenterFilterList
|
||||||
|
val commentFilterList: List<Filter>
|
||||||
|
get() = mCommentFilterList
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun addFilter(filter: Filter): Boolean {
|
||||||
|
// enable filter by default before it is added to database
|
||||||
|
filter.enable = true
|
||||||
|
if (!EhDB.addFilter(filter)) return false
|
||||||
|
when (filter.mode) {
|
||||||
|
MODE_TITLE -> {
|
||||||
|
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||||
|
mTitleFilterList.add(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE_TAG -> {
|
||||||
|
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||||
|
mTagFilterList.add(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE_TAG_NAMESPACE -> {
|
||||||
|
filter.text = filter.text!!.lowercase(Locale.getDefault())
|
||||||
|
mTagNamespaceFilterList.add(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE_UPLOADER -> mUploaderFilterList.add(filter)
|
||||||
|
MODE_COMMENTER -> mCommenterFilterList.add(filter)
|
||||||
|
MODE_COMMENT -> mCommentFilterList.add(filter)
|
||||||
|
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun triggerFilter(filter: Filter) {
|
||||||
|
EhDB.triggerFilter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun deleteFilter(filter: Filter) {
|
||||||
|
EhDB.deleteFilter(filter)
|
||||||
|
when (filter.mode) {
|
||||||
|
MODE_TITLE -> mTitleFilterList.remove(filter)
|
||||||
|
MODE_TAG -> mTagFilterList.remove(filter)
|
||||||
|
MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter)
|
||||||
|
MODE_UPLOADER -> mUploaderFilterList.remove(filter)
|
||||||
|
MODE_COMMENTER -> mCommenterFilterList.remove(filter)
|
||||||
|
MODE_COMMENT -> mCommentFilterList.remove(filter)
|
||||||
|
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun needTags(): Boolean {
|
||||||
|
return 0 != mTagFilterList.size || 0 != mTagNamespaceFilterList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun filterTitle(info: GalleryInfo?): Boolean {
|
||||||
|
if (null == info) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title
|
||||||
|
val title = info.title
|
||||||
|
val filters: List<Filter> = mTitleFilterList
|
||||||
|
if (null != title && filters.isNotEmpty()) {
|
||||||
|
var i = 0
|
||||||
|
val n = filters.size
|
||||||
|
while (i < n) {
|
||||||
|
if (filters[i].enable!! && title.lowercase(Locale.getDefault()).contains(
|
||||||
|
filters[i].text!!
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun filterUploader(info: GalleryInfo?): Boolean {
|
||||||
|
if (null == info) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uploader
|
||||||
|
val uploader = info.uploader
|
||||||
|
val filters: List<Filter> = mUploaderFilterList
|
||||||
|
if (null != uploader && filters.isNotEmpty()) {
|
||||||
|
var i = 0
|
||||||
|
val n = filters.size
|
||||||
|
while (i < n) {
|
||||||
|
if (filters[i].enable!! && uploader == filters[i].text) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchTag(tag: String?, filter: String?): Boolean {
|
||||||
|
if (null == tag || null == filter) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val tagNamespace: String?
|
||||||
|
val tagName: String
|
||||||
|
val filterNamespace: String?
|
||||||
|
val filterName: String
|
||||||
|
var index = tag.indexOf(':')
|
||||||
|
if (index < 0) {
|
||||||
|
tagNamespace = null
|
||||||
|
tagName = tag
|
||||||
|
} else {
|
||||||
|
tagNamespace = tag.substring(0, index)
|
||||||
|
tagName = tag.substring(index + 1)
|
||||||
|
}
|
||||||
|
index = filter.indexOf(':')
|
||||||
|
if (index < 0) {
|
||||||
|
filterNamespace = null
|
||||||
|
filterName = filter
|
||||||
|
} else {
|
||||||
|
filterNamespace = filter.substring(0, index)
|
||||||
|
filterName = filter.substring(index + 1)
|
||||||
|
}
|
||||||
|
return if (null != tagNamespace && null != filterNamespace &&
|
||||||
|
tagNamespace != filterNamespace
|
||||||
|
) {
|
||||||
|
false
|
||||||
|
} else tagName == filterName
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun filterTag(info: GalleryInfo?): Boolean {
|
||||||
|
if (null == info) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag
|
||||||
|
val tags = info.simpleTags
|
||||||
|
val filters: List<Filter> = mTagFilterList
|
||||||
|
if (null != tags && filters.isNotEmpty()) {
|
||||||
|
for (tag in tags) {
|
||||||
|
var i = 0
|
||||||
|
val n = filters.size
|
||||||
|
while (i < n) {
|
||||||
|
if (filters[i].enable!! && matchTag(tag, filters[i].text)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchTagNamespace(tag: String?, filter: String?): Boolean {
|
||||||
|
if (null == tag || null == filter) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val tagNamespace: String
|
||||||
|
val index = tag.indexOf(':')
|
||||||
|
return if (index >= 0) {
|
||||||
|
tagNamespace = tag.substring(0, index)
|
||||||
|
tagNamespace == filter
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun filterTagNamespace(info: GalleryInfo?): Boolean {
|
||||||
|
if (null == info) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val tags = info.simpleTags
|
||||||
|
val filters: List<Filter> = mTagNamespaceFilterList
|
||||||
|
if (null != tags && filters.isNotEmpty()) {
|
||||||
|
for (tag in tags) {
|
||||||
|
var i = 0
|
||||||
|
val n = filters.size
|
||||||
|
while (i < n) {
|
||||||
|
if (filters[i].enable!! && matchTagNamespace(tag, filters[i].text)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun filterCommenter(commenter: String?): Boolean {
|
||||||
|
if (null == commenter) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val filters: List<Filter> = mCommenterFilterList
|
||||||
|
if (filters.isNotEmpty()) {
|
||||||
|
var i = 0
|
||||||
|
val n = filters.size
|
||||||
|
while (i < n) {
|
||||||
|
if (filters[i].enable!! && commenter == filters[i].text) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun filterComment(comment: String?): Boolean {
|
||||||
|
if (null == comment) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val filters: List<Filter> = mCommentFilterList
|
||||||
|
if (filters.isNotEmpty()) {
|
||||||
|
var i = 0
|
||||||
|
val n = filters.size
|
||||||
|
while (i < n) {
|
||||||
|
if (filters[i].enable!!) {
|
||||||
|
val p = Pattern.compile(filters[i].text!!)
|
||||||
|
val m = p.matcher(comment)
|
||||||
|
if (m.find()) return false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
|
||||||
|
class EhRequest {
|
||||||
|
internal var job: Job? = null
|
||||||
|
val isActive
|
||||||
|
get() = job?.isActive ?: false
|
||||||
|
var method = 0
|
||||||
|
private set
|
||||||
|
var args: Array<out Any?>? = null
|
||||||
|
private set
|
||||||
|
var callback: EhClient.Callback<Any?>? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setMethod(method: Int): EhRequest {
|
||||||
|
this.method = method
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setArgs(vararg args: Any?): EhRequest {
|
||||||
|
this.args = args
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun setCallback(callback: EhClient.Callback<*>?): EhRequest {
|
||||||
|
this.callback = callback as EhClient.Callback<Any?>?
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueue(scope: CoroutineScope) {
|
||||||
|
EhClient.enqueue(this, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DelicateCoroutinesApi
|
||||||
|
fun enqueue() {
|
||||||
|
EhClient.enqueue(this, GlobalScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueue(fragment: Fragment) {
|
||||||
|
enqueue(fragment.viewLifecycleOwner.lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueue(activity: Activity) {
|
||||||
|
check(activity is FragmentActivity)
|
||||||
|
enqueue(activity.lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun cancel() {
|
||||||
|
if (isActive) {
|
||||||
|
job?.cancel()
|
||||||
|
callback?.onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import com.hippo.okhttp.ChromeRequestBuilder
|
||||||
|
|
||||||
|
class EhRequestBuilder @JvmOverloads constructor(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
origin: String? = null
|
||||||
|
) : ChromeRequestBuilder(url) {
|
||||||
|
init {
|
||||||
|
referer?.let { addHeader("Referer", it) }
|
||||||
|
origin?.let { addHeader("Origin", it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Tarsin Norbin
|
||||||
|
*
|
||||||
|
* This file is part of EhViewer
|
||||||
|
*
|
||||||
|
* EhViewer is free software: you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* EhViewer is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.Socket
|
||||||
|
import javax.net.ssl.SSLSocket
|
||||||
|
import javax.net.ssl.SSLSocketFactory
|
||||||
|
|
||||||
|
object EhSSLSocketFactory : SSLSocketFactory() {
|
||||||
|
private val sslSocketFactory = getDefault() as SSLSocketFactory
|
||||||
|
override fun getDefaultCipherSuites(): Array<String> {
|
||||||
|
return sslSocketFactory.defaultCipherSuites
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSupportedCipherSuites(): Array<String> {
|
||||||
|
return sslSocketFactory.supportedCipherSuites
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
|
||||||
|
val address = s.inetAddress.hostAddress.takeIf { EhDns.isInHosts(host) }
|
||||||
|
val socket = sslSocketFactory.createSocket(s, address ?: host, port, autoClose) as SSLSocket
|
||||||
|
val sslSession = socket.session
|
||||||
|
Log.d(
|
||||||
|
"EhSSLSocketFactory",
|
||||||
|
"Host: " + host + " Address: " + address + " Protocol:" + sslSession.protocol + " CipherSuite:" + sslSession.cipherSuite
|
||||||
|
)
|
||||||
|
return socket
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSocket(host: String, port: Int): Socket {
|
||||||
|
return sslSocketFactory.createSocket(host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSocket(
|
||||||
|
host: String,
|
||||||
|
port: Int,
|
||||||
|
localHost: InetAddress,
|
||||||
|
localPort: Int
|
||||||
|
): Socket {
|
||||||
|
return sslSocketFactory.createSocket(host, port, localHost, localPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSocket(host: InetAddress, port: Int): Socket {
|
||||||
|
return sslSocketFactory.createSocket(host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createSocket(
|
||||||
|
address: InetAddress,
|
||||||
|
port: Int,
|
||||||
|
localAddress: InetAddress,
|
||||||
|
localPort: Int
|
||||||
|
): Socket {
|
||||||
|
return sslSocketFactory.createSocket(address, port, localAddress, localPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.hippo.ehviewer.AppConfig
|
||||||
|
import com.hippo.ehviewer.EhApplication
|
||||||
|
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
||||||
|
import com.hippo.ehviewer.R
|
||||||
|
import com.hippo.util.HashCodeUtils
|
||||||
|
import com.hippo.yorozuya.FileUtils
|
||||||
|
import com.hippo.yorozuya.copyToFile
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
|
||||||
|
private typealias TagGroup = Map<String, String>
|
||||||
|
private typealias TagGroups = Map<String, TagGroup>
|
||||||
|
|
||||||
|
object EhTagDatabase {
|
||||||
|
private const val NAMESPACE_PREFIX = "n"
|
||||||
|
private lateinit var tagGroups: TagGroups
|
||||||
|
|
||||||
|
fun isInitialized(): Boolean {
|
||||||
|
return this::tagGroups.isInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JSONObject.toTagGroups(): TagGroups =
|
||||||
|
keys().asSequence().associateWith { getJSONObject(it).toTagGroup() }
|
||||||
|
|
||||||
|
private fun JSONObject.toTagGroup(): TagGroup =
|
||||||
|
keys().asSequence().associateWith { getString(it) }
|
||||||
|
|
||||||
|
private fun updateData(source: BufferedSource) {
|
||||||
|
try {
|
||||||
|
tagGroups = JSONObject(source.readString(StandardCharsets.UTF_8)).toTagGroups()
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
|
||||||
|
return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalSuggestFlow(
|
||||||
|
tags: Map<String, String>,
|
||||||
|
keyword: String,
|
||||||
|
translate: Boolean,
|
||||||
|
exactly: Boolean
|
||||||
|
): Flow<Pair<String?, String>> = flow {
|
||||||
|
if (exactly) {
|
||||||
|
tags[keyword]?.let {
|
||||||
|
emit(Pair(it.takeIf { translate }, keyword))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (translate) {
|
||||||
|
tags.forEach { (tag, hint) ->
|
||||||
|
if (tag != keyword &&
|
||||||
|
(tag.containsIgnoreSpace(keyword) || hint.containsIgnoreSpace(keyword))
|
||||||
|
) {
|
||||||
|
emit(Pair(hint, tag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tags.keys.forEach { tag ->
|
||||||
|
if (tag != keyword && tag.containsIgnoreSpace(keyword)) {
|
||||||
|
emit(Pair(null, tag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Construct a cold flow for tag database suggestions */
|
||||||
|
fun suggestFlow(
|
||||||
|
keyword: String,
|
||||||
|
translate: Boolean,
|
||||||
|
exactly: Boolean = false
|
||||||
|
): Flow<Pair<String?, String>> = flow {
|
||||||
|
val keywordPrefix = keyword.substringBefore(':')
|
||||||
|
val keywordTag = keyword.drop(keywordPrefix.length + 1)
|
||||||
|
val prefix = namespaceToPrefix(keywordPrefix) ?: keywordPrefix
|
||||||
|
val tags = tagGroups[prefix.takeIf { keywordTag.isNotEmpty() && it != NAMESPACE_PREFIX }]
|
||||||
|
tags?.let {
|
||||||
|
internalSuggestFlow(it, keywordTag, translate, exactly).collect { (hint, tag) ->
|
||||||
|
emit(Pair(hint, "$prefix:$tag"))
|
||||||
|
}
|
||||||
|
} ?: tagGroups.forEach { (prefix, tags) ->
|
||||||
|
if (prefix != NAMESPACE_PREFIX) {
|
||||||
|
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
|
||||||
|
emit(Pair(hint, "$prefix:$tag"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
|
||||||
|
emit(Pair(hint, "$tag:"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.removeSpace(): String = replace(" ", "")
|
||||||
|
|
||||||
|
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
||||||
|
removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||||
|
|
||||||
|
|
||||||
|
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
|
||||||
|
it["artist"] = "a"
|
||||||
|
it["cosplayer"] = "cos"
|
||||||
|
it["character"] = "c"
|
||||||
|
it["female"] = "f"
|
||||||
|
it["group"] = "g"
|
||||||
|
it["language"] = "l"
|
||||||
|
it["male"] = "m"
|
||||||
|
it["mixed"] = "x"
|
||||||
|
it["other"] = "o"
|
||||||
|
it["parody"] = "p"
|
||||||
|
it["reclass"] = "r"
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun namespaceToPrefix(namespace: String): String? {
|
||||||
|
return NAMESPACE_TO_PREFIX[namespace]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMetadata(context: Context): Array<String>? {
|
||||||
|
return context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||||
|
.takeIf { it.size == 4 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTranslatable(context: Context): Boolean {
|
||||||
|
return context.resources.getBoolean(R.bool.tag_translatable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileContent(file: File): String? {
|
||||||
|
try {
|
||||||
|
file.source().buffer().use { return it.readString(StandardCharsets.UTF_8) }
|
||||||
|
} catch (e: IOException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileSha1(file: File): String? {
|
||||||
|
try {
|
||||||
|
FileInputStream(file).use { stream ->
|
||||||
|
val digest = MessageDigest.getInstance("SHA-1")
|
||||||
|
var n: Int
|
||||||
|
val buffer = ByteArray(4 * 1024)
|
||||||
|
while (stream.read(buffer).also { n = it } != -1) {
|
||||||
|
digest.update(buffer, 0, n)
|
||||||
|
}
|
||||||
|
return HashCodeUtils.bytesToHexString(digest.digest())
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
return null
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkData(sha1File: File, dataFile: File): Boolean {
|
||||||
|
val s1 = getFileContent(sha1File) ?: return false
|
||||||
|
val s2 = getFileSha1(dataFile) ?: return false
|
||||||
|
return s1 == s2
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
|
||||||
|
val request: Request = Request.Builder().url(url).build()
|
||||||
|
val call = client.newCall(request)
|
||||||
|
runCatching {
|
||||||
|
call.execute().use { response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
response.body.use {
|
||||||
|
it.copyToFile(file)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
file.delete()
|
||||||
|
it.printStackTrace()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
@JvmStatic
|
||||||
|
fun update() {
|
||||||
|
launchIO {
|
||||||
|
updateInternal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun updateInternal() {
|
||||||
|
val urls = getMetadata(EhApplication.application)
|
||||||
|
urls?.let {
|
||||||
|
val sha1Name = urls[0]
|
||||||
|
val sha1Url = urls[1]
|
||||||
|
val dataName = urls[2]
|
||||||
|
val dataUrl = urls[3]
|
||||||
|
|
||||||
|
try {
|
||||||
|
val dir = AppConfig.getFilesDir("tag-translations")
|
||||||
|
checkNotNull(dir)
|
||||||
|
|
||||||
|
// Check current sha1 and current data
|
||||||
|
val sha1File = File(dir, sha1Name)
|
||||||
|
val dataFile = File(dir, dataName)
|
||||||
|
if (!checkData(sha1File, dataFile)) {
|
||||||
|
FileUtils.delete(sha1File)
|
||||||
|
FileUtils.delete(dataFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current EhTagDatabase
|
||||||
|
if (!isInitialized() && dataFile.exists()) {
|
||||||
|
try {
|
||||||
|
dataFile.source().buffer().use { updateData(it) }
|
||||||
|
} catch (e: IOException) {
|
||||||
|
FileUtils.delete(sha1File)
|
||||||
|
FileUtils.delete(dataFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val client = okHttpClient
|
||||||
|
|
||||||
|
// Save new sha1
|
||||||
|
val tempSha1File = File(dir, "$sha1Name.tmp")
|
||||||
|
check(save(client, sha1Url, tempSha1File))
|
||||||
|
|
||||||
|
// Check new sha1 and current data
|
||||||
|
if (checkData(tempSha1File, dataFile)) {
|
||||||
|
// The data is the same
|
||||||
|
FileUtils.delete(tempSha1File)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save new data
|
||||||
|
val tempDataFile = File(dir, "$dataName.tmp")
|
||||||
|
check(save(client, dataUrl, tempDataFile))
|
||||||
|
|
||||||
|
// Check new sha1 and new data
|
||||||
|
if (!checkData(tempSha1File, tempDataFile)) {
|
||||||
|
FileUtils.delete(tempSha1File)
|
||||||
|
FileUtils.delete(tempDataFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace current sha1 and current data with new sha1 and new data
|
||||||
|
FileUtils.delete(sha1File)
|
||||||
|
FileUtils.delete(dataFile)
|
||||||
|
tempSha1File.renameTo(sha1File)
|
||||||
|
tempDataFile.renameTo(dataFile)
|
||||||
|
|
||||||
|
// Read new EhTagDatabase
|
||||||
|
try {
|
||||||
|
dataFile.source().buffer().use { updateData(it) }
|
||||||
|
} catch (_: IOException) {
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
|
import com.hippo.network.UrlBuilder
|
||||||
|
|
||||||
|
object EhUrl {
|
||||||
|
const val SITE_E = 0
|
||||||
|
const val SITE_EX = 1
|
||||||
|
const val DOMAIN_EX = "exhentai.org"
|
||||||
|
const val DOMAIN_E = "e-hentai.org"
|
||||||
|
const val DOMAIN_LOFI = "lofi.e-hentai.org"
|
||||||
|
const val HOST_EX = "https://$DOMAIN_EX/"
|
||||||
|
const val API_EX = HOST_EX + "api.php"
|
||||||
|
const val URL_FAVORITES_EX = HOST_EX + "favorites.php"
|
||||||
|
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
|
||||||
|
const val URL_MY_TAGS_EX = HOST_EX + "mytags"
|
||||||
|
const val URL_WATCHED_EX = HOST_EX + "watched"
|
||||||
|
const val HOST_E = "https://$DOMAIN_E/"
|
||||||
|
const val API_E = HOST_E + "api.php"
|
||||||
|
const val URL_FAVORITES_E = HOST_E + "favorites.php"
|
||||||
|
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
|
||||||
|
const val URL_MY_TAGS_E = HOST_E + "mytags"
|
||||||
|
const val URL_WATCHED_E = HOST_E + "watched"
|
||||||
|
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
|
||||||
|
const val URL_POPULAR_E = "https://e-hentai.org/popular"
|
||||||
|
const val URL_POPULAR_EX = "https://exhentai.org/popular"
|
||||||
|
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
|
||||||
|
const val URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php"
|
||||||
|
const val URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login"
|
||||||
|
const val URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00"
|
||||||
|
const val URL_FORUMS = "https://forums.e-hentai.org/"
|
||||||
|
const val URL_FUNDS = HOST_E + "exchange.php?t=gp"
|
||||||
|
const val URL_HOME = HOST_E + "home.php"
|
||||||
|
const val URL_NEWS = HOST_E + "news.php"
|
||||||
|
const val REFERER_EX = "https://$DOMAIN_EX"
|
||||||
|
const val ORIGIN_EX = REFERER_EX
|
||||||
|
const val REFERER_E = "https://$DOMAIN_E"
|
||||||
|
const val ORIGIN_E = REFERER_E
|
||||||
|
const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
|
||||||
|
const val URL_PREFIX_THUMB_EX = "https://exhentai.org/t/"
|
||||||
|
|
||||||
|
val host: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> HOST_E
|
||||||
|
SITE_EX -> HOST_EX
|
||||||
|
else -> HOST_E
|
||||||
|
}
|
||||||
|
|
||||||
|
val favoritesUrl: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> URL_FAVORITES_E
|
||||||
|
SITE_EX -> URL_FAVORITES_EX
|
||||||
|
else -> URL_FAVORITES_E
|
||||||
|
}
|
||||||
|
|
||||||
|
val apiUrl: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> API_E
|
||||||
|
SITE_EX -> API_EX
|
||||||
|
else -> API_E
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val referer: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> REFERER_E
|
||||||
|
SITE_EX -> REFERER_EX
|
||||||
|
else -> REFERER_E
|
||||||
|
}
|
||||||
|
|
||||||
|
val origin: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> ORIGIN_E
|
||||||
|
SITE_EX -> ORIGIN_EX
|
||||||
|
else -> ORIGIN_E
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val uConfigUrl: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> URL_UCONFIG_E
|
||||||
|
SITE_EX -> URL_UCONFIG_EX
|
||||||
|
else -> URL_UCONFIG_E
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val myTagsUrl: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> URL_MY_TAGS_E
|
||||||
|
SITE_EX -> URL_MY_TAGS_EX
|
||||||
|
else -> URL_MY_TAGS_E
|
||||||
|
}
|
||||||
|
|
||||||
|
val popularUrl: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> URL_POPULAR_E
|
||||||
|
SITE_EX -> URL_POPULAR_EX
|
||||||
|
else -> URL_POPULAR_E
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageSearchUrl: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> URL_IMAGE_SEARCH_E
|
||||||
|
SITE_EX -> URL_IMAGE_SEARCH_EX
|
||||||
|
else -> URL_IMAGE_SEARCH_E
|
||||||
|
}
|
||||||
|
|
||||||
|
val watchedUrl: String
|
||||||
|
get() = when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> URL_WATCHED_E
|
||||||
|
SITE_EX -> URL_WATCHED_EX
|
||||||
|
else -> URL_WATCHED_E
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getGalleryDetailUrl(gid: Long, token: String?): String {
|
||||||
|
return getGalleryDetailUrl(gid, token, 0, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean): String {
|
||||||
|
val builder = UrlBuilder(host + "g/" + gid + '/' + token + '/')
|
||||||
|
if (index != 0) {
|
||||||
|
builder.addQuery("p", index)
|
||||||
|
}
|
||||||
|
if (allComment) {
|
||||||
|
builder.addQuery("hc", 1)
|
||||||
|
}
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getGalleryMultiPageViewerUrl(gid: Long, token: String): String {
|
||||||
|
val builder = UrlBuilder(host + "mpv/" + gid + '/' + token + '/')
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getPageUrl(gid: Long, index: Int, pToken: String): String {
|
||||||
|
return host + "s/" + pToken + '/' + gid + '-' + (index + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAddFavorites(gid: Long, token: String?): String {
|
||||||
|
return host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDownloadArchive(gid: Long, token: String?, or: String): String {
|
||||||
|
return host + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTagDefinitionUrl(tag: String): String {
|
||||||
|
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getThumbUrlPrefix(): String {
|
||||||
|
return when (Settings.getGallerySite()) {
|
||||||
|
SITE_E -> URL_PREFIX_THUMB_E
|
||||||
|
SITE_EX -> URL_PREFIX_THUMB_EX
|
||||||
|
else -> URL_PREFIX_THUMB_E
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import com.hippo.ehviewer.EhApplication.Companion.ehCookieStore
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
object EhUtils {
|
||||||
|
const val NONE = -1 // Use it for homepage
|
||||||
|
const val PRIVATE = 0x400
|
||||||
|
const val UNKNOWN = 0x800
|
||||||
|
|
||||||
|
// https://youtrack.jetbrains.com/issue/KT-4749
|
||||||
|
private const val BG_COLOR_DOUJINSHI = 0xfff44336u
|
||||||
|
private const val BG_COLOR_MANGA = 0xffff9800u
|
||||||
|
private const val BG_COLOR_ARTIST_CG = 0xfffbc02du
|
||||||
|
private const val BG_COLOR_GAME_CG = 0xff4caf50u
|
||||||
|
private const val BG_COLOR_WESTERN = 0xff8bc34au
|
||||||
|
private const val BG_COLOR_NON_H = 0xff2196f3u
|
||||||
|
private const val BG_COLOR_IMAGE_SET = 0xff3f51b5u
|
||||||
|
private const val BG_COLOR_COSPLAY = 0xff9c27b0u
|
||||||
|
private const val BG_COLOR_ASIAN_PORN = 0xff9575cdu
|
||||||
|
private const val BG_COLOR_MISC = 0xfff06292u
|
||||||
|
private const val BG_COLOR_UNKNOWN = 0xff000000u
|
||||||
|
|
||||||
|
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||||
|
private val PATTERN_TITLE_PREFIX = Pattern.compile(
|
||||||
|
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||||
|
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
|
||||||
|
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
|
||||||
|
Pattern.CASE_INSENSITIVE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val CATEGORY_VALUES = hashMapOf(
|
||||||
|
EhConfig.MISC to arrayOf("misc"),
|
||||||
|
EhConfig.DOUJINSHI to arrayOf("doujinshi"),
|
||||||
|
EhConfig.MANGA to arrayOf("manga"),
|
||||||
|
EhConfig.ARTIST_CG to arrayOf("artistcg", "Artist CG Sets", "Artist CG"),
|
||||||
|
EhConfig.GAME_CG to arrayOf("gamecg", "Game CG Sets", "Game CG"),
|
||||||
|
EhConfig.IMAGE_SET to arrayOf("imageset", "Image Sets", "Image Set"),
|
||||||
|
EhConfig.COSPLAY to arrayOf("cosplay"),
|
||||||
|
EhConfig.ASIAN_PORN to arrayOf("asianporn", "Asian Porn"),
|
||||||
|
EhConfig.NON_H to arrayOf("non-h"),
|
||||||
|
EhConfig.WESTERN to arrayOf("western"),
|
||||||
|
PRIVATE to arrayOf("private"),
|
||||||
|
UNKNOWN to arrayOf("unknown")
|
||||||
|
)
|
||||||
|
private val CATEGORY_STRINGS = CATEGORY_VALUES.entries.map { (k, v) -> v to k }
|
||||||
|
|
||||||
|
val isExHentai: Boolean
|
||||||
|
get() = Settings.getGallerySite() == EhUrl.SITE_EX
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getCategory(type: String?): Int {
|
||||||
|
for (entry in CATEGORY_STRINGS) {
|
||||||
|
for (str in entry.first)
|
||||||
|
if (str.equals(type, ignoreCase = true))
|
||||||
|
return entry.second
|
||||||
|
}
|
||||||
|
return UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCategory(type: Int): String {
|
||||||
|
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCategoryColor(category: Int): Int {
|
||||||
|
return when (category) {
|
||||||
|
EhConfig.DOUJINSHI -> BG_COLOR_DOUJINSHI
|
||||||
|
EhConfig.MANGA -> BG_COLOR_MANGA
|
||||||
|
EhConfig.ARTIST_CG -> BG_COLOR_ARTIST_CG
|
||||||
|
EhConfig.GAME_CG -> BG_COLOR_GAME_CG
|
||||||
|
EhConfig.WESTERN -> BG_COLOR_WESTERN
|
||||||
|
EhConfig.NON_H -> BG_COLOR_NON_H
|
||||||
|
EhConfig.IMAGE_SET -> BG_COLOR_IMAGE_SET
|
||||||
|
EhConfig.COSPLAY -> BG_COLOR_COSPLAY
|
||||||
|
EhConfig.ASIAN_PORN -> BG_COLOR_ASIAN_PORN
|
||||||
|
EhConfig.MISC -> BG_COLOR_MISC
|
||||||
|
else -> BG_COLOR_UNKNOWN
|
||||||
|
}.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun signOut() {
|
||||||
|
ehCookieStore.signOut()
|
||||||
|
Settings.putAvatar(null)
|
||||||
|
Settings.putDisplayName(null)
|
||||||
|
Settings.putGallerySite(EhUrl.SITE_E)
|
||||||
|
Settings.putNeedSignIn(true)
|
||||||
|
Settings.putSelectSite(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun needSignedIn(): Boolean {
|
||||||
|
return Settings.getNeedSignIn() && !ehCookieStore.hasSignedIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getSuitableTitle(gi: GalleryInfo): String {
|
||||||
|
return if (Settings.getShowJpnTitle()) {
|
||||||
|
if (TextUtils.isEmpty(gi.titleJpn)) gi.title else gi.titleJpn
|
||||||
|
} else {
|
||||||
|
if (TextUtils.isEmpty(gi.title)) gi.titleJpn else gi.title
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractTitle(fullTitle: String?): String? {
|
||||||
|
var title: String = fullTitle ?: return null
|
||||||
|
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("")
|
||||||
|
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("")
|
||||||
|
// Sometimes title is combined by romaji and english translation.
|
||||||
|
// Only need romaji.
|
||||||
|
// TODO But not sure every '|' means that
|
||||||
|
val index = title.indexOf('|')
|
||||||
|
if (index >= 0) {
|
||||||
|
title = title.substring(0, index)
|
||||||
|
}
|
||||||
|
return title.ifEmpty { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fixThumbUrl(url: String): String {
|
||||||
|
return EhUrl.getThumbUrlPrefix() +
|
||||||
|
url.removePrefix(EhUrl.URL_PREFIX_THUMB_E).removePrefix(EhUrl.URL_PREFIX_THUMB_EX)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun handleThumbUrlResolution(url: String?): String? {
|
||||||
|
if (null == url) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val resolution = when (Settings.getThumbResolution()) {
|
||||||
|
0 -> return url
|
||||||
|
1 -> "250"
|
||||||
|
2 -> "300"
|
||||||
|
else -> return url
|
||||||
|
}
|
||||||
|
val index1 = url.lastIndexOf('_')
|
||||||
|
val index2 = url.lastIndexOf('.')
|
||||||
|
return if (index1 >= 0 && index2 >= 0 && index1 < index2) {
|
||||||
|
url.substring(0, index1 + 1) + resolution + url.substring(index2)
|
||||||
|
} else {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.hippo.ehviewer.client.data
|
||||||
|
|
||||||
|
interface AbstractGalleryInfo {
|
||||||
|
var gid: Long
|
||||||
|
var token: String?
|
||||||
|
var title: String?
|
||||||
|
var titleJpn: String?
|
||||||
|
var thumb: String?
|
||||||
|
var category: Int
|
||||||
|
var posted: String?
|
||||||
|
var uploader: String?
|
||||||
|
var disowned: Boolean
|
||||||
|
var rating: Float
|
||||||
|
var rated: Boolean
|
||||||
|
var simpleTags: Array<String>?
|
||||||
|
var pages: Int
|
||||||
|
var thumbWidth: Int
|
||||||
|
var thumbHeight: Int
|
||||||
|
var spanSize: Int
|
||||||
|
var spanIndex: Int
|
||||||
|
var spanGroupIndex: Int
|
||||||
|
var simpleLanguage: String?
|
||||||
|
var favoriteSlot: Int
|
||||||
|
var favoriteName: String?
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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 androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
open class BaseGalleryInfo(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "GID")
|
||||||
|
override var gid: Long = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "TOKEN")
|
||||||
|
override var token: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "TITLE")
|
||||||
|
override var title: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "TITLE_JPN")
|
||||||
|
override var titleJpn: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "THUMB")
|
||||||
|
override var thumb: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "CATEGORY")
|
||||||
|
override var category: Int = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "POSTED")
|
||||||
|
override var posted: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "UPLOADER")
|
||||||
|
override var uploader: String? = null,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var disowned: Boolean = false,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "RATING")
|
||||||
|
override var rating: Float = 0f,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var rated: Boolean = false,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var simpleTags: Array<String>? = null,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var pages: Int = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var thumbWidth: Int = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var thumbHeight: Int = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var spanSize: Int = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var spanIndex: Int = 0,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var spanGroupIndex: Int = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "SIMPLE_LANGUAGE")
|
||||||
|
override var simpleLanguage: String? = null,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var favoriteSlot: Int = -2,
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
override var favoriteName: String? = null
|
||||||
|
|
||||||
|
) : GalleryInfo
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.hippo.ehviewer.client.EhUrl
|
||||||
|
import com.hippo.network.UrlBuilder
|
||||||
|
import com.hippo.util.encodeUTF8
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class FavListUrlBuilder(
|
||||||
|
private var mPrev: String? = null,
|
||||||
|
private var mNext: String? = null,
|
||||||
|
var jumpTo: String? = null,
|
||||||
|
var keyword: String? = null,
|
||||||
|
var favCat: Int = FAV_CAT_ALL,
|
||||||
|
) : Parcelable {
|
||||||
|
fun setIndex(index: String?, isNext: Boolean) {
|
||||||
|
mNext = index.takeIf { isNext }
|
||||||
|
mPrev = index.takeUnless { isNext }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): String {
|
||||||
|
val ub = UrlBuilder(EhUrl.favoritesUrl)
|
||||||
|
if (isValidFavCat(favCat)) {
|
||||||
|
ub.addQuery("favcat", favCat.toString())
|
||||||
|
} else if (favCat == FAV_CAT_ALL) {
|
||||||
|
ub.addQuery("favcat", "all")
|
||||||
|
}
|
||||||
|
keyword?.takeIf { it.isNotBlank() }?.let { ub.addQuery("f_search", encodeUTF8(it)) }
|
||||||
|
mPrev?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("prev", it) }
|
||||||
|
mNext?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("next", it) }
|
||||||
|
jumpTo?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("seek", it) }
|
||||||
|
return ub.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FAV_CAT_ALL = -1
|
||||||
|
const val FAV_CAT_LOCAL = -2
|
||||||
|
fun isValidFavCat(favCat: Int): Boolean {
|
||||||
|
return favCat in 0..9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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 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,50 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
@JvmField
|
||||||
|
var id: Long = 0,
|
||||||
|
@JvmField
|
||||||
|
var score: Int = 0,
|
||||||
|
@JvmField
|
||||||
|
var editable: Boolean = false,
|
||||||
|
@JvmField
|
||||||
|
var voteUpAble: Boolean = false,
|
||||||
|
@JvmField
|
||||||
|
var voteUpEd: Boolean = false,
|
||||||
|
@JvmField
|
||||||
|
var voteDownAble: Boolean = false,
|
||||||
|
@JvmField
|
||||||
|
var voteDownEd: Boolean = false,
|
||||||
|
@JvmField
|
||||||
|
var uploader: Boolean = false,
|
||||||
|
@JvmField
|
||||||
|
var voteState: String? = null,
|
||||||
|
@JvmField
|
||||||
|
var time: Long = 0,
|
||||||
|
@JvmField
|
||||||
|
var user: String? = null,
|
||||||
|
@JvmField
|
||||||
|
var comment: String? = null,
|
||||||
|
@JvmField
|
||||||
|
var lastEdited: Long = 0
|
||||||
|
) : Parcelable
|
||||||
Loaded 100 of 1031 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user