Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f66a463393 | ||
|
|
33fd89041c | ||
|
|
40271d744b | ||
|
|
e6eba042b9 | ||
|
|
aeb9b47228 | ||
|
|
8b2ea9a418 | ||
|
|
97bca3eecc | ||
|
|
d7c64f540b | ||
|
|
793ba815f7 | ||
|
|
d901326640 | ||
|
|
de2321ce8f | ||
|
|
39398365e5 | ||
|
|
baf8333b70 | ||
|
|
5c406ac8b0 | ||
|
|
f07ff61069 | ||
|
|
2163238f48 | ||
|
|
1f4b93bddd | ||
|
|
ab2c60824a | ||
|
|
dbe455350d | ||
|
|
78cb123af1 | ||
|
|
5cd2496f7f | ||
|
|
06d4abd916 | ||
|
|
881ad21ea2 | ||
|
|
257bfe843e | ||
|
|
49eb3bc234 | ||
|
|
3006e25f3d | ||
|
|
79fbe9cbd0 | ||
|
|
36b0eeef5a | ||
|
|
eeeeb3ca0d | ||
|
|
345dbc2b86 | ||
|
|
cca4adc6a3 | ||
|
|
8decbed291 | ||
|
|
960ac5d2fe | ||
|
|
20aef7f77c | ||
|
|
96d1ba780b | ||
|
|
8383821324 | ||
|
|
f46a5e4652 | ||
|
|
d8d2e8299d | ||
|
|
284af1e6fb | ||
|
|
c7576a3941 | ||
|
|
c216bc3b82 | ||
|
|
2ac3b179c0 | ||
|
|
aa2269fd9f | ||
|
|
793a17afea | ||
|
|
dab6dcc04d | ||
|
|
be398a6ab6 | ||
|
|
80cffe479f | ||
|
|
30c5fe38c4 | ||
|
|
87b538d703 | ||
|
|
affe4ffbf1 | ||
|
|
3d74869187 | ||
|
|
58d90ff49c | ||
|
|
07217f1481 | ||
|
|
cf87df35a6 | ||
|
|
c69cb76ecd | ||
|
|
0ad0cc771f | ||
|
|
ca8c6883f9 | ||
|
|
812d9d9860 | ||
|
|
f2673ddd9a | ||
|
|
a6f789f446 | ||
|
|
bba6ea4b3c | ||
|
|
ae6c664f03 | ||
|
|
2b3755a09c | ||
|
|
efde6e9f3d | ||
|
|
cff64839bb | ||
|
|
d199b58f89 | ||
|
|
8005f9be56 | ||
|
|
c13186da78 | ||
|
|
f407d38421 | ||
|
|
19af89d58a | ||
|
|
b7b2d07d60 | ||
|
|
e8bba2a2e5 | ||
|
|
f8e01265d4 | ||
|
|
e0c5763a47 | ||
|
|
fe3fd59083 | ||
|
|
09693f91c7 | ||
|
|
b69500a441 | ||
|
|
f344a9a0a5 | ||
|
|
ef62454c00 | ||
|
|
fe2cd1b4f4 | ||
|
|
19b9fa3581 | ||
|
|
431a8565ee | ||
|
|
3880f757a5 | ||
|
|
acfc81056b | ||
|
|
5f1200a9b3 | ||
|
|
f370ae64ad | ||
|
|
509c818ed0 | ||
|
|
124d1ff638 | ||
|
|
0348eba0fd | ||
|
|
1d27292bf1 | ||
|
|
1619f6cce6 | ||
|
|
489538baaa | ||
|
|
8794b7cc4d | ||
|
|
ebd114d10e | ||
|
|
f17dd93f27 | ||
|
|
899c2e14c8 | ||
|
|
ae50280fb1 | ||
|
|
a2bd809fbc | ||
|
|
963f129bcf | ||
|
|
37c0148d0b | ||
|
|
1fa485e31f | ||
|
|
dceea3b2ff | ||
|
|
bab7770857 | ||
|
|
af9251db0c | ||
|
|
010b990f8d | ||
|
|
0e61e4b00a | ||
|
|
66e43924bd | ||
|
|
3437bb88a2 | ||
|
|
f6e0733080 | ||
|
|
add2676292 | ||
|
|
95839dbb60 | ||
|
|
6dbc2b2e67 | ||
|
|
fc89ca007e | ||
|
|
a75fba27cd | ||
|
|
c2f1233360 | ||
|
|
4e3cadc778 | ||
|
|
73f544c14e | ||
|
|
025a947bb0 | ||
|
|
8927074cf1 | ||
|
|
da6ffe7304 | ||
|
|
956a3ac1fd | ||
|
|
f6b43cb7e3 | ||
|
|
47f3406271 | ||
|
|
4ac4d3da01 | ||
|
|
ea9ed2dc67 | ||
|
|
10ad0b6449 | ||
|
|
8b02116dda | ||
|
|
1578da3b02 | ||
|
|
c77451cae9 | ||
|
|
41b503918b | ||
|
|
5f40d8d474 | ||
|
|
3109a6258b | ||
|
|
cacf578615 | ||
|
|
bb42a84b44 | ||
|
|
d6519d4819 | ||
|
|
df5db83337 | ||
|
|
af4b751425 | ||
|
|
58209b0420 | ||
|
|
d4c39fd72f | ||
|
|
24505cb77c | ||
|
|
80225bfe6c | ||
|
|
14b24d6c58 | ||
|
|
3c10d3fe00 | ||
|
|
fd6a1fd1ec | ||
|
|
325578b989 | ||
|
|
705e39a50d | ||
|
|
5ac3f4cd44 | ||
|
|
80c3c697d4 | ||
|
|
8cc597836e | ||
|
|
9a214c939b | ||
|
|
11a3201e54 | ||
|
|
5156bd7747 | ||
|
|
e62738519c | ||
|
|
d87de0d3e2 | ||
|
|
2aa2c25cdd | ||
|
|
123a7cd8e3 | ||
|
|
72a46acb46 | ||
|
|
50815c6e91 | ||
|
|
2c6f98b582 | ||
|
|
240bc5d0ae | ||
|
|
e4306a7303 | ||
|
|
15179a82e3 | ||
|
|
1e3b2bebb3 | ||
|
|
2c32cafd38 | ||
|
|
ddd11477f7 | ||
|
|
f3b2d8495a | ||
|
|
a56dd32425 | ||
|
|
873b5e8540 | ||
|
|
34719b599e | ||
|
|
a3a3a7b46d | ||
|
|
1a46f75693 | ||
|
|
90b4d7777e | ||
|
|
c363f4df51 | ||
|
|
4dc0981b34 | ||
|
|
e2772bde04 | ||
|
|
f1666d7a64 |
No files matched your search
File renamed without changes.
File renamed without changes.
@@ -13,17 +13,8 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
baseline-profile:
|
baseline-profile:
|
||||||
runs-on: ubuntu-latest
|
runs-on: nekomoee
|
||||||
steps:
|
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
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
@@ -31,13 +22,19 @@ jobs:
|
|||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 21
|
java-version-file: .java-version
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Setup Android SDK
|
||||||
run: rustup target add armv7-linux-androideabi
|
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
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: https://github.com/Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: "./app/src/main/rust"
|
workspaces: "./app/src/main/rust"
|
||||||
|
|
||||||
@@ -48,7 +45,7 @@ jobs:
|
|||||||
key: cmake-
|
key: cmake-
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v6
|
||||||
with:
|
with:
|
||||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||||
cache-read-only: true
|
cache-read-only: true
|
||||||
@@ -59,7 +56,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload reports
|
- name: Upload reports
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: reports
|
name: reports
|
||||||
path: benchmark/build/reports
|
path: benchmark/build/reports
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '*'
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: temurin
|
|
||||||
java-version: 21
|
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
|
||||||
run: rustup target add armv7-linux-androideabi
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: 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@v5
|
|
||||||
with:
|
|
||||||
cache-read-only: true
|
|
||||||
|
|
||||||
- name: Check build-logic
|
|
||||||
run: ./gradlew :build-logic:convention:check
|
|
||||||
|
|
||||||
- name: Spotless Check
|
|
||||||
run: ./gradlew spotlessCheck
|
|
||||||
|
|
||||||
- name: Lint Check
|
|
||||||
run: ./gradlew lintMarshmallowRelease
|
|
||||||
|
|
||||||
default:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: temurin
|
|
||||||
java-version: 21
|
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
|
||||||
run: rustup target add armv7-linux-androideabi
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: 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@v5
|
|
||||||
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-
|
|
||||||
|
|
||||||
# https://github.com/mikepenz/AboutLibraries/issues/1225
|
|
||||||
- name: Generate aboutlibraries.json
|
|
||||||
run: ./gradlew :app:prepareLibraryDefinitionsDefaultRelease --rerun
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: ./gradlew assembleDefaultRelease
|
|
||||||
|
|
||||||
- name: Upload ARM64
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
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@v6
|
|
||||||
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@v6
|
|
||||||
with:
|
|
||||||
name: default-armeabi-v7a-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
|
|
||||||
|
|
||||||
- name: Upload mapping
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: default-mapping-${{ github.sha }}
|
|
||||||
path: app/build/outputs/mapping/defaultRelease
|
|
||||||
|
|
||||||
- name: Upload native debug symbols
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: default-native-debug-symbols-${{ github.sha }}
|
|
||||||
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
|
|
||||||
|
|
||||||
marshmallow:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: temurin
|
|
||||||
java-version: 21
|
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
|
||||||
run: rustup target add armv7-linux-androideabi
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: 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@v5
|
|
||||||
with:
|
|
||||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
|
||||||
|
|
||||||
- name: CMake Cache
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: app/.cxx
|
|
||||||
key: cmake-marshmallow-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
|
|
||||||
restore-keys: cmake-marshmallow-
|
|
||||||
|
|
||||||
# https://github.com/mikepenz/AboutLibraries/issues/1225
|
|
||||||
- name: Generate aboutlibraries.json
|
|
||||||
run: ./gradlew :app:prepareLibraryDefinitionsMarshmallowRelease --rerun
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: ./gradlew assembleMarshmallowRelease
|
|
||||||
|
|
||||||
- name: Upload ARM64
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: marshmallow-arm64-v8a-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk
|
|
||||||
|
|
||||||
- name: Upload x86_64
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: marshmallow-x86_64-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk
|
|
||||||
|
|
||||||
- name: Upload ARM32
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: marshmallow-armeabi-v7a-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
|
|
||||||
|
|
||||||
- name: Upload mapping
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: marshmallow-mapping-${{ github.sha }}
|
|
||||||
path: app/build/outputs/mapping/marshmallowRelease
|
|
||||||
|
|
||||||
- name: Upload native debug symbols
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: marshmallow-native-debug-symbols-${{ github.sha }}
|
|
||||||
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
name: 'Lock Threads'
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 8 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: lock-threads
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
action:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: dessant/lock-threads@v6
|
|
||||||
with:
|
|
||||||
issue-inactive-days: '1'
|
|
||||||
process-only: 'issues'
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
name: Releases
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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: 21
|
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
|
||||||
run: rustup target add armv7-linux-androideabi
|
|
||||||
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: 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@v5
|
|
||||||
with:
|
|
||||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
|
||||||
cache-read-only: true
|
|
||||||
|
|
||||||
- name: Generate Baseline Profile
|
|
||||||
run: ./gradlew generateBaselineProfile
|
|
||||||
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
|
||||||
|
|
||||||
# https://github.com/mikepenz/AboutLibraries/issues/1225
|
|
||||||
- name: Generate aboutlibraries.json
|
|
||||||
run: ./gradlew
|
|
||||||
:app:prepareLibraryDefinitionsDefaultRelease --rerun
|
|
||||||
:app:prepareLibraryDefinitionsMarshmallowRelease --rerun
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: ./gradlew assembleRelease
|
|
||||||
|
|
||||||
- name: Rename Apks
|
|
||||||
run: |
|
|
||||||
mv app/build/outputs/apk/default/release/app-default-universal-release.apk EhViewer-${{ github.ref_name }}-default-universal.apk
|
|
||||||
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
|
||||||
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-x86_64.apk
|
|
||||||
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
|
|
||||||
mv app/build/outputs/mapping/defaultRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-mapping.txt
|
|
||||||
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
|
||||||
|
|
||||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
|
|
||||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
|
|
||||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
|
|
||||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
|
|
||||||
mv app/build/outputs/mapping/marshmallowRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
|
||||||
mv app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
|
||||||
|
|
||||||
- name: Releases
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
body: Bump Version
|
|
||||||
files: |
|
|
||||||
EhViewer-${{ github.ref_name }}-default-universal.apk
|
|
||||||
EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
|
||||||
EhViewer-${{ github.ref_name }}-default-x86_64.apk
|
|
||||||
EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
|
|
||||||
EhViewer-${{ github.ref_name }}-default-mapping.txt
|
|
||||||
EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
|
||||||
|
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
|
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
|
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
|
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
|
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
25
|
||||||
@@ -1,103 +1,25 @@
|
|||||||
<p align="right">
|
|
||||||
<strong>English</strong>
|
|
||||||
<span> | </span>
|
|
||||||
<a href="/docs/README/zh-cn.md">
|
|
||||||
简体中文
|
|
||||||
</a>
|
|
||||||
<span> | </span>
|
|
||||||
<a href="/docs/README/zh-tw.md">
|
|
||||||
正體中文
|
|
||||||
</a>
|
|
||||||
<span> | </span>
|
|
||||||
<a href="/docs/README/ja.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/FooIbar/EhViewer-art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
|
||||||
<br>EhViewer<br>
|
<br>EhViewer<br>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
|
Standalone fork of
|
||||||
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
|
<a href="https://codeberg.org/EhViewer/EhViewer">EhViewer (codeberg)</a>
|
||||||
</a>
|
maintained at <a href="https://gitea.yrkaierx.com/yenru0/EhViewer">gitea.yrkaierx.com/yenru0/EhViewer</a>
|
||||||
<a href="/LICENSE">
|
|
||||||
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
|
|
||||||
</a>
|
|
||||||
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
|
|
||||||
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
|
||||||
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/FooIbar/EhViewer/issues">
|
|
||||||
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
# Changes over upstream
|
||||||
<h3>
|
|
||||||
<a href="#description">
|
|
||||||
Description
|
|
||||||
</a>
|
|
||||||
<span> | </span>
|
|
||||||
<a href="#download">
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
<span> | </span>
|
|
||||||
<a href="#screenshot">
|
|
||||||
Screenshot
|
|
||||||
</a>
|
|
||||||
<span> | </span>
|
|
||||||
<a href="#thanks">
|
|
||||||
Thanks
|
|
||||||
</a>
|
|
||||||
<span> | </span>
|
|
||||||
<a href="#license">
|
|
||||||
License
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
# Description
|
- 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
|
||||||
|
|
||||||
A modern EhViewer fork dedicated to high-performance
|
# Upstream
|
||||||
|
|
||||||
with [Material Design 3](https://m3.material.io/)
|
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.
|
||||||
and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview) Support
|
|
||||||
|
|
||||||
# Download
|
|
||||||
|
|
||||||
| Flavor | Minimum Android Version | Notes |
|
|
||||||
|-------------|-------------------------|--------------------------------|
|
|
||||||
| Default | 8.0 | Full support |
|
|
||||||
| Marshmallow | 6.0* | Limited support, no guarantees |
|
|
||||||
|
|
||||||
*Devices running Android 6 need to have the [ISRG Root X1](https://letsencrypt.org/certs/isrgrootx1.pem) certificate installed
|
|
||||||
|
|
||||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
|
||||||
<img alt="Get it on GitHub" src="https://github.com/FooIbar/EhViewer-art/blob/master/get-it-on-github.svg" width="200px"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
# Screenshot
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
# Thanks
|
|
||||||
|
|
||||||
Here is the libraries
|
|
||||||
|
|
||||||
- [Arrow](https://arrow-kt.io/)
|
|
||||||
- [AOSP & AndroidX](https://source.android.com/)
|
|
||||||
- [Kotlin & KotlinX](https://kotlinlang.org/)
|
|
||||||
- [Material Icons](https://github.com/google/material-design-icons)
|
|
||||||
- [Ktor](https://ktor.io/)
|
|
||||||
- [Coil](https://coil-kt.github.io/coil/)
|
|
||||||
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
|
|
||||||
- [libarchive](https://www.libarchive.org/)
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
|
|||||||
+12
-7
@@ -49,15 +49,20 @@ android {
|
|||||||
|
|
||||||
val repoName = providers.exec {
|
val repoName = providers.exec {
|
||||||
commandLine = "git remote get-url origin".split(' ')
|
commandLine = "git remote get-url origin".split(' ')
|
||||||
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
|
}.standardOutput.asText.get().trim().removeSuffix(".git").removePrefix("https://codeberg.org/")
|
||||||
.removeSuffix(".git")
|
|
||||||
|
val snapshot = !hasProperty("release")
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "moe.tarsin.ehviewer"
|
applicationId = "yenru0.yrkaierx.ehviewer"
|
||||||
versionCode = 180063
|
versionCode = 180064
|
||||||
versionName = "1.14.6"
|
versionName = if (snapshot) {
|
||||||
// versionNameSuffix = "-SNAPSHOT"
|
"1.15.0-SNAPSHOT"
|
||||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
|
} else {
|
||||||
|
"1.15.0"
|
||||||
|
}
|
||||||
|
buildConfigField("boolean", "SNAPSHOT", "$snapshot")
|
||||||
|
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||||
buildConfigField("long", "COMMIT_TIME", commitTime)
|
buildConfigField("long", "COMMIT_TIME", commitTime)
|
||||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||||
|
|||||||
Vendored
+4
@@ -15,6 +15,10 @@
|
|||||||
java.nio.ByteBuffer source;
|
java.nio.ByteBuffer source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
|
||||||
|
static boolean isLinkBufferComposerEnabled() return true;
|
||||||
|
}
|
||||||
|
|
||||||
# Ktor logger
|
# Ktor logger
|
||||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
|
|
||||||
|
|||||||
@@ -118,10 +118,6 @@
|
|||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".ui.SecurityActivity"
|
|
||||||
android:configChanges="screenSize"
|
|
||||||
android:label="@string/settings" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".shortcuts.ShortcutsActivity"
|
android:name=".shortcuts.ShortcutsActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ option(XZ_TOOL_XZDEC OFF)
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
liblzma
|
liblzma
|
||||||
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
|
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
|
||||||
GIT_TAG v5.8.1
|
GIT_TAG v5.8.3
|
||||||
GIT_SHALLOW 1
|
GIT_SHALLOW 1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
|
|||||||
else ()
|
else ()
|
||||||
set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin")
|
set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin")
|
||||||
endif ()
|
endif ()
|
||||||
set(CONFIGURE_COMMAND ./configure --disable-public-key --disable-dependency-tracking
|
set(CONFIGURE_COMMAND ${WSL} ./.bootstrap && ${WSL} ./configure --disable-public-key --disable-dependency-tracking
|
||||||
"--host=${CMAKE_C_COMPILER_TARGET}"
|
"--host=${CMAKE_C_COMPILER_TARGET}"
|
||||||
"CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments"
|
"CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments"
|
||||||
"AR=${TOOLCHAIN}/llvm-ar"
|
"AR=${TOOLCHAIN}/llvm-ar"
|
||||||
@@ -56,9 +56,11 @@ set(CONFIGURE_COMMAND ./configure --disable-public-key --disable-dependency-trac
|
|||||||
)
|
)
|
||||||
ExternalProject_Add(
|
ExternalProject_Add(
|
||||||
nettle
|
nettle
|
||||||
URL https://ftpmirror.gnu.org/nettle/nettle-3.10.2.tar.gz
|
GIT_REPOSITORY https://gitlab.com/gnutls/nettle.git
|
||||||
URL_MD5 b28bcbf6f045ff007940a9401673600d
|
GIT_TAG nettle_4.0_release_20260205
|
||||||
CONFIGURE_COMMAND ${WSL} ${CONFIGURE_COMMAND}
|
GIT_SHALLOW 1
|
||||||
|
GIT_CONFIG core.autocrlf=input
|
||||||
|
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
|
||||||
BUILD_COMMAND ${WSL} make -j4 libnettle.a
|
BUILD_COMMAND ${WSL} make -j4 libnettle.a
|
||||||
INSTALL_COMMAND ""
|
INSTALL_COMMAND ""
|
||||||
BUILD_IN_SOURCE 1
|
BUILD_IN_SOURCE 1
|
||||||
@@ -101,7 +103,7 @@ set(LIBARCHIVE_PATCH
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
libarchive
|
libarchive
|
||||||
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
||||||
GIT_TAG v3.8.4
|
GIT_TAG v3.8.7
|
||||||
GIT_SHALLOW 1
|
GIT_SHALLOW 1
|
||||||
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
|
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
|
||||||
)
|
)
|
||||||
@@ -138,8 +140,8 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ../../../../rust-
|
|||||||
unset(Rust_TOOLCHAIN CACHE)
|
unset(Rust_TOOLCHAIN CACHE)
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
Corrosion
|
Corrosion
|
||||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
GIT_REPOSITORY https://github.com/FooIbar/corrosion.git
|
||||||
GIT_TAG v0.6
|
GIT_TAG v0.6.1
|
||||||
GIT_SHALLOW 1
|
GIT_SHALLOW 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Java_com_hippo_ehviewer_jni_HashKt_sha1(JNIEnv *env, jclass clazz, jint fd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
byte digest[SHA1_DIGEST_SIZE];
|
byte digest[SHA1_DIGEST_SIZE];
|
||||||
sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest);
|
sha1_digest(&ctx, digest);
|
||||||
|
|
||||||
byte byte;
|
byte byte;
|
||||||
char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
|
char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ import com.hippo.ehviewer.ktor.configureClient
|
|||||||
import com.hippo.ehviewer.ktor.configureCommon
|
import com.hippo.ehviewer.ktor.configureCommon
|
||||||
import com.hippo.ehviewer.ktor.isCronetAvailable
|
import com.hippo.ehviewer.ktor.isCronetAvailable
|
||||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||||
import com.hippo.ehviewer.ui.lockObserver
|
|
||||||
import com.hippo.ehviewer.ui.screen.detailCache
|
import com.hippo.ehviewer.ui.screen.detailCache
|
||||||
import com.hippo.ehviewer.ui.tools.dataStateFlow
|
import com.hippo.ehviewer.ui.tools.dataStateFlow
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
@@ -109,7 +108,6 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lifecycle.addObserver(lockObserver)
|
|
||||||
CrashHandler.install()
|
CrashHandler.install()
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
System.loadLibrary("ehviewer")
|
System.loadLibrary("ehviewer")
|
||||||
@@ -124,6 +122,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
}
|
}
|
||||||
launchIO {
|
launchIO {
|
||||||
EhTagDatabase.launchUpdate()
|
EhTagDatabase.launchUpdate()
|
||||||
|
@Suppress("UNUSED_EXPRESSION")
|
||||||
launch { EhDB }
|
launch { EhDB }
|
||||||
launch { dataStateFlow.value }
|
launch { dataStateFlow.value }
|
||||||
launch { OSUtils.totalMemory }
|
launch { OSUtils.totalMemory }
|
||||||
@@ -197,7 +196,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
add(GifDecoder.Factory())
|
add(GifDecoder.Factory())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
diskCache { imageCache }
|
diskCache { thumbCache }
|
||||||
crossfade(300)
|
crossfade(300)
|
||||||
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
||||||
if (drawable != null) error(drawable.asImage(true))
|
if (drawable != null) error(drawable.asImage(true))
|
||||||
@@ -220,7 +219,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
val ktorClient by lazy {
|
val ktorClient by lazy {
|
||||||
if (isCronetAvailable && Settings.enableCronet.value) {
|
if (isCronetAvailable && Settings.enableQuic.value) {
|
||||||
HttpClient(Cronet) {
|
HttpClient(Cronet) {
|
||||||
engine { configureClient(Settings.enableQuic.value) }
|
engine { configureClient(Settings.enableQuic.value) }
|
||||||
configureCommon()
|
configureCommon()
|
||||||
@@ -239,10 +238,10 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageCache by lazy {
|
val thumbCache by lazy {
|
||||||
diskCache {
|
diskCache {
|
||||||
directory(appCtx.cacheDir.toOkioPath() / "image_cache")
|
directory(appCtx.cacheDir.toOkioPath() / "thumb")
|
||||||
maxSizeBytes(Settings.readCacheSize.value.coerceIn(320, 5120).toLong() * 1024 * 1024)
|
maxSizeBytes(80L * 1024 * 1024)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.hippo.ehviewer
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.client.hosts
|
||||||
|
import com.hippo.ehviewer.client.hostsDsl
|
||||||
|
|
||||||
|
val builtInHosts = hostsDsl {
|
||||||
|
hosts("exhentai.org") {
|
||||||
|
"178.175.128.251" blockedInCN false
|
||||||
|
"178.175.128.252" blockedInCN false
|
||||||
|
"178.175.128.253" blockedInCN false
|
||||||
|
"178.175.128.254" blockedInCN false
|
||||||
|
"178.175.129.251" blockedInCN false
|
||||||
|
"178.175.129.252" blockedInCN false
|
||||||
|
"178.175.129.253" blockedInCN false
|
||||||
|
"178.175.129.254" blockedInCN false
|
||||||
|
"178.175.132.19" blockedInCN false
|
||||||
|
"178.175.132.20" blockedInCN false
|
||||||
|
"178.175.132.21" blockedInCN false
|
||||||
|
"178.175.132.22" blockedInCN false
|
||||||
|
}
|
||||||
|
hosts("api.github.com") {
|
||||||
|
"140.82.114.5" blockedInCN false
|
||||||
|
"140.82.114.6" blockedInCN false
|
||||||
|
"140.82.116.5" blockedInCN false
|
||||||
|
"140.82.116.6" blockedInCN false
|
||||||
|
"140.82.121.5" blockedInCN true
|
||||||
|
"140.82.121.6" blockedInCN true
|
||||||
|
"20.200.245.245" blockedInCN false
|
||||||
|
"20.201.28.148" blockedInCN false
|
||||||
|
"20.205.243.168" blockedInCN false
|
||||||
|
"20.26.156.210" blockedInCN false
|
||||||
|
"20.27.177.116" blockedInCN false
|
||||||
|
"20.87.245.6" blockedInCN false
|
||||||
|
"4.237.22.34" blockedInCN false
|
||||||
|
}
|
||||||
|
hosts("github.com") {
|
||||||
|
"140.82.113.4" blockedInCN false
|
||||||
|
"140.82.114.3" blockedInCN false
|
||||||
|
"140.82.116.4" blockedInCN false
|
||||||
|
"140.82.121.3" blockedInCN true
|
||||||
|
"140.82.121.4" blockedInCN true
|
||||||
|
"20.200.245.247" blockedInCN false
|
||||||
|
"20.201.28.151" blockedInCN false
|
||||||
|
"20.205.243.166" blockedInCN false
|
||||||
|
"20.26.156.215" blockedInCN false
|
||||||
|
"20.27.177.113" blockedInCN false
|
||||||
|
"20.87.245.0" blockedInCN false
|
||||||
|
"4.237.22.38" blockedInCN false
|
||||||
|
}
|
||||||
|
hosts("raw.githubusercontent.com") {
|
||||||
|
"185.199.108.133" blockedInCN false
|
||||||
|
"185.199.109.133" blockedInCN false
|
||||||
|
"185.199.110.133" blockedInCN false
|
||||||
|
"185.199.111.133" blockedInCN false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ import com.ehviewer.core.preferences.edit
|
|||||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||||
import com.hippo.ehviewer.download.DownloadsFilterMode
|
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||||
import com.hippo.ehviewer.download.SortMode
|
import com.hippo.ehviewer.download.SortMode
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -112,7 +111,7 @@ object Settings : DataStorePreferences(null) {
|
|||||||
val saveCrashLog = boolPref("save_crash_log", true)
|
val saveCrashLog = boolPref("save_crash_log", true)
|
||||||
val readCacheSize = intPref("read_cache_size_2", 640)
|
val readCacheSize = intPref("read_cache_size_2", 640)
|
||||||
val enableCronet = boolPref("enable_cronet", true)
|
val enableCronet = boolPref("enable_cronet", true)
|
||||||
val enableQuic = boolPref("enable_quic", true)
|
val enableQuic = boolPref("enable_quic", false)
|
||||||
val hardwareBitmapThreshold = intPref("hardware_bitmap_threshold", 16384)
|
val hardwareBitmapThreshold = intPref("hardware_bitmap_threshold", 16384)
|
||||||
val preloadThumbAggressively = boolPref("preload_thumb_aggressively", false)
|
val preloadThumbAggressively = boolPref("preload_thumb_aggressively", false)
|
||||||
val animateItems = boolPref("animate_items", true)
|
val animateItems = boolPref("animate_items", true)
|
||||||
@@ -120,8 +119,8 @@ object Settings : DataStorePreferences(null) {
|
|||||||
|
|
||||||
// About
|
// About
|
||||||
val backupBeforeUpdate = boolPref("backup_before_update", false)
|
val backupBeforeUpdate = boolPref("backup_before_update", false)
|
||||||
val useCIUpdateChannel = boolPref("ci_update_channel", AppConfig.isSnapshot)
|
val useCIUpdateChannel = boolPref("ci_update_channel", BuildConfig.SNAPSHOT)
|
||||||
val updateIntervalDays = intPref("update_interval_days", 7)
|
val updateIntervalDays = intPref("update_interval_days", 0)
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
val languageFilter = intPref("language_filter", -1)
|
val languageFilter = intPref("language_filter", -1)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
|
import com.ehviewer.core.util.isAtLeastQ
|
||||||
|
import com.hippo.ehviewer.builtInHosts
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
import okhttp3.AsyncDns
|
||||||
|
import okhttp3.Dns
|
||||||
|
import okhttp3.android.AndroidAsyncDns
|
||||||
|
|
||||||
|
private typealias HostsMap = MutableMap<String, List<InetAddress>>
|
||||||
|
|
||||||
|
const val CFSUFFIX = ".cdn.cloudflare.net"
|
||||||
|
|
||||||
|
val cloudflaredDomains = listOf(
|
||||||
|
"e-hentai.org",
|
||||||
|
"api.e-hentai.org",
|
||||||
|
"upload.e-hentai.org",
|
||||||
|
"forums.e-hentai.org",
|
||||||
|
"exhentai.org",
|
||||||
|
"s.exhentai.org",
|
||||||
|
"testingcf.jsdelivr.net",
|
||||||
|
)
|
||||||
|
|
||||||
|
val dFEnabledDomains = listOf(
|
||||||
|
"github.com", "api.github.com",
|
||||||
|
"ehgt.org", "gt0.ehgt.org", "gt1.ehgt.org", "gt2.ehgt.org", "gt3.ehgt.org", "ul.ehgt.org",
|
||||||
|
"e-hentai.org", "api.e-hentai.org", "forums.e-hentai.org", "repo.e-hentai.org", "upload.e-hentai.org",
|
||||||
|
"exhentai.org", "s.exhentai.org",
|
||||||
|
)
|
||||||
|
|
||||||
|
val dohSkipDomains = listOf(
|
||||||
|
"www.recaptcha.net",
|
||||||
|
"www.gstatic.cn",
|
||||||
|
"www.gstatic.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun hostsDsl(builder: HostsMap.() -> Unit): HostsMap = mutableMapOf<String, List<InetAddress>>().apply(builder)
|
||||||
|
|
||||||
|
fun interface HostMapBuilder {
|
||||||
|
infix fun String.blockedInCN(boolean: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HostsMap.hosts(vararg hosts: String, builder: HostMapBuilder.() -> Unit) = apply {
|
||||||
|
hosts.forEach { host ->
|
||||||
|
fun String.toInetAddress() = InetAddress.getByName(this).let { InetAddress.getByAddress(host, it.address) }
|
||||||
|
mutableListOf<InetAddress>().apply {
|
||||||
|
HostMapBuilder { if (!it) add(toInetAddress()) }.apply(builder)
|
||||||
|
put(host, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val systemDns = if (isAtLeastQ) AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6) else Dns.SYSTEM
|
||||||
|
|
||||||
|
object EhDns : Dns {
|
||||||
|
override fun lookup(hostname: String): List<InetAddress> = when {
|
||||||
|
(hostname in "exhentai.org") ->
|
||||||
|
builtInHosts[hostname] ?: systemDns.lookup("s.exhentai.org$CFSUFFIX")
|
||||||
|
(hostname in cloudflaredDomains) ->
|
||||||
|
builtInHosts[hostname] ?: systemDns.lookup("$hostname$CFSUFFIX")
|
||||||
|
(hostname in dohSkipDomains) ->
|
||||||
|
systemDns.lookup(hostname)
|
||||||
|
else ->
|
||||||
|
builtInHosts[hostname] ?: systemDns.lookup(hostname)
|
||||||
|
}.shuffled()
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ private fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuff
|
|||||||
{ !it.hasRemaining() },
|
{ !it.hasRemaining() },
|
||||||
)
|
)
|
||||||
if (empty) {
|
if (empty) {
|
||||||
val message = if (EhUtils.isExHentai) {
|
val message = if (response.request.url.host == EhUrl.DOMAIN_EX) {
|
||||||
"Sad Panda\n(without panda)"
|
"Sad Panda\n(without panda)"
|
||||||
} else {
|
} else {
|
||||||
appCtx.getString(R.string.error_ip_banned)
|
appCtx.getString(R.string.error_ip_banned)
|
||||||
@@ -194,7 +194,12 @@ private suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline
|
|||||||
|
|
||||||
object EhEngine {
|
object EhEngine {
|
||||||
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
|
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
|
||||||
response.headers["Location"]?.takeUnless { Url(it).isLogin } ?: throw InsufficientGpException()
|
val location = response.headers["Location"] ?: run {
|
||||||
|
val body = response.bodyAsUtf8Text()
|
||||||
|
rethrowExactly(response, body.left(), InsufficientGpException())
|
||||||
|
}
|
||||||
|
if (Url(location).isLogin) throw InsufficientGpException()
|
||||||
|
location
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPTokenListFromMpv(gid: Long, token: String) = ehRequest(EhUrl.getGalleryMultiPageViewerUrl(gid, token), EhUrl.referer)
|
suspend fun getPTokenListFromMpv(gid: Long, token: String) = ehRequest(EhUrl.getGalleryMultiPageViewerUrl(gid, token), EhUrl.referer)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.Socket
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.SSLSocket
|
||||||
|
import javax.net.ssl.SSLSocketFactory
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
const val EXCEPTIONAL_DOMAIN = "hath.network"
|
||||||
|
private val sslSocketFactory: SSLSocketFactory = SSLContext.getInstance("TLS").apply {
|
||||||
|
init(null, null, null)
|
||||||
|
}.socketFactory
|
||||||
|
|
||||||
|
object EhSSLSocketFactory : SSLSocketFactory() {
|
||||||
|
override fun getDefaultCipherSuites(): Array<String> = sslSocketFactory.defaultCipherSuites
|
||||||
|
override fun getSupportedCipherSuites(): Array<String> = sslSocketFactory.supportedCipherSuites
|
||||||
|
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket = sslSocketFactory.createSocket(s, resolveHost(s, host), port, autoClose) as SSLSocket
|
||||||
|
|
||||||
|
override fun createSocket(host: String, port: Int): Socket = sslSocketFactory.createSocket(host, port)
|
||||||
|
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket = sslSocketFactory.createSocket(host, port, localHost, localPort)
|
||||||
|
override fun createSocket(host: InetAddress, port: Int): Socket = sslSocketFactory.createSocket(host, port)
|
||||||
|
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket = sslSocketFactory.createSocket(address, port, localAddress, localPort)
|
||||||
|
|
||||||
|
private fun resolveHost(socket: Socket, host: String): String = socket.inetAddress.hostAddress.takeIf {
|
||||||
|
host in dFEnabledDomains || EXCEPTIONAL_DOMAIN in host
|
||||||
|
} ?: host
|
||||||
|
}
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.install(sslSocketFactory: SSLSocketFactory) = apply {
|
||||||
|
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())!!
|
||||||
|
factory.init(null as KeyStore?)
|
||||||
|
val manager = factory.trustManagers!!
|
||||||
|
val trustManager = manager.filterIsInstance<X509TrustManager>().first()
|
||||||
|
sslSocketFactory(sslSocketFactory, trustManager)
|
||||||
|
}
|
||||||
@@ -53,12 +53,7 @@ import splitties.init.appCtx
|
|||||||
object EhTagDatabase : CoroutineScope {
|
object EhTagDatabase : CoroutineScope {
|
||||||
override val coroutineContext = Dispatchers.IO + Job()
|
override val coroutineContext = Dispatchers.IO + Job()
|
||||||
private const val NAMESPACE_PREFIX = "n"
|
private const val NAMESPACE_PREFIX = "n"
|
||||||
private val PREFIXES = arrayOf(
|
private val PREFIXES = ["-", "~", "tag:", "weak:"]
|
||||||
"-",
|
|
||||||
"~",
|
|
||||||
"tag:",
|
|
||||||
"weak:",
|
|
||||||
)
|
|
||||||
private lateinit var tagGroups: Map<String, Map<String, String>>
|
private lateinit var tagGroups: Map<String, Map<String, String>>
|
||||||
private val updateLock = Mutex()
|
private val updateLock = Mutex()
|
||||||
var initialized by mutableStateOf(false)
|
var initialized by mutableStateOf(false)
|
||||||
|
|||||||
@@ -107,14 +107,14 @@ object EhUrl {
|
|||||||
else -> URL_IMAGE_SEARCH_E
|
else -> URL_IMAGE_SEARCH_E
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) = ehUrl(listOf("g", "$gid", token)) {
|
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) = ehUrl(["g", "$gid", token]) {
|
||||||
if (index != 0) addQueryParameter("p", "$index")
|
if (index != 0) addQueryParameter("p", "$index")
|
||||||
if (allComment) addQueryParameter("hc", "1")
|
if (allComment) addQueryParameter("hc", "1")
|
||||||
}.buildString()
|
}.buildString()
|
||||||
|
|
||||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", "$gid", token)).buildString()
|
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(["mpv", "$gid", token]).buildString()
|
||||||
|
|
||||||
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(listOf("s", pToken, "$gid-${index + 1}")) {
|
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(["s", pToken, "$gid-${index + 1}"]) {
|
||||||
addQueryParameterIfNotBlank("nl", nl)
|
addQueryParameterIfNotBlank("nl", nl)
|
||||||
}.buildString()
|
}.buildString()
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ object EhUrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||||
path = if (path.isNullOrBlank()) emptyList() else listOf(path),
|
path = if (path.isNullOrBlank()) [] else [path],
|
||||||
host = host,
|
host = host,
|
||||||
parameters = Parameters.build(builder),
|
parameters = Parameters.build(builder),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ data class ListUrlBuilder(
|
|||||||
private var jumpTo: String? = null,
|
private var jumpTo: String? = null,
|
||||||
private var range: Int = 0,
|
private var range: Int = 0,
|
||||||
var category: Int = EhUtils.NONE,
|
var category: Int = EhUtils.NONE,
|
||||||
private var mKeyword: String? = null,
|
var keyword: String? = null,
|
||||||
var hash: String? = null,
|
var hash: String? = null,
|
||||||
var language: Int = -1,
|
var language: Int = -1,
|
||||||
var advanceSearch: Int = -1,
|
var advanceSearch: Int = -1,
|
||||||
@@ -69,16 +69,10 @@ data class ListUrlBuilder(
|
|||||||
jumpTo = date
|
jumpTo = date
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyword: String?
|
|
||||||
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
|
|
||||||
set(keyword) {
|
|
||||||
mKeyword = keyword
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(q: QuickSearch) : this(
|
constructor(q: QuickSearch) : this(
|
||||||
mode = q.mode,
|
mode = q.mode,
|
||||||
category = q.category,
|
category = q.category,
|
||||||
mKeyword = q.keyword,
|
keyword = q.keyword,
|
||||||
advanceSearch = q.advanceSearch,
|
advanceSearch = q.advanceSearch,
|
||||||
minRating = q.minRating,
|
minRating = q.minRating,
|
||||||
pageFrom = q.pageFrom,
|
pageFrom = q.pageFrom,
|
||||||
@@ -90,7 +84,7 @@ data class ListUrlBuilder(
|
|||||||
name = name,
|
name = name,
|
||||||
mode = mode,
|
mode = mode,
|
||||||
category = category,
|
category = category,
|
||||||
keyword = mKeyword,
|
keyword = keyword,
|
||||||
advanceSearch = advanceSearch,
|
advanceSearch = advanceSearch,
|
||||||
minRating = minRating,
|
minRating = minRating,
|
||||||
pageFrom = pageFrom,
|
pageFrom = pageFrom,
|
||||||
@@ -107,7 +101,7 @@ data class ListUrlBuilder(
|
|||||||
if (q.category != this.category) {
|
if (q.category != this.category) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (q.keyword != mKeyword) {
|
if (q.keyword != keyword) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (q.advanceSearch != advanceSearch) {
|
if (q.advanceSearch != advanceSearch) {
|
||||||
@@ -129,7 +123,7 @@ data class ListUrlBuilder(
|
|||||||
jumpTo = params["seek"],
|
jumpTo = params["seek"],
|
||||||
range = params["range"]?.toIntOrNull() ?: 0,
|
range = params["range"]?.toIntOrNull() ?: 0,
|
||||||
category = params["f_cats"]?.toIntOrNull()?.let(EhUtils::invCategory) ?: EhUtils.NONE,
|
category = params["f_cats"]?.toIntOrNull()?.let(EhUtils::invCategory) ?: EhUtils.NONE,
|
||||||
mKeyword = params["f_search"],
|
keyword = params["f_search"],
|
||||||
) {
|
) {
|
||||||
if (params["advsearch"] == "1") {
|
if (params["advsearch"] == "1") {
|
||||||
advanceSearch = 0
|
advanceSearch = 0
|
||||||
@@ -160,7 +154,7 @@ data class ListUrlBuilder(
|
|||||||
if (category > 0) {
|
if (category > 0) {
|
||||||
addQueryParameter("f_cats", "${EhUtils.invCategory(category)}")
|
addQueryParameter("f_cats", "${EhUtils.invCategory(category)}")
|
||||||
}
|
}
|
||||||
val query = mKeyword?.let { keyword ->
|
val query = keyword?.let { keyword ->
|
||||||
if (language == -1 || "gid:" in keyword || "l:" in keyword || "language:" in keyword) {
|
if (language == -1 || "gid:" in keyword || "l:" in keyword || "language:" in keyword) {
|
||||||
keyword
|
keyword
|
||||||
} else {
|
} else {
|
||||||
@@ -214,7 +208,7 @@ data class ListUrlBuilder(
|
|||||||
}.buildString()
|
}.buildString()
|
||||||
MODE_UPLOADER, MODE_TAG -> {
|
MODE_UPLOADER, MODE_TAG -> {
|
||||||
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
||||||
ehUrl(listOf(path, requireNotNull(mKeyword))) {
|
ehUrl([path, requireNotNull(keyword)]) {
|
||||||
addQueryParameterIfNotBlank("prev", prev)
|
addQueryParameterIfNotBlank("prev", prev)
|
||||||
addQueryParameterIfNotBlank("next", next)
|
addQueryParameterIfNotBlank("next", next)
|
||||||
addQueryParameterIfNotBlank("seek", jumpTo)
|
addQueryParameterIfNotBlank("seek", jumpTo)
|
||||||
@@ -226,7 +220,7 @@ data class ListUrlBuilder(
|
|||||||
addQueryParameter("f_shash", requireNotNull(hash))
|
addQueryParameter("f_shash", requireNotNull(hash))
|
||||||
}.buildString()
|
}.buildString()
|
||||||
MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
||||||
addQueryParameter("tl", requireNotNull(mKeyword))
|
addQueryParameter("tl", requireNotNull(keyword))
|
||||||
addQueryParameterIfNotBlank("p", jumpTo)
|
addQueryParameterIfNotBlank("p", jumpTo)
|
||||||
}.buildString()
|
}.buildString()
|
||||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ object GalleryListParser {
|
|||||||
throw ParseException("Can't parse gallery list", it)
|
throw ParseException("Can't parse gallery list", it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val emptyResult = GalleryListResult(null, null, arrayListOf())
|
val emptyResult = GalleryListResult(null, null, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GalleryListResult(
|
data class GalleryListResult(
|
||||||
val prev: String?,
|
val prev: String?,
|
||||||
val next: String?,
|
val next: String?,
|
||||||
val galleryInfoList: ArrayList<BaseGalleryInfo>,
|
val galleryInfoList: MutableList<BaseGalleryInfo>,
|
||||||
)
|
)
|
||||||
|
|
||||||
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
|
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
|
||||||
@@ -48,7 +48,7 @@ object GalleryListUrlParser {
|
|||||||
ListUrlBuilder(
|
ListUrlBuilder(
|
||||||
mode = ListUrlBuilder.MODE_TOPLIST,
|
mode = ListUrlBuilder.MODE_TOPLIST,
|
||||||
jumpTo = url.parameters["p"],
|
jumpTo = url.parameters["p"],
|
||||||
mKeyword = tl,
|
keyword = tl,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import com.ehviewer.core.files.isDirectory
|
|||||||
import com.ehviewer.core.files.isFile
|
import com.ehviewer.core.files.isFile
|
||||||
import com.ehviewer.core.files.sendTo
|
import com.ehviewer.core.files.sendTo
|
||||||
import com.ehviewer.core.files.toUri
|
import com.ehviewer.core.files.toUri
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
import com.hippo.ehviewer.EhApplication.Companion.thumbCache
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.client.getThumbKey
|
import com.hippo.ehviewer.client.getThumbKey
|
||||||
import com.hippo.ehviewer.download.downloadLocation
|
import com.hippo.ehviewer.download.downloadLocation
|
||||||
@@ -53,7 +53,7 @@ object DownloadThumbInterceptor : Interceptor {
|
|||||||
// Accessing the recreated file immediately after deleting it throws
|
// Accessing the recreated file immediately after deleting it throws
|
||||||
// FileNotFoundException, so we just overwrite the existing file.
|
// FileNotFoundException, so we just overwrite the existing file.
|
||||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||||
imageCache.read(key) {
|
thumbCache.read(key) {
|
||||||
data sendTo thumb
|
data sendTo thumb
|
||||||
}
|
}
|
||||||
if (thumb != v1Thumb) v1Thumb.delete()
|
if (thumb != v1Thumb) v1Thumb.delete()
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ import kotlinx.coroutines.async
|
|||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
|
||||||
import kotlinx.coroutines.flow.sample
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class DownloadService :
|
class DownloadService :
|
||||||
@@ -375,8 +373,8 @@ class DownloadService :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun run() {
|
suspend fun run() {
|
||||||
channel.receiveAsFlow().sample(DELAY).collect {
|
while (true) {
|
||||||
when (it) {
|
when (channel.receive()) {
|
||||||
Ops.Notify -> {
|
Ops.Notify -> {
|
||||||
if (ActivityCompat.checkSelfPermission(
|
if (ActivityCompat.checkSelfPermission(
|
||||||
service,
|
service,
|
||||||
@@ -389,6 +387,7 @@ class DownloadService :
|
|||||||
Ops.Cancel -> notifyManager.cancel(id)
|
Ops.Cancel -> notifyManager.cancel(id)
|
||||||
Ops.StartForeground -> service.startForeground(id, builder.build())
|
Ops.StartForeground -> service.startForeground(id, builder.build())
|
||||||
}
|
}
|
||||||
|
delay(DELAY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
|
|||||||
return SortMode(field, order, groupByDownloadLabel)
|
return SortMode(field, order, groupByDownloadLabel)
|
||||||
}
|
}
|
||||||
val Default = SortMode(Field.TIME, Order.DESC)
|
val Default = SortMode(Field.TIME, Order.DESC)
|
||||||
val All = listOf(
|
val All = [
|
||||||
Default,
|
Default,
|
||||||
SortMode(Field.TIME, Order.ASC),
|
SortMode(Field.TIME, Order.ASC),
|
||||||
SortMode(Field.GID, Order.DESC),
|
SortMode(Field.GID, Order.DESC),
|
||||||
@@ -57,6 +57,6 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
|
|||||||
SortMode(Field.TITLE, Order.DESC),
|
SortMode(Field.TITLE, Order.DESC),
|
||||||
SortMode(Field.PAGES, Order.ASC),
|
SortMode(Field.PAGES, Order.ASC),
|
||||||
SortMode(Field.PAGES, Order.DESC),
|
SortMode(Field.PAGES, Order.DESC),
|
||||||
)
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,9 @@ import coil3.request.CachePolicy
|
|||||||
import coil3.request.ErrorResult
|
import coil3.request.ErrorResult
|
||||||
import coil3.request.SuccessResult
|
import coil3.request.SuccessResult
|
||||||
import coil3.request.allowHardware
|
import coil3.request.allowHardware
|
||||||
import coil3.size.Dimension
|
import coil3.request.maxBitmapSize
|
||||||
import coil3.size.Precision
|
import coil3.size.Precision
|
||||||
|
import coil3.size.Scale
|
||||||
import coil3.size.Size
|
import coil3.size.Size
|
||||||
import coil3.size.SizeResolver
|
import coil3.size.SizeResolver
|
||||||
import com.ehviewer.core.files.openFileDescriptor
|
import com.ehviewer.core.files.openFileDescriptor
|
||||||
@@ -90,8 +91,10 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
|
private val sizeResolver = with(appCtx.resources.displayMetrics) {
|
||||||
private val sizeResolver = SizeResolver(Size(targetWidth, Dimension.Undefined))
|
val targetSize = minOf(widthPixels, heightPixels) * 4 / 3
|
||||||
|
SizeResolver(Size(targetSize, targetSize))
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
|
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
|
||||||
val request = with(appCtx) {
|
val request = with(appCtx) {
|
||||||
@@ -99,7 +102,9 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
|||||||
onLeft { data(it.source) }
|
onLeft { data(it.source) }
|
||||||
onRight { data(it.source.toUri()) }
|
onRight { data(it.source.toUri()) }
|
||||||
size(sizeResolver)
|
size(sizeResolver)
|
||||||
|
scale(Scale.FILL)
|
||||||
precision(Precision.INEXACT)
|
precision(Precision.INEXACT)
|
||||||
|
maxBitmapSize(Size.ORIGINAL)
|
||||||
allowHardware(false)
|
allowHardware(false)
|
||||||
hardwareThreshold(Settings.hardwareBitmapThreshold.value)
|
hardwareThreshold(Settings.hardwareBitmapThreshold.value)
|
||||||
maybeCropBorder(Settings.cropBorder.value)
|
maybeCropBorder(Settings.cropBorder.value)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ fun <T : HttpClientEngineConfig> HttpClientConfig<T>.configureCommon(redirect: B
|
|||||||
}
|
}
|
||||||
install(HttpTimeout) {
|
install(HttpTimeout) {
|
||||||
reset()
|
reset()
|
||||||
requestTimeoutMillis = 10_000
|
requestTimeoutMillis = 30_000
|
||||||
}
|
}
|
||||||
install(UserAgent)
|
install(UserAgent)
|
||||||
defaultRequest {
|
defaultRequest {
|
||||||
|
|||||||
@@ -32,20 +32,21 @@ import java.nio.ByteBuffer
|
|||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.consumeEach
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.job
|
import kotlinx.coroutines.job
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
|
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
|
||||||
// Limit thread to 1 since we are async & non-blocking
|
// Limit thread to 1 since we are async & non-blocking
|
||||||
override val dispatcher = Dispatchers.Default.limitedParallelism(1)
|
// TODO: Investigate https://github.com/FooIbar/EhViewer/issues/2854
|
||||||
|
// override val dispatcher = Dispatchers.Default.limitedParallelism(1)
|
||||||
|
|
||||||
override val supportedCapabilities = setOf(HttpTimeoutCapability)
|
override val supportedCapabilities = setOf(HttpTimeoutCapability)
|
||||||
|
|
||||||
private val executor = dispatcher.asExecutor()
|
private val executor = dispatcher.limitedParallelism(1).asExecutor()
|
||||||
private val pool = DirectByteBufferPool(32)
|
private val pool = DirectByteBufferPool(32)
|
||||||
private val client by lazy {
|
private val client by lazy {
|
||||||
with(config) { HttpEngine.Builder(context).apply(config).build() }
|
with(config) { HttpEngine.Builder(context).apply(config).build() }
|
||||||
@@ -92,7 +93,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
|
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
|
||||||
chunkChan.trySend(byteBuffer).getOrThrow()
|
chunkChan.trySendBlocking(byteBuffer).getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package com.hippo.ehviewer.ktor
|
package com.hippo.ehviewer.ktor
|
||||||
|
|
||||||
import com.ehviewer.core.util.isAtLeastQ
|
import com.hippo.ehviewer.Settings
|
||||||
|
import com.hippo.ehviewer.client.EhDns
|
||||||
|
import com.hippo.ehviewer.client.EhSSLSocketFactory
|
||||||
|
import com.hippo.ehviewer.client.install
|
||||||
import io.ktor.client.engine.okhttp.OkHttpConfig
|
import io.ktor.client.engine.okhttp.OkHttpConfig
|
||||||
import okhttp3.AsyncDns
|
import java.net.Proxy
|
||||||
import okhttp3.android.AndroidAsyncDns
|
|
||||||
|
|
||||||
fun OkHttpConfig.configureClient() {
|
fun OkHttpConfig.configureClient() {
|
||||||
config {
|
config {
|
||||||
if (isAtLeastQ) {
|
dns(EhDns)
|
||||||
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
|
proxy(Proxy.NO_PROXY)
|
||||||
|
if (!Settings.enableQuic.value) {
|
||||||
|
install(EhSSLSocketFactory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ private val xml = XML {
|
|||||||
}
|
}
|
||||||
xmlDeclMode = XmlDeclMode.Charset
|
xmlDeclMode = XmlDeclMode.Charset
|
||||||
xmlVersion = XmlVersion.XML10
|
xmlVersion = XmlVersion.XML10
|
||||||
indent = 2
|
setIndent(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun GalleryInfo.getComicInfo(): ComicInfo {
|
fun GalleryInfo.getComicInfo(): ComicInfo {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import com.ehviewer.core.files.sendTo
|
|||||||
import com.ehviewer.core.model.GalleryDetail
|
import com.ehviewer.core.model.GalleryDetail
|
||||||
import com.ehviewer.core.model.GalleryInfo
|
import com.ehviewer.core.model.GalleryInfo
|
||||||
import com.ehviewer.core.util.logcat
|
import com.ehviewer.core.util.logcat
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
|
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
@@ -47,6 +46,7 @@ import com.hippo.ehviewer.download.downloadLocation
|
|||||||
import com.hippo.ehviewer.download.tempDownloadDir
|
import com.hippo.ehviewer.download.tempDownloadDir
|
||||||
import com.hippo.ehviewer.image.PathSource
|
import com.hippo.ehviewer.image.PathSource
|
||||||
import com.hippo.ehviewer.jni.archiveFdBatch
|
import com.hippo.ehviewer.jni.archiveFdBatch
|
||||||
|
import com.hippo.ehviewer.ktbuilder.diskCache
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.copyTo
|
import com.hippo.ehviewer.util.copyTo
|
||||||
import com.hippo.ehviewer.util.sha1
|
import com.hippo.ehviewer.util.sha1
|
||||||
@@ -57,6 +57,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock
|
|||||||
import kotlin.concurrent.read
|
import kotlin.concurrent.read
|
||||||
import kotlin.concurrent.write
|
import kotlin.concurrent.write
|
||||||
import okio.Path
|
import okio.Path
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
class SpiderDen(val info: GalleryInfo) {
|
class SpiderDen(val info: GalleryInfo) {
|
||||||
private val gid = info.gid
|
private val gid = info.gid
|
||||||
@@ -151,6 +153,10 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
removeTempFile(index)
|
removeTempFile(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearCache() {
|
||||||
|
repeat(info.pages, ::removeFromCache)
|
||||||
|
}
|
||||||
|
|
||||||
private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) {
|
private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) {
|
||||||
val name = perFilename(index, extension)
|
val name = perFilename(index, extension)
|
||||||
read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } }
|
read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } }
|
||||||
@@ -328,7 +334,7 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
downloadDir?.run {
|
downloadDir?.run {
|
||||||
resolve(COMIC_INFO_FILE).also {
|
resolve(COMIC_INFO_FILE).also {
|
||||||
if (info !is GalleryDetail && fetchMetadata) {
|
if (info !is GalleryDetail && fetchMetadata) {
|
||||||
EhEngine.fillGalleryListByApi(listOf(info))
|
EhEngine.fillGalleryListByApi([info])
|
||||||
}
|
}
|
||||||
info.getComicInfo().apply {
|
info.getComicInfo().apply {
|
||||||
writeComicInfo(this, it)
|
writeComicInfo(this, it)
|
||||||
@@ -342,6 +348,12 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val sCache by lazy {
|
||||||
|
diskCache {
|
||||||
|
directory(appCtx.cacheDir.toOkioPath() / "image_cache")
|
||||||
|
maxSizeBytes(Settings.readCacheSize.value.coerceIn(320, 5120).toLong() * 1024 * 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
private const val TEMP_SUFFIX = ".tmp"
|
private const val TEMP_SUFFIX = ".tmp"
|
||||||
private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
|
private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
|
||||||
private val FileHashRegex = Regex("/h/([0-9a-f]{40})")
|
private val FileHashRegex = Regex("/h/([0-9a-f]{40})")
|
||||||
|
|||||||
@@ -594,6 +594,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
previousPToken,
|
previousPToken,
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
|
if (it is FatalException) throw it
|
||||||
forceHtml = true
|
forceHtml = true
|
||||||
return@repeat
|
return@repeat
|
||||||
}.let {
|
}.let {
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ suspend fun showMoveDownloadLabel(info: GalleryInfo) {
|
|||||||
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
|
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
|
||||||
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
|
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
|
||||||
val label = if (selected == 0) null else labels[selected]
|
val label = if (selected == 0) null else labels[selected]
|
||||||
DownloadManager.changeLabel(listOf(downloadInfo), label)
|
DownloadManager.changeLabel([downloadInfo], label)
|
||||||
}
|
}
|
||||||
|
|
||||||
context(_: DialogState)
|
context(_: DialogState)
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.navigation.ActivityNavigator
|
|
||||||
import com.ehviewer.core.util.isAtLeastR
|
|
||||||
import com.hippo.ehviewer.Settings
|
|
||||||
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
|
||||||
|
|
||||||
abstract class EhActivity : AppCompatActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
// https://issuetracker.google.com/204791558
|
|
||||||
// Fix system bars insets still exist in fullscreen mode on API < 30
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
if (!isAtLeastR) {
|
|
||||||
with(window.decorView) {
|
|
||||||
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
interceptSecurityOrReturn()
|
|
||||||
super.onResume()
|
|
||||||
window.setSecureScreen(Settings.enabledSecurity.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
|
||||||
super.finish()
|
|
||||||
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,10 @@ import android.content.pm.verify.domain.DomainVerificationManager
|
|||||||
import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE
|
import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.SharedTransitionLayout
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
@@ -63,7 +66,7 @@ import androidx.compose.material3.SnackbarHost
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.material3.rememberDrawerState2
|
import androidx.compose.material3.rememberDrawerState2
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -98,6 +101,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
@@ -113,6 +117,7 @@ import com.ehviewer.core.ui.icons.filled.Subscriptions
|
|||||||
import com.ehviewer.core.ui.util.LocalSnackBarFabPadding
|
import com.ehviewer.core.ui.util.LocalSnackBarFabPadding
|
||||||
import com.ehviewer.core.ui.util.LocalWindowSizeClass
|
import com.ehviewer.core.ui.util.LocalWindowSizeClass
|
||||||
import com.ehviewer.core.util.isAtLeastQ
|
import com.ehviewer.core.util.isAtLeastQ
|
||||||
|
import com.ehviewer.core.util.isAtLeastR
|
||||||
import com.ehviewer.core.util.isAtLeastS
|
import com.ehviewer.core.util.isAtLeastS
|
||||||
import com.ehviewer.core.util.withIOContext
|
import com.ehviewer.core.util.withIOContext
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.initialized
|
import com.hippo.ehviewer.EhApplication.Companion.initialized
|
||||||
@@ -153,6 +158,7 @@ import com.ramcosta.composedestinations.DestinationsNavHost
|
|||||||
import com.ramcosta.composedestinations.spec.Direction
|
import com.ramcosta.composedestinations.spec.Direction
|
||||||
import com.ramcosta.composedestinations.utils.currentDestinationAsState
|
import com.ramcosta.composedestinations.utils.currentDestinationAsState
|
||||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||||
|
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -175,7 +181,7 @@ private val navItems = arrayOf<Triple<Direction, Int, ImageVector>>(
|
|||||||
Triple(SettingsScreenDestination, R.string.settings, Icons.Default.Settings),
|
Triple(SettingsScreenDestination, R.string.settings, Icons.Default.Settings),
|
||||||
)
|
)
|
||||||
|
|
||||||
class MainActivity : EhActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private var shareUrl: String? = null
|
private var shareUrl: String? = null
|
||||||
|
|
||||||
@@ -203,6 +209,15 @@ class MainActivity : EhActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen().setKeepOnScreenCondition { !initialized }
|
installSplashScreen().setKeepOnScreenCondition { !initialized }
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
// https://issuetracker.google.com/204791558
|
||||||
|
// Fix system bars insets still exist in fullscreen mode on API < 30
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (!isAtLeastR) {
|
||||||
|
with(window.decorView) {
|
||||||
|
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
setMD3Content {
|
setMD3Content {
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val navDrawerState = rememberDrawerState(DrawerValue.Closed)
|
val navDrawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
@@ -253,6 +268,15 @@ class MainActivity : EhActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
Settings.enabledSecurity.valueFlow().collect {
|
||||||
|
window.setSecureScreen(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isAuthenticationSupported()) {
|
||||||
|
SecurityScreen(onError = { moveTaskToBack(true) }, modifier = Modifier.zIndex(1f))
|
||||||
|
}
|
||||||
|
|
||||||
val cannotParse = stringResource(R.string.error_cannot_parse_the_url)
|
val cannotParse = stringResource(R.string.error_cannot_parse_the_url)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
intentFlow.collect { intent ->
|
intentFlow.collect { intent ->
|
||||||
@@ -276,7 +300,7 @@ class MainActivity : EhActivity() {
|
|||||||
if ("text/plain" == type) {
|
if ("text/plain" == type) {
|
||||||
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
|
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
if (keyword != null && !navWithUrl(keyword)) {
|
if (keyword != null && !navWithUrl(keyword)) {
|
||||||
navigate(ListUrlBuilder(mKeyword = keyword).asDst())
|
navigate(ListUrlBuilder(keyword = keyword).asDst())
|
||||||
}
|
}
|
||||||
} else if (type != null && type.startsWith("image/")) {
|
} else if (type != null && type.startsWith("image/")) {
|
||||||
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
||||||
@@ -356,7 +380,7 @@ class MainActivity : EhActivity() {
|
|||||||
var snackbarFabPadding by remember { mutableStateOf(0.dp) }
|
var snackbarFabPadding by remember { mutableStateOf(0.dp) }
|
||||||
val drawerEnabled = drawerHandle.isNotEmpty()
|
val drawerEnabled = drawerHandle.isNotEmpty()
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val adaptiveInfo = currentWindowAdaptiveInfo()
|
val adaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||||
val needSignIn by Settings.needSignIn.collectAsState()
|
val needSignIn by Settings.needSignIn.collectAsState()
|
||||||
val launchPage by Settings.launchPage.collectAsState()
|
val launchPage by Settings.launchPage.collectAsState()
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.biometric.AuthenticationRequest.Biometric
|
|
||||||
import androidx.biometric.AuthenticationRequest.Companion.biometricRequest
|
|
||||||
import androidx.biometric.AuthenticationResult
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
|
||||||
import androidx.biometric.registerForAuthenticationResult
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import com.ehviewer.core.i18n.R
|
|
||||||
import com.hippo.ehviewer.Settings
|
|
||||||
|
|
||||||
context(ctx: Context)
|
|
||||||
fun isAuthenticationSupported(): Boolean {
|
|
||||||
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
|
||||||
return BiometricManager.from(ctx).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
|
|
||||||
}
|
|
||||||
|
|
||||||
class SecurityActivity : AppCompatActivity() {
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
if (isAuthenticationSupported()) {
|
|
||||||
startAuthentication()
|
|
||||||
} else {
|
|
||||||
onSuccess()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val authLauncher = registerForAuthenticationResult { result ->
|
|
||||||
when (result) {
|
|
||||||
is AuthenticationResult.Success -> onSuccess()
|
|
||||||
is AuthenticationResult.Error -> onError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun startAuthentication() {
|
|
||||||
if (isAuthenticating) return
|
|
||||||
isAuthenticating = true
|
|
||||||
val request = biometricRequest(
|
|
||||||
title = getString(R.string.settings_privacy_require_unlock),
|
|
||||||
authFallback = Biometric.Fallback.DeviceCredential,
|
|
||||||
) {}
|
|
||||||
authLauncher.launch(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSuccess() {
|
|
||||||
isAuthenticating = false
|
|
||||||
locked = false
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onError() {
|
|
||||||
moveTaskToBack(true)
|
|
||||||
isAuthenticating = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isAuthenticating = false
|
|
||||||
var locked = true
|
|
||||||
var lockedLastLeaveTime: Long = 0
|
|
||||||
|
|
||||||
val lockObserver = object : DefaultLifecycleObserver {
|
|
||||||
override fun onPause(owner: LifecycleOwner) {
|
|
||||||
if (!locked) {
|
|
||||||
lockedLastLeaveTime = System.currentTimeMillis() / 1000
|
|
||||||
}
|
|
||||||
locked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.interceptSecurityOrReturn() {
|
|
||||||
val lockedResumeTime = System.currentTimeMillis() / 1000
|
|
||||||
val lockedDelayTime = lockedResumeTime - lockedLastLeaveTime
|
|
||||||
if (lockedDelayTime < Settings.securityDelay.value * 60) {
|
|
||||||
locked = false
|
|
||||||
} else if (Settings.security.value && isAuthenticationSupported() && locked) {
|
|
||||||
startActivity(Intent(this, SecurityActivity::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* 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.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.biometric.AuthenticationRequest.Biometric
|
||||||
|
import androidx.biometric.AuthenticationRequest.Companion.biometricRequest
|
||||||
|
import androidx.biometric.AuthenticationResult
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
import androidx.biometric.compose.rememberAuthenticationLauncher
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
|
import com.ehviewer.core.i18n.R
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
|
import com.hippo.ehviewer.collectAsState
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.TimeSource
|
||||||
|
|
||||||
|
context(ctx: Context)
|
||||||
|
fun isAuthenticationSupported(): Boolean {
|
||||||
|
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||||
|
return BiometricManager.from(ctx).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SecurityScreen(onError: () -> Unit, modifier: Modifier) {
|
||||||
|
val security by Settings.security.collectAsState()
|
||||||
|
var locked by remember { mutableStateOf(security) }
|
||||||
|
var isAuthenticating by remember { mutableStateOf(false) }
|
||||||
|
val title = stringResource(R.string.settings_privacy_require_unlock)
|
||||||
|
val launcher = rememberAuthenticationLauncher { result ->
|
||||||
|
isAuthenticating = false
|
||||||
|
when (result) {
|
||||||
|
is AuthenticationResult.Success -> locked = false
|
||||||
|
else -> onError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun startAuthentication() {
|
||||||
|
if (isAuthenticating) return
|
||||||
|
isAuthenticating = true
|
||||||
|
val request = biometricRequest(title, Biometric.Fallback.DeviceCredential) {}
|
||||||
|
launcher.launch(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
val securityDelay by Settings.securityDelay.collectAsState { it.minutes }
|
||||||
|
var leaveTime by remember { mutableStateOf(TimeSource.Monotonic.markNow()) }
|
||||||
|
LifecycleResumeEffect(Unit) {
|
||||||
|
if (security && leaveTime.elapsedNow() > securityDelay) {
|
||||||
|
locked = true
|
||||||
|
}
|
||||||
|
onPauseOrDispose {
|
||||||
|
if (!locked) {
|
||||||
|
leaveTime = TimeSource.Monotonic.markNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locked) {
|
||||||
|
val windowInfo = LocalWindowInfo.current
|
||||||
|
LaunchedEffect(windowInfo) {
|
||||||
|
snapshotFlow { windowInfo.isWindowFocused }.collect {
|
||||||
|
if (it) startAuthentication()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Surface(modifier = modifier.fillMaxSize()) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import coil3.decode.DecodeUtils
|
|||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
import coil3.size.Scale
|
import coil3.size.Scale
|
||||||
|
import coil3.size.Size
|
||||||
import com.ehviewer.core.util.toIntOrDefault
|
import com.ehviewer.core.util.toIntOrDefault
|
||||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -21,7 +22,7 @@ class CoilImageGetter(private val onSuccess: () -> Unit) : Html.ImageGetter {
|
|||||||
val srcHeight = groupValues[2].toInt()
|
val srcHeight = groupValues[2].toInt()
|
||||||
val dstWidth = groupValues[3].toIntOrDefault(200)
|
val dstWidth = groupValues[3].toIntOrDefault(200)
|
||||||
val dstHeight = dstWidth / 2 * 3
|
val dstHeight = dstWidth / 2 * 3
|
||||||
val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT)
|
val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT, Size.ORIGINAL)
|
||||||
setBounds(0, 0, (srcWidth * multiplier).roundToInt(), (srcHeight * multiplier).roundToInt())
|
setBounds(0, 0, (srcWidth * multiplier).roundToInt(), (srcHeight * multiplier).roundToInt())
|
||||||
} ?: setBounds(0, 0, 200, 300)
|
} ?: setBounds(0, 0, 200, 300)
|
||||||
with(appCtx) {
|
with(appCtx) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ fun AnimatedVisibilityScope.WebViewSignInScreen(navigator: DestinationsNavigator
|
|||||||
Settings.desktopSite.value = false
|
Settings.desktopSite.value = false
|
||||||
}
|
}
|
||||||
if (EhCookieStore.hasSignedIn()) {
|
if (EhCookieStore.hasSignedIn()) {
|
||||||
|
EhCookieStore.flush()
|
||||||
postLogin()
|
postLogin()
|
||||||
state.webView?.destroy()
|
state.webView?.destroy()
|
||||||
bgWork { awaitCancellation() }
|
bgWork { awaitCancellation() }
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import com.hippo.ehviewer.ui.tools.DialogState
|
|||||||
import com.hippo.ehviewer.ui.tools.awaitResult
|
import com.hippo.ehviewer.ui.tools.awaitResult
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
private val categoryTable = arrayOf(
|
private val categoryTable = [
|
||||||
EhUtils.DOUJINSHI to R.string.doujinshi,
|
EhUtils.DOUJINSHI to R.string.doujinshi,
|
||||||
EhUtils.MANGA to R.string.manga,
|
EhUtils.MANGA to R.string.manga,
|
||||||
EhUtils.ARTIST_CG to R.string.artist_cg,
|
EhUtils.ARTIST_CG to R.string.artist_cg,
|
||||||
@@ -58,7 +58,7 @@ private val categoryTable = arrayOf(
|
|||||||
EhUtils.COSPLAY to R.string.cosplay,
|
EhUtils.COSPLAY to R.string.cosplay,
|
||||||
EhUtils.ASIAN_PORN to R.string.asian_porn,
|
EhUtils.ASIAN_PORN to R.string.asian_porn,
|
||||||
EhUtils.MISC to R.string.misc,
|
EhUtils.MISC to R.string.misc,
|
||||||
)
|
]
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
context(_: DialogState, _: CoroutineScope, _: Context)
|
context(_: DialogState, _: CoroutineScope, _: Context)
|
||||||
@@ -193,9 +193,6 @@ fun SearchFilter(
|
|||||||
}
|
}
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
|
||||||
Text(text = error, style = MaterialTheme.typography.bodySmall)
|
|
||||||
},
|
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(imageVector = Icons.Default.Info, contentDescription = null)
|
Icon(imageVector = Icons.Default.Info, contentDescription = null)
|
||||||
},
|
},
|
||||||
@@ -204,6 +201,9 @@ fun SearchFilter(
|
|||||||
headlineColor = MaterialTheme.colorScheme.error,
|
headlineColor = MaterialTheme.colorScheme.error,
|
||||||
leadingIconColor = MaterialTheme.colorScheme.error,
|
leadingIconColor = MaterialTheme.colorScheme.error,
|
||||||
),
|
),
|
||||||
|
content = {
|
||||||
|
Text(text = error, style = MaterialTheme.typography.bodySmall)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import androidx.compose.material.icons.filled.Brightness5
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -23,6 +19,8 @@ import androidx.core.graphics.green
|
|||||||
import androidx.core.graphics.red
|
import androidx.core.graphics.red
|
||||||
import com.ehviewer.core.i18n.R
|
import com.ehviewer.core.i18n.R
|
||||||
import com.ehviewer.core.ui.component.RollingNumber
|
import com.ehviewer.core.ui.component.RollingNumber
|
||||||
|
import com.ehviewer.core.ui.component.rememberIntPrefSliderState
|
||||||
|
import com.ehviewer.core.ui.component.rememberIntSliderState
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.asMutableState
|
import com.hippo.ehviewer.asMutableState
|
||||||
|
|
||||||
@@ -34,12 +32,11 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
|
|||||||
field = customBrightness,
|
field = customBrightness,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = customBrightness.value) {
|
AnimatedVisibility(visible = customBrightness.value) {
|
||||||
val brightness = Settings.customBrightnessValue.asMutableState()
|
val brightness = rememberIntPrefSliderState(Settings.customBrightnessValue, -75..100)
|
||||||
SliderChoice(
|
SliderChoice(
|
||||||
startSlot = { Icon(imageVector = Icons.Default.Brightness5, contentDescription = null) },
|
startSlot = { Icon(imageVector = Icons.Default.Brightness5, contentDescription = null) },
|
||||||
endSlot = { RollingNumber(number = brightness.value, length = 3) },
|
endSlot = { RollingNumber(number = brightness.value, length = 3) },
|
||||||
range = -75..100,
|
state = brightness,
|
||||||
field = brightness,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val colorFilter = Settings.colorFilter.asMutableState()
|
val colorFilter = Settings.colorFilter.asMutableState()
|
||||||
@@ -48,47 +45,39 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
|
|||||||
field = colorFilter,
|
field = colorFilter,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = colorFilter.value) {
|
AnimatedVisibility(visible = colorFilter.value) {
|
||||||
var color by Settings.colorFilterValue.asMutableState()
|
var color by Settings.colorFilterValue
|
||||||
val rf = remember { mutableIntStateOf(color.red) }
|
val r = rememberIntSliderState(color.red, 0..255)
|
||||||
val r by rf
|
val g = rememberIntSliderState(color.green, 0..255)
|
||||||
val gf = remember { mutableIntStateOf(color.green) }
|
val b = rememberIntSliderState(color.blue, 0..255)
|
||||||
val g by gf
|
val a = rememberIntSliderState(color.alpha, 0..255)
|
||||||
val bf = remember { mutableIntStateOf(color.blue) }
|
color = (a.value shl 24) or (r.value shl 16) or (g.value shl 8) or (b.value)
|
||||||
val b by bf
|
|
||||||
val af = remember { mutableIntStateOf(color.alpha) }
|
|
||||||
val a by af
|
|
||||||
color = (a shl 24) or (r shl 16) or (g shl 8) or (b)
|
|
||||||
Column {
|
Column {
|
||||||
SliderChoice(
|
SliderChoice(
|
||||||
startSlot = { Text(text = "R") },
|
startSlot = { Text(text = "R") },
|
||||||
endSlot = { RollingNumber(number = r, length = 3) },
|
endSlot = { RollingNumber(number = r.value, length = 3) },
|
||||||
range = 0..255,
|
state = r,
|
||||||
field = rf,
|
|
||||||
)
|
)
|
||||||
SliderChoice(
|
SliderChoice(
|
||||||
startSlot = { Text(text = "G") },
|
startSlot = { Text(text = "G") },
|
||||||
endSlot = { RollingNumber(number = g, length = 3) },
|
endSlot = { RollingNumber(number = g.value, length = 3) },
|
||||||
range = 0..255,
|
state = g,
|
||||||
field = gf,
|
|
||||||
)
|
)
|
||||||
SliderChoice(
|
SliderChoice(
|
||||||
startSlot = { Text(text = "B") },
|
startSlot = { Text(text = "B") },
|
||||||
endSlot = { RollingNumber(number = b, length = 3) },
|
endSlot = { RollingNumber(number = b.value, length = 3) },
|
||||||
range = 0..255,
|
state = b,
|
||||||
field = bf,
|
|
||||||
)
|
)
|
||||||
SliderChoice(
|
SliderChoice(
|
||||||
startSlot = { Text(text = "A") },
|
startSlot = { Text(text = "A") },
|
||||||
endSlot = { RollingNumber(number = a, length = 3) },
|
endSlot = { RollingNumber(number = a.value, length = 3) },
|
||||||
range = 0..255,
|
state = a,
|
||||||
field = af,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpinnerChoice(
|
SpinnerChoice(
|
||||||
title = stringResource(id = R.string.pref_color_filter_mode),
|
title = stringResource(id = R.string.pref_color_filter_mode),
|
||||||
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.color_filter_modes),
|
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.color_filter_modes),
|
||||||
values = listOf(0, 1, 2, 3, 4, 5),
|
values = [0, 1, 2, 3, 4, 5],
|
||||||
field = Settings.colorFilterMode.asMutableState(),
|
field = Settings.colorFilterMode.asMutableState(),
|
||||||
)
|
)
|
||||||
SwitchChoice(
|
SwitchChoice(
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package com.hippo.ehviewer.ui.reader
|
package com.hippo.ehviewer.ui.reader
|
||||||
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -11,8 +16,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.WavyProgressIndicatorDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -50,7 +57,6 @@ import com.hippo.ehviewer.gallery.progressObserved
|
|||||||
import com.hippo.ehviewer.gallery.statusObserved
|
import com.hippo.ehviewer.gallery.statusObserved
|
||||||
import com.hippo.ehviewer.image.Image
|
import com.hippo.ehviewer.image.Image
|
||||||
import com.hippo.ehviewer.util.AdsPlaceholderFile
|
import com.hippo.ehviewer.util.AdsPlaceholderFile
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.CombinedCircularProgressIndicator
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
|
|
||||||
@@ -83,7 +89,22 @@ fun PagerItem(
|
|||||||
modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT),
|
modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CombinedCircularProgressIndicator(progress = state.progressObserved)
|
AnimatedContent(
|
||||||
|
targetState = state is PageStatus.Loading,
|
||||||
|
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||||
|
label = "progressState",
|
||||||
|
) { determinate ->
|
||||||
|
val animatedProgress by animateFloatAsState(
|
||||||
|
targetValue = state.progressObserved,
|
||||||
|
animationSpec = WavyProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||||
|
label = "progress",
|
||||||
|
)
|
||||||
|
if (determinate) {
|
||||||
|
CircularWavyProgressIndicator(progress = { animatedProgress })
|
||||||
|
} else {
|
||||||
|
CircularWavyProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is PageStatus.Ready -> {
|
is PageStatus.Ready -> {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ private fun PagerSetting() = Column {
|
|||||||
SpinnerChoice(
|
SpinnerChoice(
|
||||||
title = stringResource(id = R.string.pref_viewer_nav),
|
title = stringResource(id = R.string.pref_viewer_nav),
|
||||||
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.pager_nav),
|
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.pager_nav),
|
||||||
values = listOf(0, 1, 2, 3, 4, 5),
|
values = [0, 1, 2, 3, 4, 5],
|
||||||
field = pagerNav,
|
field = pagerNav,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = pagerNav.value != 5) {
|
AnimatedVisibility(visible = pagerNav.value != 5) {
|
||||||
@@ -66,7 +66,7 @@ private fun PagerSetting() = Column {
|
|||||||
SpinnerChoice(
|
SpinnerChoice(
|
||||||
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
||||||
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
|
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
|
||||||
values = listOf(0, 1, 2, 3),
|
values = [0, 1, 2, 3],
|
||||||
field = Settings.readerPagerNavInverted.asMutableState(),
|
field = Settings.readerPagerNavInverted.asMutableState(),
|
||||||
)
|
)
|
||||||
SwitchChoice(
|
SwitchChoice(
|
||||||
@@ -79,7 +79,7 @@ private fun PagerSetting() = Column {
|
|||||||
SpinnerChoice(
|
SpinnerChoice(
|
||||||
title = stringResource(id = R.string.pref_image_scale_type),
|
title = stringResource(id = R.string.pref_image_scale_type),
|
||||||
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.image_scale_type),
|
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.image_scale_type),
|
||||||
values = listOf(1, 2, 3, 4, 5, 6),
|
values = [1, 2, 3, 4, 5, 6],
|
||||||
field = scaleType,
|
field = scaleType,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = scaleType.value == 1) {
|
AnimatedVisibility(visible = scaleType.value == 1) {
|
||||||
@@ -91,7 +91,7 @@ private fun PagerSetting() = Column {
|
|||||||
SpinnerChoice(
|
SpinnerChoice(
|
||||||
title = stringResource(id = R.string.pref_zoom_start),
|
title = stringResource(id = R.string.pref_zoom_start),
|
||||||
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.zoom_start),
|
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.zoom_start),
|
||||||
values = listOf(1, 2, 3, 4),
|
values = [1, 2, 3, 4],
|
||||||
field = Settings.zoomStart.asMutableState(),
|
field = Settings.zoomStart.asMutableState(),
|
||||||
)
|
)
|
||||||
SwitchChoice(
|
SwitchChoice(
|
||||||
@@ -111,14 +111,14 @@ private fun WebtoonSetting() = Column {
|
|||||||
SpinnerChoice(
|
SpinnerChoice(
|
||||||
title = stringResource(id = R.string.pref_viewer_nav),
|
title = stringResource(id = R.string.pref_viewer_nav),
|
||||||
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.webtoon_nav),
|
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.webtoon_nav),
|
||||||
values = listOf(0, 1, 2, 3, 4, 5),
|
values = [0, 1, 2, 3, 4, 5],
|
||||||
field = webtoonNav,
|
field = webtoonNav,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = webtoonNav.value != 5) {
|
AnimatedVisibility(visible = webtoonNav.value != 5) {
|
||||||
SpinnerChoice(
|
SpinnerChoice(
|
||||||
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
||||||
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
|
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
|
||||||
values = listOf(0, 1, 2, 3),
|
values = [0, 1, 2, 3],
|
||||||
field = Settings.readerWebtoonNavInverted.asMutableState(),
|
field = Settings.readerWebtoonNavInverted.asMutableState(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.only
|
|||||||
import androidx.compose.foundation.layout.safeDrawing
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material3.BottomSheetDefaults
|
import androidx.compose.material3.BottomSheetDefaults
|
||||||
@@ -26,9 +25,10 @@ import androidx.compose.material3.CircularWavyProgressIndicator
|
|||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -92,6 +92,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderContentOverlay
|
|||||||
import eu.kanade.tachiyomi.ui.reader.ReaderPageSheetMeta
|
import eu.kanade.tachiyomi.ui.reader.ReaderPageSheetMeta
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
@@ -214,9 +215,9 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
|||||||
uiController.showTransientSystemBarsBySwipe = false
|
uiController.showTransientSystemBarsBySwipe = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val lazyListState = rememberLazyListState(LazyLayoutCacheWindow(SCROLL_FRACTION, SCROLL_FRACTION), pageLoader.startPage)
|
val lazyListState = rememberLazyListState(pageLoader.startPage)
|
||||||
val pagerState = rememberPagerState(pageLoader.startPage) { pageLoader.size }
|
val pagerState = rememberPagerState(pageLoader.startPage) { pageLoader.size }
|
||||||
val syncState = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
|
val sync = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
|
||||||
var appbarVisible by remember { mutableStateOf(false) }
|
var appbarVisible by remember { mutableStateOf(false) }
|
||||||
val isWebtoon by rememberUpdatedState(ReadingModeType.isWebtoon(readingMode))
|
val isWebtoon by rememberUpdatedState(ReadingModeType.isWebtoon(readingMode))
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
@@ -231,7 +232,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
syncState.Sync(isWebtoon) { appbarVisible = false }
|
sync.Sync(isWebtoon) { appbarVisible = false }
|
||||||
val bgColor by collectBackgroundColorAsState()
|
val bgColor by collectBackgroundColorAsState()
|
||||||
val isDarkTheme = isSystemInDarkTheme()
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
LaunchedEffect(isDarkTheme) {
|
LaunchedEffect(isDarkTheme) {
|
||||||
@@ -251,7 +252,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
|||||||
val blocked = page.status is PageStatus.Blocked
|
val blocked = page.status is PageStatus.Blocked
|
||||||
dialog { cont ->
|
dialog { cont ->
|
||||||
fun dispose() = cont.resume(Unit)
|
fun dispose() = cont.resume(Unit)
|
||||||
val state = rememberModalBottomSheetState()
|
val state = rememberBottomSheetState(SheetValue.Hidden)
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { dispose() },
|
onDismissRequest = { dispose() },
|
||||||
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
|
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
|
||||||
@@ -292,10 +293,10 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
|||||||
onNavigationModeChange = { showNavigationOverlay = true },
|
onNavigationModeChange = { showNavigationOverlay = true },
|
||||||
onSelectPage = onSelectPage,
|
onSelectPage = onSelectPage,
|
||||||
onMenuRegionClick = { appbarVisible = !appbarVisible },
|
onMenuRegionClick = { appbarVisible = !appbarVisible },
|
||||||
modifier = Modifier.background(bgColor).pointerInput(syncState) {
|
modifier = Modifier.background(bgColor).pointerInput(sync) {
|
||||||
awaitEachGesture {
|
awaitEachGesture {
|
||||||
waitForUpOrCancellation()
|
waitForUpOrCancellation()
|
||||||
syncState.reset()
|
sync.reset()
|
||||||
showNavigationOverlay = false
|
showNavigationOverlay = false
|
||||||
}
|
}
|
||||||
}.fillMaxSize().windowInsetsPadding(insets),
|
}.fillMaxSize().windowInsetsPadding(insets),
|
||||||
@@ -323,7 +324,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
|||||||
)
|
)
|
||||||
if (brightness) {
|
if (brightness) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
Settings.customBrightnessValue.valueFlow().sample(100)
|
Settings.customBrightnessValue.valueFlow().sample(100.milliseconds)
|
||||||
.onCompletion { activity.setCustomBrightnessValue(0) }
|
.onCompletion { activity.setCustomBrightnessValue(0) }
|
||||||
.collect { activity.setCustomBrightnessValue(it) }
|
.collect { activity.setCustomBrightnessValue(it) }
|
||||||
}
|
}
|
||||||
@@ -332,7 +333,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
|||||||
if (showPageNumber && !appbarVisible) {
|
if (showPageNumber && !appbarVisible) {
|
||||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
|
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
|
||||||
PageIndicatorText(
|
PageIndicatorText(
|
||||||
currentPage = syncState.sliderValue,
|
currentPage = sync.slider.value,
|
||||||
totalPages = pageLoader.size,
|
totalPages = pageLoader.size,
|
||||||
modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
|
modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
|
||||||
)
|
)
|
||||||
@@ -343,9 +344,9 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
|||||||
title = pageLoader.title,
|
title = pageLoader.title,
|
||||||
isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT,
|
isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT,
|
||||||
showSeekBar = showSeekbar,
|
showSeekBar = showSeekbar,
|
||||||
currentPage = syncState.sliderValue,
|
slider = sync.slider,
|
||||||
totalPages = pageLoader.size,
|
totalPages = pageLoader.size,
|
||||||
onSliderValueChange = syncState::sliderScrollTo,
|
onSliderValueChange = sync::sliderScrollTo,
|
||||||
onClickSettings = {
|
onClickSettings = {
|
||||||
launch {
|
launch {
|
||||||
dialog { cont ->
|
dialog { cont ->
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import com.ehviewer.core.ui.component.IntSliderState
|
||||||
import com.hippo.ehviewer.gallery.PageLoader
|
import com.hippo.ehviewer.gallery.PageLoader
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
@@ -23,12 +23,11 @@ class SliderPagerDoubleSync(
|
|||||||
private val pageLoader: PageLoader,
|
private val pageLoader: PageLoader,
|
||||||
) {
|
) {
|
||||||
private var sliderFollowPager by mutableStateOf(true)
|
private var sliderFollowPager by mutableStateOf(true)
|
||||||
var sliderValue by mutableIntStateOf(pageLoader.startPage + 1)
|
val slider = IntSliderState(pageLoader.startPage + 1, 1..pageLoader.size)
|
||||||
private set
|
|
||||||
|
|
||||||
fun sliderScrollTo(index: Int) {
|
fun sliderScrollTo(index: Int) {
|
||||||
sliderFollowPager = false
|
sliderFollowPager = false
|
||||||
sliderValue = index.coerceIn(1, pageLoader.size)
|
slider.value = index
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
@@ -50,7 +49,7 @@ class SliderPagerDoubleSync(
|
|||||||
@Composable
|
@Composable
|
||||||
fun Sync(webtoon: Boolean, onPageSelected: () -> Unit) {
|
fun Sync(webtoon: Boolean, onPageSelected: () -> Unit) {
|
||||||
val currentIndexFlow = remember(webtoon) {
|
val currentIndexFlow = remember(webtoon) {
|
||||||
val initialIndex = sliderValue - 1
|
val initialIndex = slider.value - 1
|
||||||
sliderFollowPager = if (webtoon) {
|
sliderFollowPager = if (webtoon) {
|
||||||
lazyListState.firstVisibleItemIndex == initialIndex
|
lazyListState.firstVisibleItemIndex == initialIndex
|
||||||
} else {
|
} else {
|
||||||
@@ -61,14 +60,14 @@ class SliderPagerDoubleSync(
|
|||||||
if (sliderFollowPager) {
|
if (sliderFollowPager) {
|
||||||
LaunchedEffect(currentIndexFlow) {
|
LaunchedEffect(currentIndexFlow) {
|
||||||
currentIndexFlow.drop(1).collect { index ->
|
currentIndexFlow.drop(1).collect { index ->
|
||||||
sliderValue = index + 1
|
slider.value = index + 1
|
||||||
pageLoader.startPage = index
|
pageLoader.startPage = index
|
||||||
onPageSelected()
|
onPageSelected()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LaunchedEffect(webtoon) {
|
LaunchedEffect(webtoon) {
|
||||||
snapshotFlow { sliderValue - 1 }.collectLatest { index ->
|
snapshotFlow { slider.value - 1 }.collectLatest { index ->
|
||||||
if (webtoon) {
|
if (webtoon) {
|
||||||
lazyListState.scrollToItem(index)
|
lazyListState.scrollToItem(index)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -81,6 +82,7 @@ fun WebtoonViewer(
|
|||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
contentPadding = PaddingValues(horizontal = sidePadding),
|
contentPadding = PaddingValues(horizontal = sidePadding),
|
||||||
verticalArrangement = Arrangement.spacedBy(if (withGaps) 15.dp else 0.dp),
|
verticalArrangement = Arrangement.spacedBy(if (withGaps) 15.dp else 0.dp),
|
||||||
|
cacheWindow = LazyLayoutCacheWindow(SCROLL_FRACTION, SCROLL_FRACTION),
|
||||||
) {
|
) {
|
||||||
items(items, key = { it.index }) { page ->
|
items(items, key = { it.index }) { page ->
|
||||||
PagerItem(
|
PagerItem(
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.ehviewer.core.ui.component.IntSliderState
|
||||||
import com.ehviewer.core.ui.component.Slider
|
import com.ehviewer.core.ui.component.Slider
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -84,20 +85,17 @@ fun SwitchChoice(title: String, field: MutableState<Boolean>) {
|
|||||||
fun SliderChoice(
|
fun SliderChoice(
|
||||||
startSlot: @Composable () -> Unit,
|
startSlot: @Composable () -> Unit,
|
||||||
endSlot: @Composable () -> Unit,
|
endSlot: @Composable () -> Unit,
|
||||||
range: IntRange,
|
state: IntSliderState,
|
||||||
field: MutableState<Int>,
|
|
||||||
) {
|
) {
|
||||||
var value by field
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
startSlot()
|
startSlot()
|
||||||
Slider(
|
Slider(
|
||||||
value = value,
|
state = state,
|
||||||
onValueChange = { value = it },
|
onValueChange = { state.value = it },
|
||||||
modifier = Modifier.weight(1f).padding(8.dp),
|
modifier = Modifier.weight(1f).padding(8.dp),
|
||||||
valueRange = range,
|
|
||||||
)
|
)
|
||||||
endSlot()
|
endSlot()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
|
|||||||
) {
|
) {
|
||||||
faves.forEachIndexed { index, (name, count) ->
|
faves.forEachIndexed { index, (name, count) ->
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(text = name) },
|
|
||||||
trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) },
|
trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) },
|
||||||
modifier = Modifier.clip(CardDefaults.shape).clickable {
|
modifier = Modifier.clip(CardDefaults.shape).clickable {
|
||||||
val newCat = index - 2
|
val newCat = index - 2
|
||||||
@@ -169,6 +168,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
|
|||||||
launch { sheetState.close() }
|
launch { sheetState.close() }
|
||||||
},
|
},
|
||||||
colors = listItemOnDrawerColor(urlBuilder.favCat == index - 2),
|
colors = listItemOnDrawerColor(urlBuilder.favCat == index - 2),
|
||||||
|
content = { Text(text = name) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,12 +260,8 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
|
|||||||
Column {
|
Column {
|
||||||
data.forEach { (name, vote) ->
|
data.forEach { (name, vote) ->
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
trailingContent = { Text(text = vote) },
|
||||||
Text(text = name)
|
content = { Text(text = name) },
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
Text(text = vote)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,7 +405,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
|
|||||||
navigate(
|
navigate(
|
||||||
ListUrlBuilder(
|
ListUrlBuilder(
|
||||||
mode = ListUrlBuilder.MODE_UPLOADER,
|
mode = ListUrlBuilder.MODE_UPLOADER,
|
||||||
mKeyword = item.user,
|
keyword = item.user,
|
||||||
).asDst(),
|
).asDst(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ fun GalleryDetailContent(
|
|||||||
if (uploader.isNullOrEmpty() || disowned) {
|
if (uploader.isNullOrEmpty() || disowned) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, mKeyword = uploader).asDst())
|
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, keyword = uploader).asDst())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onGalleryInfoCardClick() {
|
fun onGalleryInfoCardClick() {
|
||||||
@@ -529,21 +529,21 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
|
|||||||
navigate(
|
navigate(
|
||||||
ListUrlBuilder(
|
ListUrlBuilder(
|
||||||
mode = ListUrlBuilder.MODE_NORMAL,
|
mode = ListUrlBuilder.MODE_NORMAL,
|
||||||
mKeyword = "\"" + keyword + "\"",
|
keyword = "\"" + keyword + "\"",
|
||||||
).asDst(),
|
).asDst(),
|
||||||
)
|
)
|
||||||
} else if (artistTag != null) {
|
} else if (artistTag != null) {
|
||||||
navigate(
|
navigate(
|
||||||
ListUrlBuilder(
|
ListUrlBuilder(
|
||||||
mode = ListUrlBuilder.MODE_TAG,
|
mode = ListUrlBuilder.MODE_TAG,
|
||||||
mKeyword = artistTag,
|
keyword = artistTag,
|
||||||
).asDst(),
|
).asDst(),
|
||||||
)
|
)
|
||||||
} else if (null != galleryDetail.uploader) {
|
} else if (null != galleryDetail.uploader) {
|
||||||
navigate(
|
navigate(
|
||||||
ListUrlBuilder(
|
ListUrlBuilder(
|
||||||
mode = ListUrlBuilder.MODE_UPLOADER,
|
mode = ListUrlBuilder.MODE_UPLOADER,
|
||||||
mKeyword = galleryDetail.uploader,
|
keyword = galleryDetail.uploader,
|
||||||
).asDst(),
|
).asDst(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -721,7 +721,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
|
|||||||
val upTag = stringResource(R.string.tag_vote_up)
|
val upTag = stringResource(R.string.tag_vote_up)
|
||||||
val downTag = stringResource(R.string.tag_vote_down)
|
val downTag = stringResource(R.string.tag_vote_down)
|
||||||
val withDraw = stringResource(R.string.tag_vote_withdraw)
|
val withDraw = stringResource(R.string.tag_vote_withdraw)
|
||||||
fun search(tag: String) = navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, mKeyword = tag).asDst())
|
fun search(tag: String) = navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, keyword = tag).asDst())
|
||||||
GalleryTags(
|
GalleryTags(
|
||||||
tagGroups = tags,
|
tagGroups = tags,
|
||||||
onTagClick = ::search,
|
onTagClick = ::search,
|
||||||
|
|||||||
@@ -49,14 +49,12 @@ import com.ehviewer.core.ui.util.rememberInVM
|
|||||||
import com.ehviewer.core.util.launchIO
|
import com.ehviewer.core.util.launchIO
|
||||||
import com.ehviewer.core.util.logcat
|
import com.ehviewer.core.util.logcat
|
||||||
import com.ehviewer.core.util.withIOContext
|
import com.ehviewer.core.util.withIOContext
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.client.data.fillInfo
|
import com.hippo.ehviewer.client.data.fillInfo
|
||||||
import com.hippo.ehviewer.client.getImageKey
|
|
||||||
import com.hippo.ehviewer.coil.justDownload
|
import com.hippo.ehviewer.coil.justDownload
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
import com.hippo.ehviewer.ktbuilder.executeIn
|
import com.hippo.ehviewer.ktbuilder.executeIn
|
||||||
@@ -250,10 +248,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
|
|||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = R.string.clear_image_cache_confirm))
|
Text(text = stringResource(id = R.string.clear_image_cache_confirm))
|
||||||
}
|
}
|
||||||
repeat(gd.pages) {
|
SpiderDen(gd).clearCache()
|
||||||
val key = getImageKey(gd.gid, it)
|
|
||||||
imageCache.remove(key)
|
|
||||||
}
|
|
||||||
snackbar(cacheCleared)
|
snackbar(cacheCleared)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ fun AnimatedVisibilityScope.WhatshotScreen(navigator: DestinationsNavigator) = G
|
|||||||
|
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimatedVisibilityScope.ToplistScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_TOPLIST, mKeyword = Settings.recentToplist), navigator)
|
fun AnimatedVisibilityScope.ToplistScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_TOPLIST, keyword = Settings.recentToplist), navigator)
|
||||||
|
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
@@ -172,10 +172,14 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
|||||||
|
|
||||||
LaunchedEffect(urlBuilder) {
|
LaunchedEffect(urlBuilder) {
|
||||||
if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category
|
if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category
|
||||||
var keyword = urlBuilder.keyword.takeUnless { urlBuilder.mode == MODE_TOPLIST }.orEmpty()
|
val keyword = urlBuilder.keyword?.let { keyword ->
|
||||||
if (urlBuilder.mode == MODE_TAG) {
|
when (urlBuilder.mode) {
|
||||||
keyword = wrapTagKeyword(keyword)
|
MODE_UPLOADER -> "uploader:\"$keyword\""
|
||||||
}
|
MODE_TAG -> wrapTagKeyword(keyword)
|
||||||
|
MODE_TOPLIST -> ""
|
||||||
|
else -> keyword
|
||||||
|
}
|
||||||
|
}.orEmpty()
|
||||||
searchFieldState.setTextAndPlaceCursorAtEnd(keyword)
|
searchFieldState.setTextAndPlaceCursorAtEnd(keyword)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,15 +221,13 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
|||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.padding(horizontal = 4.dp).clip(CardDefaults.shape).clickable {
|
modifier = Modifier.padding(horizontal = 4.dp).clip(CardDefaults.shape).clickable {
|
||||||
Settings.recentToplist = keyword
|
Settings.recentToplist = keyword
|
||||||
urlBuilder = ListUrlBuilder(MODE_TOPLIST, mKeyword = keyword)
|
urlBuilder = ListUrlBuilder(MODE_TOPLIST, keyword = keyword)
|
||||||
data.refresh()
|
data.refresh()
|
||||||
fabHidden = false
|
fabHidden = false
|
||||||
launch { sheetState.close() }
|
launch { sheetState.close() }
|
||||||
},
|
},
|
||||||
headlineContent = {
|
|
||||||
Text(text = name)
|
|
||||||
},
|
|
||||||
colors = listItemOnDrawerColor(urlBuilder.keyword == keyword),
|
colors = listItemOnDrawerColor(urlBuilder.keyword == keyword),
|
||||||
|
content = { Text(text = name) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -480,9 +482,9 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
|||||||
searchFieldState = searchFieldState,
|
searchFieldState = searchFieldState,
|
||||||
suggestionProvider = {
|
suggestionProvider = {
|
||||||
GalleryDetailUrlParser.parse(it, false)?.run {
|
GalleryDetailUrlParser.parse(it, false)?.run {
|
||||||
listOf(GalleryDetailUrlSuggestion(gid, token))
|
[GalleryDetailUrlSuggestion(gid, token)]
|
||||||
} ?: GalleryPageUrlParser.parse(it, false)?.run {
|
} ?: GalleryPageUrlParser.parse(it, false)?.run {
|
||||||
listOf(GalleryPageUrlSuggestion(gid, pToken, page))
|
[GalleryPageUrlSuggestion(gid, pToken, page)]
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
},
|
},
|
||||||
localSearch = false,
|
localSearch = false,
|
||||||
@@ -578,7 +580,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
|||||||
onExpandChanged = { fabExpanded = it },
|
onExpandChanged = { fabExpanded = it },
|
||||||
autoCancel = true,
|
autoCancel = true,
|
||||||
) {
|
) {
|
||||||
if (urlBuilder.mode in arrayOf(MODE_NORMAL, MODE_UPLOADER, MODE_TAG)) {
|
if (urlBuilder.mode in [MODE_NORMAL, MODE_UPLOADER, MODE_TAG]) {
|
||||||
onClick(Icons.Default.Shuffle) {
|
onClick(Icons.Default.Shuffle) {
|
||||||
urlBuilder.setRange(Random.nextInt(100))
|
urlBuilder.setRange(Random.nextInt(100))
|
||||||
data.refresh()
|
data.refresh()
|
||||||
@@ -641,6 +643,7 @@ private fun getSuitableTitleForUrlBuilder(urlBuilder: ListUrlBuilder, appName: B
|
|||||||
val canTranslate = Settings.showTagTranslations.value && EhTagDatabase.translatable && EhTagDatabase.initialized
|
val canTranslate = Settings.showTagTranslations.value && EhTagDatabase.translatable && EhTagDatabase.initialized
|
||||||
wrapTagKeyword(keyword, canTranslate)
|
wrapTagKeyword(keyword, canTranslate)
|
||||||
}
|
}
|
||||||
|
MODE_UPLOADER -> "uploader:\"$keyword\""
|
||||||
else -> keyword
|
else -> keyword
|
||||||
}
|
}
|
||||||
} else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) {
|
} else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) {
|
||||||
|
|||||||
@@ -294,7 +294,6 @@ fun SearchBarScreen(
|
|||||||
item {}
|
item {}
|
||||||
items(mSuggestionList, key = { it.keyword.hashCode() * 31 + it.canDelete.hashCode() }) {
|
items(mSuggestionList, key = { it.keyword.hashCode() * 31 + it.canDelete.hashCode() }) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(text = it.keyword) },
|
|
||||||
supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) },
|
supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) },
|
||||||
leadingContent = it.canOpenDirectly.ifTrueThen {
|
leadingContent = it.canOpenDirectly.ifTrueThen {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -312,6 +311,7 @@ fun SearchBarScreen(
|
|||||||
},
|
},
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
modifier = Modifier.clickable { it.onClick() }.thenIf(animateItems) { animateItem() },
|
modifier = Modifier.clickable { it.onClick() }.thenIf(animateItems) { animateItem() },
|
||||||
|
content = { Text(text = it.keyword) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import com.hippo.ehviewer.ui.main.NavigationIcon
|
|||||||
import com.hippo.ehviewer.ui.tools.DialogState
|
import com.hippo.ehviewer.ui.tools.DialogState
|
||||||
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
|
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
|
||||||
import com.hippo.ehviewer.updater.AppUpdater
|
import com.hippo.ehviewer.updater.AppUpdater
|
||||||
import com.hippo.ehviewer.updater.Release
|
import com.hippo.ehviewer.updater.EhRelease
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.ReadableTime
|
import com.hippo.ehviewer.util.ReadableTime
|
||||||
import com.hippo.ehviewer.util.displayString
|
import com.hippo.ehviewer.util.displayString
|
||||||
@@ -49,8 +49,8 @@ import moe.tarsin.navigate
|
|||||||
import moe.tarsin.snackbar
|
import moe.tarsin.snackbar
|
||||||
import moe.tarsin.string
|
import moe.tarsin.string
|
||||||
|
|
||||||
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}"
|
const val REPO_URL = "https://codeberg.org/${BuildConfig.REPO_NAME}"
|
||||||
private const val RELEASE_URL = "$REPO_URL/releases"
|
const val RELEASE_URL = "$REPO_URL/releases"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Stable
|
@Stable
|
||||||
@@ -124,7 +124,7 @@ fun AnimatedVisibilityScope.AboutScreen(navigator: DestinationsNavigator) = Scre
|
|||||||
}
|
}
|
||||||
|
|
||||||
context(_: Context, _: DialogState)
|
context(_: Context, _: DialogState)
|
||||||
suspend fun showNewVersion(release: Release) {
|
suspend fun showNewVersion(release: EhRelease) {
|
||||||
awaitConfirmationOrCancel(
|
awaitConfirmationOrCancel(
|
||||||
confirmText = R.string.download,
|
confirmText = R.string.download,
|
||||||
title = R.string.new_version_available,
|
title = R.string.new_version_available,
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
@@ -214,7 +215,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
|
|||||||
step = 3,
|
step = 3,
|
||||||
title = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold),
|
title = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold),
|
||||||
summary = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold_summary),
|
summary = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold_summary),
|
||||||
state = Settings.hardwareBitmapThreshold.asMutableState(),
|
pref = Settings.hardwareBitmapThreshold,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
@@ -289,7 +290,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
|
|||||||
EhDB.putLocalFavorites(result.galleryInfoList)
|
EhDB.putLocalFavorites(result.galleryInfoList)
|
||||||
launchSnackbar(string(R.string.settings_advanced_backup_favorite_start, status))
|
launchSnackbar(string(R.string.settings_advanced_backup_favorite_start, status))
|
||||||
if (result.next != null) {
|
if (result.next != null) {
|
||||||
delay(Settings.downloadDelay.value.toLong())
|
delay(Settings.downloadDelay.value.milliseconds)
|
||||||
favListUrlBuilder.setIndex(result.next, true)
|
favListUrlBuilder.setIndex(result.next, true)
|
||||||
doBackup()
|
doBackup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
|
|||||||
maxValue = 10,
|
maxValue = 10,
|
||||||
minValue = MIN_SPEED_LEVEL,
|
minValue = MIN_SPEED_LEVEL,
|
||||||
title = stringResource(id = R.string.settings_download_timeout_speed),
|
title = stringResource(id = R.string.settings_download_timeout_speed),
|
||||||
state = Settings.timeoutSpeed.asMutableState(),
|
pref = Settings.timeoutSpeed,
|
||||||
display = ::speedLevelToSpeed,
|
display = ::speedLevelToSpeed,
|
||||||
)
|
)
|
||||||
val preloadImage = Settings.preloadImage.asMutableState()
|
val preloadImage = Settings.preloadImage.asMutableState()
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
|
|||||||
minValue = 20,
|
minValue = 20,
|
||||||
step = 7,
|
step = 7,
|
||||||
title = stringResource(id = R.string.list_tile_thumb_size),
|
title = stringResource(id = R.string.list_tile_thumb_size),
|
||||||
state = Settings.listThumbSize.asMutableState(),
|
pref = Settings.listThumbSize,
|
||||||
)
|
)
|
||||||
SimpleMenuPreferenceInt(
|
SimpleMenuPreferenceInt(
|
||||||
title = stringResource(id = R.string.settings_eh_detail_size),
|
title = stringResource(id = R.string.settings_eh_detail_size),
|
||||||
@@ -207,7 +207,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
|
|||||||
maxValue = 10,
|
maxValue = 10,
|
||||||
minValue = 1,
|
minValue = 1,
|
||||||
title = stringResource(id = R.string.settings_eh_thumb_columns),
|
title = stringResource(id = R.string.settings_eh_thumb_columns),
|
||||||
state = Settings.thumbColumns.asMutableState(),
|
pref = Settings.thumbColumns,
|
||||||
)
|
)
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = stringResource(id = R.string.settings_eh_show_gallery_pages),
|
title = stringResource(id = R.string.settings_eh_show_gallery_pages),
|
||||||
@@ -235,7 +235,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
|
|||||||
minValue = -101,
|
minValue = -101,
|
||||||
title = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold),
|
title = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold),
|
||||||
summary = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold_summary),
|
summary = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold_summary),
|
||||||
state = Settings.commentThreshold.asMutableState(),
|
pref = Settings.commentThreshold,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (EhTagDatabase.translatable) {
|
if (EhTagDatabase.translatable) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import androidx.compose.material3.AlertDialog
|
|||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenu
|
||||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package com.hippo.ehviewer.ui.settings
|
package com.hippo.ehviewer.ui.settings
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibilityScope
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@@ -12,14 +10,15 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.ehviewer.core.i18n.R
|
import com.ehviewer.core.i18n.R
|
||||||
import com.hippo.ehviewer.ui.Screen
|
import com.hippo.ehviewer.ui.Screen
|
||||||
import com.hippo.ehviewer.ui.main.NavigationIcon
|
import com.hippo.ehviewer.ui.main.NavigationIcon
|
||||||
import com.hippo.ehviewer.ui.openBrowser
|
import com.hippo.ehviewer.ui.openBrowser
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.variant.LibrariesVariant
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryActionKind
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryBadges
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
@@ -42,10 +41,19 @@ fun AnimatedVisibilityScope.LicenseScreen(navigator: DestinationsNavigator) = Sc
|
|||||||
libraries = libraries,
|
libraries = libraries,
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
padding = LibraryDefaults.libraryPadding(licensePadding = LibraryDefaults.chipPadding(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp))),
|
badges = LibraryBadges(funding = false),
|
||||||
textStyles = LibraryDefaults.libraryTextStyles(licensesTextStyle = MaterialTheme.typography.labelSmall),
|
variant = LibrariesVariant.Refined,
|
||||||
onLibraryClick = { library ->
|
onActionClick = { lib, kind ->
|
||||||
library.website?.let { openBrowser(it) }
|
val open = { url: String -> url.isNotBlank().also { if (it) openBrowser(url) } }
|
||||||
|
val website = lib.website.orEmpty()
|
||||||
|
val source = lib.scm?.url.orEmpty()
|
||||||
|
val license = lib.licenses.firstOrNull()?.url.orEmpty()
|
||||||
|
when (kind) {
|
||||||
|
LibraryActionKind.Source -> open(source)
|
||||||
|
LibraryActionKind.Website -> open(website)
|
||||||
|
LibraryActionKind.License -> open(license)
|
||||||
|
LibraryActionKind.Sponsor -> false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import androidx.compose.ui.res.integerArrayResource
|
|||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import com.ehviewer.core.preferences.PrefDelegate
|
||||||
import com.ehviewer.core.ui.component.RollingNumber
|
import com.ehviewer.core.ui.component.RollingNumber
|
||||||
|
import com.ehviewer.core.ui.component.rememberIntPrefSliderState
|
||||||
import com.hippo.ehviewer.ui.openBrowser
|
import com.hippo.ehviewer.ui.openBrowser
|
||||||
import com.hippo.ehviewer.util.ProgressDialog
|
import com.hippo.ehviewer.util.ProgressDialog
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
@@ -71,19 +73,16 @@ fun SwitchPreference(title: String, summary: String? = null, state: MutableState
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, title: String, summary: String? = null, state: MutableState<Int>, enabled: Boolean = true, display: (Int) -> Int = { it }) {
|
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, title: String, summary: String? = null, pref: PrefDelegate<Int>, enabled: Boolean = true, display: (Int) -> Int = { it }) {
|
||||||
val sliderState = remember { mutableIntStateOf(state.value) }
|
val state = rememberIntPrefSliderState(pref, minValue..maxValue, step)
|
||||||
IntSliderPreference(
|
IntSliderPreference(
|
||||||
state = state,
|
state = state,
|
||||||
title = { Text(title) },
|
title = { Text(title) },
|
||||||
valueRange = minValue..maxValue,
|
|
||||||
valueSteps = step,
|
|
||||||
sliderState = sliderState,
|
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
summary = summary?.let { { Text(it) } },
|
summary = summary?.let { { Text(it) } },
|
||||||
valueText = {
|
valueText = {
|
||||||
RollingNumber(
|
RollingNumber(
|
||||||
number = display(sliderState.intValue),
|
number = display(state.value),
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
length = 5,
|
length = 5,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -17,6 +18,7 @@ import com.ehviewer.core.util.launch
|
|||||||
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
|
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.asMutableState
|
import com.hippo.ehviewer.asMutableState
|
||||||
|
import com.hippo.ehviewer.collectAsState
|
||||||
import com.hippo.ehviewer.ui.Screen
|
import com.hippo.ehviewer.ui.Screen
|
||||||
import com.hippo.ehviewer.ui.isAuthenticationSupported
|
import com.hippo.ehviewer.ui.isAuthenticationSupported
|
||||||
import com.hippo.ehviewer.ui.main.NavigationIcon
|
import com.hippo.ehviewer.ui.main.NavigationIcon
|
||||||
@@ -48,17 +50,17 @@ fun AnimatedVisibilityScope.PrivacyScreen(navigator: DestinationsNavigator) = Sc
|
|||||||
enabled = isAuthenticationSupported(),
|
enabled = isAuthenticationSupported(),
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = security.value) {
|
AnimatedVisibility(visible = security.value) {
|
||||||
val securityDelay = Settings.securityDelay.asMutableState()
|
val delay by Settings.securityDelay.collectAsState()
|
||||||
val summary = if (securityDelay.value == 0) {
|
val summary = if (delay == 0) {
|
||||||
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary_immediately)
|
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary_immediately)
|
||||||
} else {
|
} else {
|
||||||
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary, securityDelay.value)
|
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary, delay)
|
||||||
}
|
}
|
||||||
IntSliderPreference(
|
IntSliderPreference(
|
||||||
maxValue = 30,
|
maxValue = 30,
|
||||||
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
|
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
|
||||||
summary = summary,
|
summary = summary,
|
||||||
state = securityDelay,
|
pref = Settings.securityDelay,
|
||||||
enabled = isAuthenticationSupported(),
|
enabled = isAuthenticationSupported(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ fun AnnotatedString.normalizeSpan(): AnnotatedString {
|
|||||||
val italic = filterMerged(SpanStyle(fontStyle = FontStyle.Italic)) { it.fontStyle == FontStyle.Italic }
|
val italic = filterMerged(SpanStyle(fontStyle = FontStyle.Italic)) { it.fontStyle == FontStyle.Italic }
|
||||||
val underline = filterMerged(SpanStyle(textDecoration = TextDecoration.Underline)) { it.textDecoration == TextDecoration.Underline }
|
val underline = filterMerged(SpanStyle(textDecoration = TextDecoration.Underline)) { it.textDecoration == TextDecoration.Underline }
|
||||||
val linethrough = filterMerged(SpanStyle(textDecoration = TextDecoration.LineThrough)) { it.textDecoration == TextDecoration.LineThrough }
|
val linethrough = filterMerged(SpanStyle(textDecoration = TextDecoration.LineThrough)) { it.textDecoration == TextDecoration.LineThrough }
|
||||||
listOf(bold, italic, underline, linethrough).flatten()
|
bold + italic + underline + linethrough
|
||||||
}
|
}
|
||||||
return AnnotatedString(text, spans)
|
return AnnotatedString(text, spans)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import androidx.compose.material3.DatePickerDefaults
|
|||||||
import androidx.compose.material3.DatePickerDialog
|
import androidx.compose.material3.DatePickerDialog
|
||||||
import androidx.compose.material3.DisplayMode
|
import androidx.compose.material3.DisplayMode
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenu
|
||||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
@@ -684,10 +685,10 @@ suspend fun awaitSelectItemWithIcon(
|
|||||||
}
|
}
|
||||||
itemsIndexed(items) { index, (icon, text) ->
|
itemsIndexed(items) { index, (icon, text) ->
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) },
|
|
||||||
modifier = Modifier.clickable { resume(index) }.padding(horizontal = 8.dp),
|
modifier = Modifier.clickable { resume(index) }.padding(horizontal = 8.dp),
|
||||||
leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) },
|
leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) },
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
content = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -756,17 +757,17 @@ private fun CheckableItem(text: String, checked: Boolean, modifier: Modifier = M
|
|||||||
val textStyle = MaterialTheme.typography.titleMedium
|
val textStyle = MaterialTheme.typography.titleMedium
|
||||||
val checkedColor = MaterialTheme.colorScheme.primary
|
val checkedColor = MaterialTheme.colorScheme.primary
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = if (checked) textStyle.copy(color = checkedColor) else textStyle,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
trailingContent = checked.ifTrueThen {
|
trailingContent = checked.ifTrueThen {
|
||||||
Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = checkedColor)
|
Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = checkedColor)
|
||||||
},
|
},
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
content = {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = if (checked) textStyle.copy(color = checkedColor) else textStyle,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.hippo.ehviewer.spider.timeoutBySpeed
|
|||||||
import com.hippo.ehviewer.util.copyTo
|
import com.hippo.ehviewer.util.copyTo
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
import io.ktor.client.request.accept
|
import io.ktor.client.request.accept
|
||||||
import io.ktor.client.request.bearerAuth
|
|
||||||
import io.ktor.client.request.prepareGet
|
import io.ktor.client.request.prepareGet
|
||||||
import io.ktor.client.statement.bodyAsChannel
|
import io.ktor.client.statement.bodyAsChannel
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
@@ -21,17 +20,18 @@ import kotlin.time.Instant
|
|||||||
import kotlinx.io.asSource
|
import kotlinx.io.asSource
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import okio.Path
|
import okio.Path
|
||||||
import tachiyomi.data.release.GithubArtifacts
|
import tachiyomi.data.release.Artifacts
|
||||||
import tachiyomi.data.release.GithubCommitComparison
|
import tachiyomi.data.release.CommitComparison
|
||||||
import tachiyomi.data.release.GithubRelease
|
import tachiyomi.data.release.Release
|
||||||
import tachiyomi.data.release.GithubRepo
|
import tachiyomi.data.release.Repository
|
||||||
import tachiyomi.data.release.GithubWorkflowRuns
|
import tachiyomi.data.release.WorkflowRuns
|
||||||
|
import tachiyomi.data.release.downloadLink
|
||||||
|
|
||||||
private const val API_URL = "https://api.github.com/repos/${BuildConfig.REPO_NAME}"
|
const val API_URL = "https://codeberg.org/api/v1/repos/${BuildConfig.REPO_NAME}"
|
||||||
private const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
|
const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
|
||||||
|
|
||||||
object AppUpdater {
|
object AppUpdater {
|
||||||
suspend fun checkForUpdate(forceCheck: Boolean = false): Release? {
|
suspend fun checkForUpdate(forceCheck: Boolean = false): EhRelease? {
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
val last = Instant.fromEpochSeconds(Settings.lastUpdateTime)
|
val last = Instant.fromEpochSeconds(Settings.lastUpdateTime)
|
||||||
val interval = Settings.updateIntervalDays.value
|
val interval = Settings.updateIntervalDays.value
|
||||||
@@ -39,31 +39,30 @@ object AppUpdater {
|
|||||||
Settings.lastUpdateTime = now.epochSeconds
|
Settings.lastUpdateTime = now.epochSeconds
|
||||||
if (Settings.useCIUpdateChannel.value) {
|
if (Settings.useCIUpdateChannel.value) {
|
||||||
val curSha = BuildConfig.COMMIT_SHA
|
val curSha = BuildConfig.COMMIT_SHA
|
||||||
val branch = ghStatement(API_URL).executeAndParseAs<GithubRepo>().defaultBranch
|
val branch = statement(API_URL).executeAndParseAs<Repository>().defaultBranch
|
||||||
val workflowRunsUrl = "$API_URL/actions/workflows/ci.yml/runs?branch=$branch&event=push&status=success&per_page=1"
|
val run = statement("$API_URL/actions/runs?event=push&status=success&ref=refs/heads/$branch").executeAndParseAs<WorkflowRuns>().workflowRuns[0]
|
||||||
val workflowRun = ghStatement(workflowRunsUrl).executeAndParseAs<GithubWorkflowRuns>().workflowRuns[0]
|
val shortSha = run.headSha.take(7)
|
||||||
val shortSha = workflowRun.headSha.take(7)
|
|
||||||
if (shortSha != curSha) {
|
if (shortSha != curSha) {
|
||||||
val artifacts = ghStatement(workflowRun.artifactsUrl).executeAndParseAs<GithubArtifacts>()
|
val artifacts = statement("$API_URL/actions/runs/${run.id}/artifacts").executeAndParseAs<Artifacts>()
|
||||||
val archiveUrl = artifacts.getDownloadLink()
|
val archiveUrl = artifacts.downloadLink
|
||||||
val changelog = runSuspendCatching {
|
val changelog = runSuspendCatching {
|
||||||
val commitComparisonUrl = "$API_URL/compare/$curSha...$shortSha"
|
val commitComparisonUrl = "$API_URL/compare/$curSha...$shortSha"
|
||||||
val result = ghStatement(commitComparisonUrl).executeAndParseAs<GithubCommitComparison>()
|
val result = statement(commitComparisonUrl).executeAndParseAs<CommitComparison>()
|
||||||
// TODO: Prettier format, Markdown?
|
// TODO: Prettier format, Markdown?
|
||||||
result.commits.joinToString("\n") { commit ->
|
result.commits.joinToString("\n") { commit ->
|
||||||
"${commit.commit.message.takeWhile { it != '\n' }} (@${commit.commit.author.name})"
|
"${commit.commit.message.takeWhile { it != '\n' }} (@${commit.commit.author.name})"
|
||||||
}
|
}
|
||||||
}.getOrDefault(workflowRun.title)
|
}.getOrDefault(run.title)
|
||||||
return Release(shortSha, changelog, archiveUrl)
|
return EhRelease(shortSha, changelog, archiveUrl)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val curVersion = BuildConfig.RAW_VERSION_NAME
|
val curVersion = BuildConfig.RAW_VERSION_NAME
|
||||||
val release = ghStatement(LATEST_RELEASE_URL).executeAndParseAs<GithubRelease>()
|
val release = statement(LATEST_RELEASE_URL).executeAndParseAs<Release>()
|
||||||
val latestVersion = release.version
|
val latestVersion = release.version
|
||||||
val description = release.info
|
val description = release.info
|
||||||
val downloadUrl = release.getDownloadLink()
|
val downloadUrl = release.getDownloadLink()
|
||||||
if (latestVersion != curVersion) {
|
if (latestVersion != curVersion) {
|
||||||
return Release(latestVersion, description, downloadUrl)
|
return EhRelease(latestVersion, description, downloadUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +74,7 @@ object AppUpdater {
|
|||||||
timeoutBySpeed(
|
timeoutBySpeed(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
ghStatement(url) {
|
statement(url) {
|
||||||
// https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#get-a-release-asset
|
// https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#get-a-release-asset
|
||||||
if (!isZip) accept(ContentType.Application.OctetStream)
|
if (!isZip) accept(ContentType.Application.OctetStream)
|
||||||
it()
|
it()
|
||||||
@@ -98,22 +97,14 @@ object AppUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend inline fun ghStatement(
|
suspend inline fun statement(
|
||||||
url: String,
|
url: String,
|
||||||
builder: HttpRequestBuilder.() -> Unit = {},
|
builder: HttpRequestBuilder.() -> Unit = {},
|
||||||
) = ktorClient.prepareGet(url) {
|
) = ktorClient.prepareGet(url) {
|
||||||
bearerAuth(GithubTokenParts.joinToString("_"))
|
|
||||||
apply(builder)
|
apply(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val GithubTokenParts = arrayOf(
|
data class EhRelease(
|
||||||
"github",
|
|
||||||
"pat",
|
|
||||||
"11A4H2ACI0iGDuL1O6wPYW",
|
|
||||||
"OTFg8xaCNUwR1NHaJE1AT3LoYPfz6bouI7E7ReLf8GjIRFHCL5UsHL9EnWP",
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Release(
|
|
||||||
val version: String,
|
val version: String,
|
||||||
val changelog: String,
|
val changelog: String,
|
||||||
val downloadLink: String,
|
val downloadLink: String,
|
||||||
|
|||||||
@@ -39,18 +39,14 @@ object AppConfig {
|
|||||||
private const val CRASH = "crash"
|
private const val CRASH = "crash"
|
||||||
private const val TAG_TRANSLATIONS = "tag-translations"
|
private const val TAG_TRANSLATIONS = "tag-translations"
|
||||||
|
|
||||||
private val abi = Build.SUPPORTED_ABIS[0].takeIf {
|
private val abi = Build.SUPPORTED_ABIS[0].takeIf { it in ["arm64-v8a", "x86_64", "armeabi-v7a"] } ?: "universal"
|
||||||
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a")
|
|
||||||
} ?: "universal"
|
|
||||||
|
|
||||||
val isBenchmark = "nonMinified" in BuildConfig.BUILD_TYPE || "benchmark" in BuildConfig.BUILD_TYPE
|
val isBenchmark = "nonMinified" in BuildConfig.BUILD_TYPE || "benchmark" in BuildConfig.BUILD_TYPE
|
||||||
|
|
||||||
fun matchVariant(name: String) = name.contains(BuildConfig.FLAVOR) && name.contains(abi)
|
fun matchVariant(name: String) = abi in name
|
||||||
|
|
||||||
val commitTime = ParserUtils.formatDate(BuildConfig.COMMIT_TIME * 1000)
|
val commitTime = ParserUtils.formatDate(BuildConfig.COMMIT_TIME * 1000)
|
||||||
|
|
||||||
val isSnapshot = "SNAPSHOT" in BuildConfig.VERSION_NAME
|
|
||||||
|
|
||||||
private val externalAppDir: File?
|
private val externalAppDir: File?
|
||||||
get() {
|
get() {
|
||||||
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
|
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import android.content.ClipDescription
|
|||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.PersistableBundle
|
||||||
import android.view.textclassifier.TextClassifier
|
import android.view.textclassifier.TextClassifier
|
||||||
import androidx.core.os.persistableBundleOf
|
|
||||||
import com.ehviewer.core.i18n.R
|
import com.ehviewer.core.i18n.R
|
||||||
import com.hippo.ehviewer.ui.MainActivity
|
import com.hippo.ehviewer.ui.MainActivity
|
||||||
import moe.tarsin.tip
|
import moe.tarsin.tip
|
||||||
@@ -33,7 +33,9 @@ fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
|
|||||||
clipboardManager.setPrimaryClip(
|
clipboardManager.setPrimaryClip(
|
||||||
ClipData.newPlainText(null, text).apply {
|
ClipData.newPlainText(null, text).apply {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isSensitive) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isSensitive) {
|
||||||
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
|
description.extras = PersistableBundle().apply {
|
||||||
|
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
package com.hippo.ehviewer.util
|
package com.hippo.ehviewer.util
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalLocale
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.ehviewer.core.i18n.R
|
import com.ehviewer.core.i18n.R
|
||||||
import com.ehviewer.core.util.toLocalDateTime
|
import com.ehviewer.core.util.toLocalDateTime
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
@@ -127,7 +127,7 @@ object ReadableTime {
|
|||||||
val timeDate = time.toLocalDateTime(timeZone).date
|
val timeDate = time.toLocalDateTime(timeZone).date
|
||||||
val nowYear = nowDate.year
|
val nowYear = nowDate.year
|
||||||
val timeYear = timeDate.year
|
val timeYear = timeDate.year
|
||||||
val isZh = Locale.getDefault().language == "zh"
|
val isZh = LocalLocale.current.language == "zh"
|
||||||
if (nowYear == timeYear) {
|
if (nowYear == timeYear) {
|
||||||
if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR
|
if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenu
|
||||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.FlexibleBottomAppBar
|
import androidx.compose.material3.FlexibleBottomAppBar
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -19,6 +18,9 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.ehviewer.core.ui.component.IntSliderState
|
||||||
|
import com.ehviewer.core.ui.component.RollingNumber
|
||||||
|
import com.ehviewer.core.ui.component.RollingNumberPlaceholder
|
||||||
import com.ehviewer.core.ui.component.Slider
|
import com.ehviewer.core.ui.component.Slider
|
||||||
import com.ehviewer.core.ui.component.defaultMaxTickCount
|
import com.ehviewer.core.ui.component.defaultMaxTickCount
|
||||||
import com.ehviewer.core.ui.util.HapticFeedbackType
|
import com.ehviewer.core.ui.util.HapticFeedbackType
|
||||||
@@ -29,7 +31,7 @@ import com.ehviewer.core.ui.util.rememberHapticFeedback
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChapterNavigator(
|
fun ChapterNavigator(
|
||||||
isRtl: Boolean,
|
isRtl: Boolean,
|
||||||
currentPage: Int,
|
slider: IntSliderState,
|
||||||
totalPages: Int,
|
totalPages: Int,
|
||||||
onSliderValueChange: (Int) -> Unit,
|
onSliderValueChange: (Int) -> Unit,
|
||||||
containerColor: Color,
|
containerColor: Color,
|
||||||
@@ -40,14 +42,13 @@ fun ChapterNavigator(
|
|||||||
modifier = Modifier.padding(horizontal = horizontalPadding).clip(CircleShape).background(containerColor).padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = horizontalPadding).clip(CircleShape).background(containerColor).padding(horizontal = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(text = "$currentPage")
|
RollingNumber(slider.value, length = "$totalPages".length)
|
||||||
val steps = totalPages - 2
|
|
||||||
val maxTickCount = defaultMaxTickCount()
|
val maxTickCount = defaultMaxTickCount()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
if (steps < maxTickCount) {
|
if (slider.state.steps < maxTickCount) {
|
||||||
val sliderDragged by interactionSource.collectIsDraggedAsState()
|
val sliderDragged by interactionSource.collectIsDraggedAsState()
|
||||||
val hapticFeedback = rememberHapticFeedback()
|
val hapticFeedback = rememberHapticFeedback()
|
||||||
LaunchedEffect(currentPage) {
|
LaunchedEffect(slider.value) {
|
||||||
if (sliderDragged) {
|
if (sliderDragged) {
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
|
||||||
}
|
}
|
||||||
@@ -55,13 +56,11 @@ fun ChapterNavigator(
|
|||||||
}
|
}
|
||||||
Slider(
|
Slider(
|
||||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
|
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
|
||||||
value = currentPage,
|
state = slider,
|
||||||
valueRange = 1..totalPages,
|
|
||||||
steps = steps,
|
|
||||||
onValueChange = onSliderValueChange,
|
onValueChange = onSliderValueChange,
|
||||||
maxTickCount = maxTickCount,
|
maxTickCount = maxTickCount,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
Text(text = "$totalPages")
|
RollingNumberPlaceholder(totalPages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.ehviewer.core.ui.component.IntSliderState
|
||||||
import com.hippo.ehviewer.ui.main.NavigationIcon
|
import com.hippo.ehviewer.ui.main.NavigationIcon
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ fun BoxScope.ReaderAppBars(
|
|||||||
title: String,
|
title: String,
|
||||||
isRtl: Boolean,
|
isRtl: Boolean,
|
||||||
showSeekBar: Boolean,
|
showSeekBar: Boolean,
|
||||||
currentPage: Int,
|
slider: IntSliderState,
|
||||||
totalPages: Int,
|
totalPages: Int,
|
||||||
onSliderValueChange: (Int) -> Unit,
|
onSliderValueChange: (Int) -> Unit,
|
||||||
onClickSettings: () -> Unit,
|
onClickSettings: () -> Unit,
|
||||||
@@ -65,7 +66,7 @@ fun BoxScope.ReaderAppBars(
|
|||||||
if (showSeekBar && totalPages > 1) {
|
if (showSeekBar && totalPages > 1) {
|
||||||
ChapterNavigator(
|
ChapterNavigator(
|
||||||
isRtl = isRtl,
|
isRtl = isRtl,
|
||||||
currentPage = currentPage,
|
slider = slider,
|
||||||
totalPages = totalPages,
|
totalPages = totalPages,
|
||||||
onSliderValueChange = onSliderValueChange,
|
onSliderValueChange = onSliderValueChange,
|
||||||
containerColor = backgroundColor,
|
containerColor = backgroundColor,
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
|
||||||
import androidx.compose.animation.core.LinearEasing
|
|
||||||
import androidx.compose.animation.core.RepeatMode
|
|
||||||
import androidx.compose.animation.core.animateFloat
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.material3.CircularWavyProgressIndicator
|
|
||||||
import androidx.compose.material3.WavyProgressIndicatorDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CombinedCircularProgressIndicator(progress: Float) {
|
|
||||||
val animatedProgress by animateFloatAsState(
|
|
||||||
targetValue = progress,
|
|
||||||
animationSpec = WavyProgressIndicatorDefaults.ProgressAnimationSpec,
|
|
||||||
label = "progress",
|
|
||||||
)
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = progress == 0f,
|
|
||||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
|
||||||
label = "progressState",
|
|
||||||
) { indeterminate ->
|
|
||||||
if (indeterminate) {
|
|
||||||
// Indeterminate
|
|
||||||
CircularWavyProgressIndicator()
|
|
||||||
} else {
|
|
||||||
// Determinate
|
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "infiniteRotation")
|
|
||||||
val rotation by infiniteTransition.animateFloat(
|
|
||||||
initialValue = 0f,
|
|
||||||
targetValue = 360f,
|
|
||||||
animationSpec = infiniteRepeatable(
|
|
||||||
animation = tween(2000, easing = LinearEasing),
|
|
||||||
repeatMode = RepeatMode.Restart,
|
|
||||||
),
|
|
||||||
label = "rotation",
|
|
||||||
)
|
|
||||||
CircularWavyProgressIndicator(
|
|
||||||
progress = { animatedProgress },
|
|
||||||
modifier = Modifier.rotate(rotation),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|||||||
* +---+---+---+
|
* +---+---+---+
|
||||||
*/
|
*/
|
||||||
object EdgeNavigation : ViewerNavigation() {
|
object EdgeNavigation : ViewerNavigation() {
|
||||||
override val originalRegions = listOf(
|
override val originalRegions = [
|
||||||
Region(
|
Region(
|
||||||
rect = Rect(0f, 0f, 0.33f, 1f),
|
rect = Rect(0f, 0f, 0.33f, 1f),
|
||||||
type = NavigationRegion.NEXT,
|
type = NavigationRegion.NEXT,
|
||||||
@@ -27,5 +27,5 @@ object EdgeNavigation : ViewerNavigation() {
|
|||||||
rect = Rect(0.66f, 0f, 1f, 1f),
|
rect = Rect(0.66f, 0f, 1f, 1f),
|
||||||
type = NavigationRegion.NEXT,
|
type = NavigationRegion.NEXT,
|
||||||
),
|
),
|
||||||
)
|
]
|
||||||
}
|
}
|
||||||
+2
-2
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|||||||
* +---+---+---+
|
* +---+---+---+
|
||||||
*/
|
*/
|
||||||
object KindlishNavigation : ViewerNavigation() {
|
object KindlishNavigation : ViewerNavigation() {
|
||||||
override val originalRegions = listOf(
|
override val originalRegions = [
|
||||||
Region(
|
Region(
|
||||||
rect = Rect(0.33f, 0.33f, 1f, 1f),
|
rect = Rect(0.33f, 0.33f, 1f, 1f),
|
||||||
type = NavigationRegion.NEXT,
|
type = NavigationRegion.NEXT,
|
||||||
@@ -23,5 +23,5 @@ object KindlishNavigation : ViewerNavigation() {
|
|||||||
rect = Rect(0f, 0.33f, 0.33f, 1f),
|
rect = Rect(0f, 0.33f, 0.33f, 1f),
|
||||||
type = NavigationRegion.PREV,
|
type = NavigationRegion.PREV,
|
||||||
),
|
),
|
||||||
)
|
]
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|||||||
* +---+---+---+
|
* +---+---+---+
|
||||||
*/
|
*/
|
||||||
object LNavigation : ViewerNavigation() {
|
object LNavigation : ViewerNavigation() {
|
||||||
override val originalRegions = listOf(
|
override val originalRegions = [
|
||||||
Region(
|
Region(
|
||||||
rect = Rect(0f, 0.33f, 0.33f, 0.66f),
|
rect = Rect(0f, 0.33f, 0.33f, 0.66f),
|
||||||
type = NavigationRegion.PREV,
|
type = NavigationRegion.PREV,
|
||||||
@@ -31,5 +31,5 @@ object LNavigation : ViewerNavigation() {
|
|||||||
rect = Rect(0f, 0.66f, 1f, 1f),
|
rect = Rect(0f, 0.66f, 1f, 1f),
|
||||||
type = NavigationRegion.NEXT,
|
type = NavigationRegion.NEXT,
|
||||||
),
|
),
|
||||||
)
|
]
|
||||||
}
|
}
|
||||||
+2
-2
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|||||||
* +---+---+---+
|
* +---+---+---+
|
||||||
*/
|
*/
|
||||||
object RightAndLeftNavigation : ViewerNavigation() {
|
object RightAndLeftNavigation : ViewerNavigation() {
|
||||||
override val originalRegions = listOf(
|
override val originalRegions = [
|
||||||
Region(
|
Region(
|
||||||
rect = Rect(0f, 0f, 0.33f, 1f),
|
rect = Rect(0f, 0f, 0.33f, 1f),
|
||||||
type = NavigationRegion.LEFT,
|
type = NavigationRegion.LEFT,
|
||||||
@@ -23,5 +23,5 @@ object RightAndLeftNavigation : ViewerNavigation() {
|
|||||||
rect = Rect(0.66f, 0f, 1f, 1f),
|
rect = Rect(0.66f, 0f, 1f, 1f),
|
||||||
type = NavigationRegion.RIGHT,
|
type = NavigationRegion.RIGHT,
|
||||||
),
|
),
|
||||||
)
|
]
|
||||||
}
|
}
|
||||||
@@ -21,39 +21,21 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableIntState
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.ehviewer.core.ui.component.IntSliderState
|
||||||
import com.ehviewer.core.ui.component.Slider
|
import com.ehviewer.core.ui.component.Slider
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IntSliderPreference(
|
fun IntSliderPreference(
|
||||||
state: MutableState<Int>,
|
state: IntSliderState,
|
||||||
title: @Composable () -> Unit,
|
title: @Composable () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
valueRange: IntRange = 0..1,
|
|
||||||
valueSteps: Int = 0,
|
|
||||||
sliderState: MutableIntState = remember { mutableIntStateOf(state.value) },
|
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
icon: @Composable (() -> Unit)? = null,
|
icon: @Composable (() -> Unit)? = null,
|
||||||
summary: @Composable (() -> Unit)? = null,
|
summary: @Composable (() -> Unit)? = null,
|
||||||
valueText: @Composable (() -> Unit)? = null,
|
valueText: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var value by state
|
|
||||||
var sliderValue by sliderState
|
|
||||||
var lastValue by remember { mutableIntStateOf(value) }
|
|
||||||
SideEffect {
|
|
||||||
if (value != lastValue) {
|
|
||||||
sliderValue = value
|
|
||||||
lastValue = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Preference(
|
Preference(
|
||||||
title = title,
|
title = title,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -64,13 +46,10 @@ fun IntSliderPreference(
|
|||||||
summary?.invoke()
|
summary?.invoke()
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Slider(
|
Slider(
|
||||||
value = sliderValue,
|
state = state,
|
||||||
onValueChange = { sliderValue = it },
|
onValueChange = { state.value = it },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
valueRange = valueRange,
|
|
||||||
steps = valueSteps,
|
|
||||||
onValueChangeFinished = { value = sliderValue },
|
|
||||||
)
|
)
|
||||||
if (valueText != null) {
|
if (valueText != null) {
|
||||||
val theme = LocalPreferenceTheme.current
|
val theme = LocalPreferenceTheme.current
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class AndroidAsyncDns(
|
|||||||
private val network: Network? = null,
|
private val network: Network? = null,
|
||||||
) : AsyncDns {
|
) : AsyncDns {
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
internal val resolver = DnsResolver.getInstance()
|
internal val resolver = DnsResolver.getInstance()
|
||||||
private val executor = Executors.newSingleThreadExecutor()
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package tachiyomi.data.release
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CommitComparison(val commits: List<Commit>)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Commit(val commit: CommitDetail)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CommitDetail(val author: CommitAuthor, val message: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CommitAuthor(val name: String)
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package tachiyomi.data.release
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubCommitComparison(val commits: List<GithubCommit>)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubCommit(val commit: GithubCommitDetail)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubCommitDetail(val author: GithubCommitAuthor, val message: String)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubCommitAuthor(val name: String)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package tachiyomi.data.release
|
|
||||||
|
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubRepo(
|
|
||||||
@SerialName("default_branch") val defaultBranch: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubWorkflowRuns(
|
|
||||||
@SerialName("workflow_runs") val workflowRuns: List<GithubWorkflowRun>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubWorkflowRun(
|
|
||||||
@SerialName("head_sha") val headSha: String,
|
|
||||||
@SerialName("display_title") val title: String,
|
|
||||||
@SerialName("artifacts_url") val artifactsUrl: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubArtifacts(
|
|
||||||
@SerialName("artifacts") val artifacts: List<GithubArtifact>,
|
|
||||||
) {
|
|
||||||
fun getDownloadLink(): String {
|
|
||||||
// The default order is upload order, so we need to sort it
|
|
||||||
return artifacts.sortedBy { it.name }.run {
|
|
||||||
find { AppConfig.matchVariant(it.name) } ?: this[0]
|
|
||||||
}.downloadLink
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GithubArtifact(
|
|
||||||
val name: String,
|
|
||||||
@SerialName("archive_download_url") val downloadLink: String,
|
|
||||||
)
|
|
||||||
+8
-9
@@ -4,15 +4,12 @@ import com.hippo.ehviewer.util.AppConfig
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains information about the latest release from GitHub.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GithubRelease(
|
data class Release(
|
||||||
@SerialName("tag_name") val version: String,
|
@SerialName("tag_name") val version: String,
|
||||||
@SerialName("body") val info: String,
|
@SerialName("body") val info: String,
|
||||||
@SerialName("html_url") val releaseLink: String,
|
@SerialName("html_url") val releaseLink: String,
|
||||||
@SerialName("assets") val assets: List<GitHubAssets>,
|
@SerialName("assets") val assets: List<Assets>,
|
||||||
) {
|
) {
|
||||||
fun getDownloadLink(): String {
|
fun getDownloadLink(): String {
|
||||||
val asset = assets.find { AppConfig.matchVariant(it.name) } ?: assets[0]
|
val asset = assets.find { AppConfig.matchVariant(it.name) } ?: assets[0]
|
||||||
@@ -20,8 +17,10 @@ data class GithubRelease(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Assets class containing download url.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GitHubAssets(val url: String, val name: String)
|
data class Assets(
|
||||||
|
@SerialName("browser_download_url")
|
||||||
|
val url: String,
|
||||||
|
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package tachiyomi.data.release
|
||||||
|
|
||||||
|
import com.ehviewer.core.util.unreachable
|
||||||
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Repository(@SerialName("default_branch") val defaultBranch: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WorkflowRuns(@SerialName("workflow_runs") val workflowRuns: List<WorkflowRun>)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WorkflowRun(
|
||||||
|
@SerialName("commit_sha") val headSha: String,
|
||||||
|
@SerialName("title") val title: String,
|
||||||
|
@SerialName("id") val id: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
typealias Artifacts = List<Artifact>
|
||||||
|
|
||||||
|
val Artifacts.downloadLink
|
||||||
|
get() = (find { AppConfig.matchVariant(it.name) } ?: unreachable()).downloadLink
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Artifact(
|
||||||
|
val name: String,
|
||||||
|
@SerialName("archive_download_url")
|
||||||
|
val downloadLink: String,
|
||||||
|
)
|
||||||
Generated
+653
-748
File diff suppressed because it is too large.
Load diff
@@ -16,7 +16,7 @@ regex-lite = "0.1"
|
|||||||
jni = { version = "0.21", optional = true }
|
jni = { version = "0.21", optional = true }
|
||||||
jni_fn = { version = "0.1", optional = true }
|
jni_fn = { version = "0.1", optional = true }
|
||||||
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
quick-xml = { default-features = false, version = "0.38" }
|
quick-xml = { default-features = false, version = "0.42" }
|
||||||
tl = { git = "https://github.com/FooIbar/tl.git", features = ["simd"] }
|
tl = { git = "https://github.com/FooIbar/tl.git", features = ["simd"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
android_logger = { default-features = false, version = "0.15" }
|
android_logger = { default-features = false, version = "0.15" }
|
||||||
@@ -25,12 +25,12 @@ serde_json = "1.0"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
image = { default-features = false, version = "0.25" }
|
image = { default-features = false, version = "0.25" }
|
||||||
rxing = { default-features = false, version = "0.8" }
|
rxing = { version = "0.9", default-features = false, features = ["decoders", "qrcode", "no_character_set_support"] }
|
||||||
ndk = { version = "0.9", optional = true }
|
ndk = { version = "0.9", optional = true }
|
||||||
libwebp-sys2 = { git = "https://github.com/FooIbar/libwebp-sys2-rs.git", features = ["1_5", "demux", "extern-types"] }
|
libwebp-sys2 = { git = "https://github.com/FooIbar/libwebp-sys2-rs.git", features = ["1_5", "demux", "extern-types"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
reqwest = "0.12"
|
reqwest = "0.13"
|
||||||
tokio = { version = "1", features = ["macros"] }
|
tokio = { version = "1", features = ["macros"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ where
|
|||||||
R: Serialize,
|
R: Serialize,
|
||||||
{
|
{
|
||||||
jni_throwing(env, |env| {
|
jni_throwing(env, |env| {
|
||||||
|
ensure!(limit > 0, "Empty response");
|
||||||
let buffer = deref_mut_direct_bytebuffer(env, str)?;
|
let buffer = deref_mut_direct_bytebuffer(env, str)?;
|
||||||
|
|
||||||
// SAFETY: ktor client ensure html content is valid utf-8.
|
// SAFETY: ktor client ensure html content is valid utf-8.
|
||||||
|
|||||||
@@ -9,20 +9,20 @@ use std::f32::consts::FRAC_1_SQRT_2;
|
|||||||
|
|
||||||
fn image_buffer_to_luma8<P: CustomPixel>(
|
fn image_buffer_to_luma8<P: CustomPixel>(
|
||||||
src: &ImageBuffer<P, &[P::Subpixel]>,
|
src: &ImageBuffer<P, &[P::Subpixel]>,
|
||||||
) -> Luma8LuminanceSource {
|
) -> Result<Luma8LuminanceSource> {
|
||||||
let (w, h) = src.dimensions();
|
let (w, h) = src.dimensions();
|
||||||
let mut dst = GrayImage::new(w, h);
|
let mut dst = GrayImage::new(w, h);
|
||||||
for (to, from) in dst.pixels_mut().zip(src.pixels()) {
|
for (to, from) in dst.pixels_mut().zip(src.pixels()) {
|
||||||
*to = from.to_luma8();
|
*to = from.to_luma8();
|
||||||
}
|
}
|
||||||
Luma8LuminanceSource::new(dst.into_raw(), w, h)
|
Ok(Luma8LuminanceSource::new(dst.into_raw(), w, h)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct QrCode;
|
pub struct QrCode;
|
||||||
|
|
||||||
impl ImageConsumer<bool> for QrCode {
|
impl ImageConsumer<bool> for QrCode {
|
||||||
fn apply<P: CustomPixel>(self, image: &ImageBuffer<P, &[P::Subpixel]>) -> Result<bool> {
|
fn apply<P: CustomPixel>(self, image: &ImageBuffer<P, &[P::Subpixel]>) -> Result<bool> {
|
||||||
let source = image_buffer_to_luma8(image);
|
let source = image_buffer_to_luma8(image)?;
|
||||||
let image = BinaryBitmap::new(HybridBinarizer::new(source));
|
let image = BinaryBitmap::new(HybridBinarizer::new(source));
|
||||||
match FinderPatternFinder::new(image.get_black_matrix()).find(&DecodeHints::default()) {
|
match FinderPatternFinder::new(image.get_black_matrix()).find(&DecodeHints::default()) {
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#![feature(f16)]
|
#![feature(f16)]
|
||||||
#![feature(never_type)]
|
|
||||||
|
|
||||||
mod ffi;
|
mod ffi;
|
||||||
pub mod img;
|
pub mod img;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
use crate::parser::utils::unescape;
|
||||||
use crate::regex;
|
use crate::regex;
|
||||||
use quick_xml::escape::unescape;
|
|
||||||
|
|
||||||
pub fn parse_fav_cat(html: &str) -> Vec<String> {
|
pub fn parse_fav_cat(html: &str) -> Vec<String> {
|
||||||
regex!("<input type=\"text\" name=\"favorite_\\d\" value=\"([^\"]+)\"")
|
regex!("<input type=\"text\" name=\"favorite_\\d\" value=\"([^\"]+)\"")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::parser::list::{BaseGalleryInfo, get_thumb_key, parse_token_and_gid, to_category_i32};
|
use crate::parser::list::{BaseGalleryInfo, get_thumb_key, parse_token_and_gid, to_category_i32};
|
||||||
|
use crate::parser::utils::unescape;
|
||||||
use crate::{EhError, get_first_child, get_first_element_by_class_name, get_node_handle_attr};
|
use crate::{EhError, get_first_child, get_first_element_by_class_name, get_node_handle_attr};
|
||||||
use crate::{get_tag_attr, get_vdom_first_element_by_class_name, regex};
|
use crate::{get_tag_attr, get_vdom_first_element_by_class_name, regex};
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use chrono::naive::NaiveDateTime;
|
use chrono::naive::NaiveDateTime;
|
||||||
use quick_xml::escape::unescape;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tl::{Bytes, Node, NodeHandle, Parser, VDom};
|
use tl::{Bytes, Node, NodeHandle, Parser, VDom};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::parser::list::{GalleryListResult, parse_info_list};
|
use crate::parser::list::{GalleryListResult, parse_info_list};
|
||||||
|
use crate::parser::utils::unescape;
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use quick_xml::escape::unescape;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tl::Parser;
|
use tl::Parser;
|
||||||
use tl::VDom;
|
use tl::VDom;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::EhError;
|
use crate::EhError;
|
||||||
|
use crate::parser::utils::unescape;
|
||||||
use crate::{EHGT_PREFIX, EX_PREFIX};
|
use crate::{EHGT_PREFIX, EX_PREFIX};
|
||||||
use crate::{get_element_by_id, get_vdom_first_element_by_class_name};
|
use crate::{get_element_by_id, get_vdom_first_element_by_class_name};
|
||||||
use crate::{get_first_element_by_class_name, query_childs_first_match_attr};
|
use crate::{get_first_element_by_class_name, query_childs_first_match_attr};
|
||||||
use crate::{get_node_attr, get_node_handle_attr, regex};
|
use crate::{get_node_attr, get_node_handle_attr, regex};
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use quick_xml::escape::unescape;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::ops::Index;
|
use std::ops::Index;
|
||||||
@@ -128,13 +128,13 @@ fn parse_gallery_info(node: &Node, parser: &Parser) -> Option<BaseGalleryInfo> {
|
|||||||
.collect();
|
.collect();
|
||||||
let (thumb, (thumb_height, thumb_width)) =
|
let (thumb, (thumb_height, thumb_width)) =
|
||||||
match tag.query_selector(parser, "[data-src]")?.next() {
|
match tag.query_selector(parser, "[data-src]")?.next() {
|
||||||
None => match tag.query_selector(parser, "[src]")?.next() {
|
None => {
|
||||||
None => return None,
|
let thumb = tag.query_selector(parser, "[src]")?.next()?;
|
||||||
Some(thumb) => (
|
(
|
||||||
get_node_handle_attr(&thumb, parser, "src")?,
|
get_node_handle_attr(&thumb, parser, "src")?,
|
||||||
parse_thumb_resolution(get_node_handle_attr(&thumb, parser, "style")?),
|
parse_thumb_resolution(get_node_handle_attr(&thumb, parser, "style")?),
|
||||||
),
|
)
|
||||||
},
|
}
|
||||||
Some(thumb) => (
|
Some(thumb) => (
|
||||||
get_node_handle_attr(&thumb, parser, "data-src")?,
|
get_node_handle_attr(&thumb, parser, "data-src")?,
|
||||||
parse_thumb_resolution(get_node_handle_attr(&thumb, parser, "style")?),
|
parse_thumb_resolution(get_node_handle_attr(&thumb, parser, "style")?),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use crate::parser::utils::unescape;
|
||||||
use crate::{EhError, get_node_handle_attr, get_tag_attr, select_first};
|
use crate::{EhError, get_node_handle_attr, get_tag_attr, select_first};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use quick_xml::escape::unescape;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tl::{Parser, VDom};
|
use tl::{Parser, VDom};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use crate::parser::utils::unescape;
|
||||||
use crate::regex;
|
use crate::regex;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use quick_xml::escape::unescape;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tl::{Parser, VDom};
|
use tl::{Parser, VDom};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
use quick_xml::escape::{EscapeError, resolve_xml_entity, unescape_with};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub fn parse_int_lenient(s: &str, default: i32) -> i32 {
|
pub fn parse_int_lenient(s: &str, default: i32) -> i32 {
|
||||||
s.replace(",", "").parse().unwrap_or(default)
|
s.replace(",", "").parse().unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unescape(raw: &str) -> Result<Cow<'_, str>, EscapeError> {
|
||||||
|
unescape_with(raw, resolve_predefined_entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
const fn resolve_predefined_entity(entity: &str) -> Option<&'static str> {
|
||||||
|
match entity.as_bytes() {
|
||||||
|
b"nbsp" => Some(" "),
|
||||||
|
b"times" => Some("×"),
|
||||||
|
_ => resolve_xml_entity(entity),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.test)
|
alias(libs.plugins.android.test)
|
||||||
alias(libs.plugins.kotlin.android)
|
|
||||||
alias(libs.plugins.baselineprofile)
|
alias(libs.plugins.baselineprofile)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +29,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(21)
|
val javaVersion = libs.versions.java.get().toInt()
|
||||||
|
jvmToolchain(javaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
baselineProfile {
|
baselineProfile {
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ gradlePlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(21)
|
val javaVersion = libs.versions.java.get().toInt()
|
||||||
|
jvmToolchain(javaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
|
|||||||
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user