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 | ||
|
|
0195f1c9b8 | ||
|
|
18d2d992a4 | ||
|
|
5582353c44 | ||
|
|
464568f29f | ||
|
|
213a9d56d2 | ||
|
|
f672462bcd | ||
|
|
9a0b0d2c65 | ||
|
|
fcaf4e3182 | ||
|
|
ea40fa9b05 | ||
|
|
2e3436dbe7 | ||
|
|
bd759dedea | ||
|
|
bac78ce018 | ||
|
|
dd95d7aaa2 | ||
|
|
d1bb5b1d87 | ||
|
|
67a3252b05 | ||
|
|
2df08f022f | ||
|
|
65673a0ed9 | ||
|
|
aef20e2e04 | ||
|
|
3bbfc8ac3a | ||
|
|
02e85a8c4d | ||
|
|
8cc531ccec | ||
|
|
30fc750e17 | ||
|
|
0ff40d3d38 | ||
|
|
9fa80db783 | ||
|
|
357503b403 | ||
|
|
10f7b2a7a9 | ||
|
|
99696b11db | ||
|
|
71291b8800 | ||
|
|
3532dbdcd6 | ||
|
|
589ba8432c | ||
|
|
273196bf5d | ||
|
|
a5fadeee3c | ||
|
|
3f784ec1de | ||
|
|
f7a70cdfc3 | ||
|
|
030100280f | ||
|
|
0d76ee43fd | ||
|
|
11a215f070 | ||
|
|
a6e61e07fc | ||
|
|
da94d298ce | ||
|
|
9e74db0f02 | ||
|
|
0d63c2fa3c | ||
|
|
bf44fde58e | ||
|
|
4bc119138f | ||
|
|
c7f5b86843 | ||
|
|
2aa815dce1 | ||
|
|
cf7f18351d | ||
|
|
5ed27963d7 | ||
|
|
85e539f0ba | ||
|
|
15ecb0e3f4 | ||
|
|
db5055da6e | ||
|
|
09e201f3f2 | ||
|
|
6f34083880 | ||
|
|
4e0c42c955 | ||
|
|
935b7dec0c | ||
|
|
d0b0651c7d | ||
|
|
e131a416e8 | ||
|
|
dd526d33bd | ||
|
|
4611a77e04 | ||
|
|
806560cf59 | ||
|
|
fd3f6eb124 | ||
|
|
8e14c6e57b | ||
|
|
20a4f4ab25 | ||
|
|
d4c64f6e66 | ||
|
|
cfb2f15a1e | ||
|
|
927a738f46 | ||
|
|
19482dbd77 | ||
|
|
43b7fb611a | ||
|
|
c39338efb7 | ||
|
|
dbd952eebb | ||
|
|
192039408c | ||
|
|
e38376d4b0 | ||
|
|
a4062cf4d1 | ||
|
|
3c47d36525 | ||
|
|
00090944d5 | ||
|
|
d2cafbb2a3 | ||
|
|
9847b95582 | ||
|
|
986412932b | ||
|
|
f13831cb74 | ||
|
|
5ed1203558 | ||
|
|
acf1fe43bf | ||
|
|
4761ef7416 | ||
|
|
70aaf38f94 | ||
|
|
a92aaf19c9 | ||
|
|
a5b408d316 | ||
|
|
f96a2e63d1 | ||
|
|
a0a9da68da | ||
|
|
a83be94d1f | ||
|
|
cc5dfbfa41 | ||
|
|
57b79b0b06 | ||
|
|
569130087f | ||
|
|
1a7b36b488 | ||
|
|
e8b5fd0425 | ||
|
|
626d5e7a8c | ||
|
|
4d94e84c13 | ||
|
|
863ec659ff | ||
|
|
071fdfc0a3 | ||
|
|
a137e9715a | ||
|
|
c28d1d34ff | ||
|
|
1230022d17 | ||
|
|
9a9f2a8d70 |
No files matched your search
@@ -1,5 +1,7 @@
|
||||
[*.{kt,kts}]
|
||||
ij_kotlin_imports_layout = *
|
||||
ij_kotlin_line_break_after_multiline_when_entry = false
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_mixed-condition-operators = disabled
|
||||
File renamed without changes.
File renamed without changes.
@@ -13,42 +13,39 @@ on:
|
||||
|
||||
jobs:
|
||||
baseline-profile:
|
||||
runs-on: ubuntu-latest
|
||||
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@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
java-version-file: .java-version
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup target add armv7-linux-androideabi
|
||||
- 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: Swatinem/rust-cache@v2
|
||||
uses: https://github.com/Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./app/src/main/rust"
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: app/.cxx
|
||||
key: cmake-
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
cache-read-only: true
|
||||
@@ -59,7 +56,7 @@ jobs:
|
||||
|
||||
- name: Upload reports
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: 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 +0,0 @@
|
||||
*.sh eol=lf
|
||||
@@ -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@v5
|
||||
|
||||
- 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@v5
|
||||
|
||||
- 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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
with:
|
||||
name: default-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultRelease
|
||||
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
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@v5
|
||||
|
||||
- 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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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@v4
|
||||
with:
|
||||
name: marshmallow-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowRelease
|
||||
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
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@v5
|
||||
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@v5
|
||||
|
||||
- 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@v4
|
||||
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,5 +1,26 @@
|
||||
# 变更日志
|
||||
|
||||
## [1.14.6] - 2025-12-17
|
||||
|
||||
### 新功能
|
||||
|
||||
* 支持显示画廊阅读进度(于 EH 设置中开启) [#2277](https://github.com/FooIbar/EhViewer/issues/2277)
|
||||
|
||||
### 改进
|
||||
|
||||
* 画廊列表变化时重新显示悬浮操作按钮 [#2681](https://github.com/FooIbar/EhViewer/issues/2681)
|
||||
* 移除文件名中的尾随空格和点以兼容 Windows [#2727](https://github.com/FooIbar/EhViewer/issues/2727)
|
||||
* 长按画廊详情中的标签时显示标签名称 [#2769](https://github.com/FooIbar/EhViewer/issues/2769)
|
||||
* 杂项优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 对话框中的下拉菜单显示错位
|
||||
* HTTP 状态错误显示为“什么都没有找到”
|
||||
* 某些老设备无法通过 Cloudflare 验证 [#2794](https://github.com/FooIbar/EhViewer/issues/2794)
|
||||
* 杂项修复
|
||||
|
||||
|
||||
## [1.14.5] - 2025-10-19
|
||||
|
||||
### 改进
|
||||
|
||||
@@ -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">
|
||||
<img src="https://github.com/Ehviewer-Overhauled/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>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
|
||||
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
|
||||
</a>
|
||||
<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>
|
||||
Standalone fork of
|
||||
<a href="https://codeberg.org/EhViewer/EhViewer">EhViewer (codeberg)</a>
|
||||
maintained at <a href="https://gitea.yrkaierx.com/yenru0/EhViewer">gitea.yrkaierx.com/yenru0/EhViewer</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<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>
|
||||
# Changes over upstream
|
||||
|
||||
# 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/)
|
||||
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/Ehviewer-Overhauled/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/)
|
||||
Synced from [EhViewer/EhViewer on codeberg](https://codeberg.org/EhViewer/EhViewer), the continuation of FooIbar's EhViewer. Feature work happens there; this fork tracks it and carries the network/resilience patches above.
|
||||
|
||||
# License
|
||||
|
||||
|
||||
+12
-13
@@ -49,21 +49,23 @@ android {
|
||||
|
||||
val repoName = providers.exec {
|
||||
commandLine = "git remote get-url origin".split(' ')
|
||||
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
|
||||
.removeSuffix(".git")
|
||||
}.standardOutput.asText.get().trim().removeSuffix(".git").removePrefix("https://codeberg.org/")
|
||||
|
||||
val chromeVersion = rootProject.layout.projectDirectory.file("chrome.version").asFile.readText().trim()
|
||||
val snapshot = !hasProperty("release")
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
versionCode = 180063
|
||||
versionName = "1.14.5"
|
||||
// versionNameSuffix = "-SNAPSHOT"
|
||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
|
||||
applicationId = "yenru0.yrkaierx.ehviewer"
|
||||
versionCode = 180064
|
||||
versionName = if (snapshot) {
|
||||
"1.15.0-SNAPSHOT"
|
||||
} else {
|
||||
"1.15.0"
|
||||
}
|
||||
buildConfigField("boolean", "SNAPSHOT", "$snapshot")
|
||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||
buildConfigField("long", "COMMIT_TIME", commitTime)
|
||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||
buildConfigField("String", "CHROME_VERSION", "\"$chromeVersion\"")
|
||||
ndk {
|
||||
if (isRelease) {
|
||||
abiFilters.addAll(supportedAbis)
|
||||
@@ -179,10 +181,7 @@ dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.biometric)
|
||||
implementation(libs.androidx.browser)
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
||||
api(platform(libs.compose.bom))
|
||||
implementation(libs.bundles.compose)
|
||||
implementation(libs.androidx.webkit)
|
||||
|
||||
implementation(libs.compose.destinations.core)
|
||||
ksp(libs.compose.destinations.compiler)
|
||||
|
||||
Vendored
+4
@@ -15,6 +15,10 @@
|
||||
java.nio.ByteBuffer source;
|
||||
}
|
||||
|
||||
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
|
||||
static boolean isLinkBufferComposerEnabled() return true;
|
||||
}
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
|
||||
@@ -118,10 +118,6 @@
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SecurityActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:label="@string/settings" />
|
||||
<activity
|
||||
android:name=".shortcuts.ShortcutsActivity"
|
||||
android:excludeFromRecents="true"
|
||||
|
||||
@@ -5,6 +5,9 @@ include(FetchContent)
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
string(CONCAT CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -ffast-math -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto "
|
||||
"-ffile-prefix-map=${ANDROID_TOOLCHAIN_ROOT}= "
|
||||
"-ffile-prefix-map=${CMAKE_SOURCE_DIR}= "
|
||||
"-ffile-prefix-map=${CMAKE_BINARY_DIR}= "
|
||||
"-mllvm -polly "
|
||||
"-mllvm -polly-run-dce "
|
||||
"-mllvm -polly-run-inliner "
|
||||
@@ -29,7 +32,7 @@ option(XZ_TOOL_XZDEC OFF)
|
||||
FetchContent_Declare(
|
||||
liblzma
|
||||
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
|
||||
GIT_TAG v5.8.1
|
||||
GIT_TAG v5.8.3
|
||||
GIT_SHALLOW 1
|
||||
)
|
||||
|
||||
@@ -38,23 +41,27 @@ include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
|
||||
|
||||
# Build GNUTLS libnettle
|
||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
|
||||
execute_process(
|
||||
COMMAND wsl wslpath ${CMAKE_CURRENT_LIST_DIR}
|
||||
OUTPUT_VARIABLE CURRENT_DIR
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}")
|
||||
set(TOOLCHAIN "$ANDROID_HOME/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin")
|
||||
set(WSL wsl)
|
||||
else ()
|
||||
set(CURRENT_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||
set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin")
|
||||
endif ()
|
||||
set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}")
|
||||
set(CONFIGURE_COMMAND ${WSL} ./.bootstrap && ${WSL} ./configure --disable-public-key --disable-dependency-tracking
|
||||
"--host=${CMAKE_C_COMPILER_TARGET}"
|
||||
"CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments"
|
||||
"AR=${TOOLCHAIN}/llvm-ar"
|
||||
"CC=${TOOLCHAIN}/clang --target=${CMAKE_C_COMPILER_TARGET}"
|
||||
"RANLIB=${TOOLCHAIN}/llvm-ranlib"
|
||||
)
|
||||
ExternalProject_Add(
|
||||
nettle
|
||||
GIT_REPOSITORY https://gitlab.com/gnutls/nettle.git
|
||||
GIT_TAG nettle_3.10.2_release_20250626
|
||||
GIT_TAG nettle_4.0_release_20260205
|
||||
GIT_SHALLOW 1
|
||||
GIT_CONFIG core.autocrlf=input
|
||||
CONFIGURE_COMMAND bash ${CURRENT_DIR}/configure_nettle.sh ${NDK_VERSION} ${CMAKE_C_COMPILER_TARGET}
|
||||
BUILD_COMMAND bash -c "make -j4 libnettle.a CFLAGS='${CMAKE_C_FLAGS} -Qunused-arguments'"
|
||||
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
|
||||
BUILD_COMMAND ${WSL} make -j4 libnettle.a
|
||||
INSTALL_COMMAND ""
|
||||
BUILD_IN_SOURCE 1
|
||||
BUILD_BYPRODUCTS <SOURCE_DIR>/libnettle.a
|
||||
@@ -96,7 +103,7 @@ set(LIBARCHIVE_PATCH
|
||||
FetchContent_Declare(
|
||||
libarchive
|
||||
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
||||
GIT_TAG v3.8.2
|
||||
GIT_TAG v3.8.7
|
||||
GIT_SHALLOW 1
|
||||
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
|
||||
)
|
||||
@@ -133,8 +140,8 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ../../../../rust-
|
||||
unset(Rust_TOOLCHAIN CACHE)
|
||||
FetchContent_Declare(
|
||||
Corrosion
|
||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
||||
GIT_TAG v0.5.2
|
||||
GIT_REPOSITORY https://github.com/FooIbar/corrosion.git
|
||||
GIT_TAG v0.6.1
|
||||
GIT_SHALLOW 1
|
||||
)
|
||||
|
||||
@@ -145,7 +152,7 @@ corrosion_import_crate(
|
||||
FEATURES $<IF:$<VERSION_GREATER_EQUAL:${ANDROID_NATIVE_API_LEVEL},26>,android-26,android>
|
||||
FLAGS $<$<NOT:$<CONFIG:Debug>>:-Z build-std -Z build-std-features=>
|
||||
)
|
||||
corrosion_add_target_rustflags(ehviewer_rust $<$<NOT:$<CONFIG:Debug>>:-Z unstable-options -C panic=immediate-abort>)
|
||||
corrosion_add_target_rustflags(ehviewer_rust $<$<NOT:$<CONFIG:Debug>>:-Z unstable-options -C panic=immediate-abort -Z location-detail=none -Z fmt-debug=none>)
|
||||
corrosion_link_libraries(ehviewer_rust android jnigraphics log webp webpdemux)
|
||||
|
||||
# Build and link our app's native lib
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
SUFFIX=toolchains/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64/bin
|
||||
CI_NDK_LOCATION=$ANDROID_HOME/ndk/$1/$SUFFIX
|
||||
ARCH_NDK_LOCATION=$ANDROID_HOME/ndk-bundle/$SUFFIX
|
||||
if test -e $CI_NDK_LOCATION ;then
|
||||
TOOLCHAIN=$CI_NDK_LOCATION
|
||||
else
|
||||
TOOLCHAIN=$ARCH_NDK_LOCATION
|
||||
fi
|
||||
TARGET=$2
|
||||
./.bootstrap && ./configure --host=$TARGET --disable-public-key --disable-dependency-tracking \
|
||||
AR=$TOOLCHAIN/llvm-ar \
|
||||
CC="$TOOLCHAIN/clang -target $TARGET" \
|
||||
RANLIB=$TOOLCHAIN/llvm-ranlib
|
||||
@@ -24,7 +24,7 @@ Java_com_hippo_ehviewer_jni_HashKt_sha1(JNIEnv *env, jclass clazz, jint fd) {
|
||||
}
|
||||
|
||||
byte digest[SHA1_DIGEST_SIZE];
|
||||
sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest);
|
||||
sha1_digest(&ctx, digest);
|
||||
|
||||
byte byte;
|
||||
char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
|
||||
|
||||
@@ -180,7 +180,6 @@ fun WebView(
|
||||
is WebContent.Url -> {
|
||||
wv.loadUrl(content.url, content.additionalHttpHeaders)
|
||||
}
|
||||
|
||||
is WebContent.Data -> {
|
||||
wv.loadDataWithBaseURL(
|
||||
content.baseUrl,
|
||||
@@ -190,14 +189,12 @@ fun WebView(
|
||||
content.historyUrl,
|
||||
)
|
||||
}
|
||||
|
||||
is WebContent.Post -> {
|
||||
wv.postUrl(
|
||||
content.url,
|
||||
content.postData,
|
||||
)
|
||||
}
|
||||
|
||||
is WebContent.NavigatorOnly -> {
|
||||
// NO-OP
|
||||
}
|
||||
@@ -502,11 +499,9 @@ class WebViewNavigator(private val coroutineScope: CoroutineScope) {
|
||||
event.encoding,
|
||||
event.historyUrl,
|
||||
)
|
||||
|
||||
is NavigationEvent.LoadUrl -> {
|
||||
loadUrl(event.url, event.additionalHttpHeaders)
|
||||
}
|
||||
|
||||
is NavigationEvent.PostUrl -> {
|
||||
postUrl(event.url, event.postData)
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ import coil3.request.crossfade
|
||||
import coil3.serviceLoaderEnabled
|
||||
import coil3.util.DebugLogger
|
||||
import com.ehviewer.core.database.SearchDatabase
|
||||
import com.ehviewer.core.database.roomDb
|
||||
import com.ehviewer.core.files.deleteContent
|
||||
import com.ehviewer.core.ui.util.initSETConnection
|
||||
import com.ehviewer.core.util.isAtLeastO
|
||||
import com.ehviewer.core.util.isAtLeastP
|
||||
import com.ehviewer.core.util.isAtLeastS
|
||||
import com.ehviewer.core.util.isAtLeastSExtension7
|
||||
import com.ehviewer.core.util.launchIO
|
||||
import com.ehviewer.core.util.logcat
|
||||
import com.ehviewer.core.util.withUIContext
|
||||
@@ -65,8 +65,8 @@ import com.hippo.ehviewer.ktbuilder.imageLoader
|
||||
import com.hippo.ehviewer.ktor.Cronet
|
||||
import com.hippo.ehviewer.ktor.configureClient
|
||||
import com.hippo.ehviewer.ktor.configureCommon
|
||||
import com.hippo.ehviewer.ktor.isCronetAvailable
|
||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||
import com.hippo.ehviewer.ui.lockObserver
|
||||
import com.hippo.ehviewer.ui.screen.detailCache
|
||||
import com.hippo.ehviewer.ui.tools.dataStateFlow
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
@@ -83,7 +83,6 @@ import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import logcat.asLog
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.arch.room.roomDb
|
||||
import splitties.init.appCtx
|
||||
|
||||
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
|
||||
@@ -109,7 +108,6 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycle.addObserver(lockObserver)
|
||||
CrashHandler.install()
|
||||
super.onCreate()
|
||||
System.loadLibrary("ehviewer")
|
||||
@@ -124,6 +122,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
}
|
||||
launchIO {
|
||||
EhTagDatabase.launchUpdate()
|
||||
@Suppress("UNUSED_EXPRESSION")
|
||||
launch { EhDB }
|
||||
launch { dataStateFlow.value }
|
||||
launch { OSUtils.totalMemory }
|
||||
@@ -146,6 +145,8 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.enableDefaults()
|
||||
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.SourceInformation)
|
||||
} else {
|
||||
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.Auto)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +196,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
diskCache { imageCache }
|
||||
diskCache { thumbCache }
|
||||
crossfade(300)
|
||||
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
||||
if (drawable != null) error(drawable.asImage(true))
|
||||
@@ -218,7 +219,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
private set
|
||||
|
||||
val ktorClient by lazy {
|
||||
if (isAtLeastSExtension7 && Settings.enableCronet.value) {
|
||||
if (isCronetAvailable && Settings.enableQuic.value) {
|
||||
HttpClient(Cronet) {
|
||||
engine { configureClient(Settings.enableQuic.value) }
|
||||
configureCommon()
|
||||
@@ -237,10 +238,10 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
}
|
||||
}
|
||||
|
||||
val imageCache by lazy {
|
||||
val thumbCache by lazy {
|
||||
diskCache {
|
||||
directory(appCtx.cacheDir.toOkioPath() / "image_cache")
|
||||
maxSizeBytes(Settings.readCacheSize.value.coerceIn(320, 5120).toLong() * 1024 * 1024)
|
||||
directory(appCtx.cacheDir.toOkioPath() / "thumb")
|
||||
maxSizeBytes(80L * 1024 * 1024)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
*/
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.room.execSQL
|
||||
import androidx.room.useWriterConnection
|
||||
import arrow.fx.coroutines.resource
|
||||
import arrow.fx.coroutines.resourceScope
|
||||
import com.ehviewer.core.data.model.asEntity
|
||||
import com.ehviewer.core.database.EhDatabase
|
||||
import com.ehviewer.core.database.Schema17to18
|
||||
import com.ehviewer.core.database.getDatabasePath
|
||||
import com.ehviewer.core.database.model.DownloadArtist
|
||||
import com.ehviewer.core.database.model.DownloadDirname
|
||||
import com.ehviewer.core.database.model.DownloadInfo
|
||||
@@ -32,13 +33,13 @@ import com.ehviewer.core.database.model.HistoryInfo
|
||||
import com.ehviewer.core.database.model.LocalFavoriteInfo
|
||||
import com.ehviewer.core.database.model.ProgressInfo
|
||||
import com.ehviewer.core.database.model.QuickSearch
|
||||
import com.ehviewer.core.database.roomDb
|
||||
import com.ehviewer.core.files.delete
|
||||
import com.ehviewer.core.files.sendTo
|
||||
import com.ehviewer.core.model.GalleryInfo
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.arch.room.roomDb
|
||||
|
||||
object EhDB {
|
||||
private const val DB_NAME = "eh.db"
|
||||
@@ -277,23 +278,26 @@ object EhDB {
|
||||
db.filterDao().update(filter)
|
||||
}
|
||||
|
||||
fun exportDB(context: Context, file: Path) {
|
||||
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
|
||||
val dbFile = context.getDatabasePath(DB_NAME)
|
||||
dbFile.toOkioPath() sendTo file
|
||||
suspend fun exportDB(file: Path) {
|
||||
db.useWriterConnection { conn ->
|
||||
conn.execSQL("PRAGMA wal_checkpoint(FULL)")
|
||||
conn.execSQL("VACUUM")
|
||||
}
|
||||
val dbFile = getDatabasePath(DB_NAME)
|
||||
dbFile sendTo file
|
||||
}
|
||||
|
||||
suspend fun importDB(context: Context, uri: Uri) = resourceScope {
|
||||
suspend fun importDB(file: Path) = resourceScope {
|
||||
val tempDBName = "tmp.db"
|
||||
val dbFile = getDatabasePath(tempDBName)
|
||||
file sendTo dbFile
|
||||
val oldDB = resource {
|
||||
context.deleteDatabase(tempDBName)
|
||||
roomDb<EhDatabase>(tempDBName) {
|
||||
createFromInputStream { context.contentResolver.openInputStream(uri) }
|
||||
addMigrations(Schema17to18())
|
||||
}
|
||||
} release { db ->
|
||||
db.close()
|
||||
context.deleteDatabase(tempDBName)
|
||||
dbFile.delete()
|
||||
}
|
||||
|
||||
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,13 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import com.ehviewer.core.network.EhCookieStore
|
||||
import com.ehviewer.core.preferences.DataStorePreferences
|
||||
import com.ehviewer.core.preferences.PrefDelegate
|
||||
import com.ehviewer.core.preferences.edit
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||
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.ReadingModeType
|
||||
import java.util.Locale
|
||||
@@ -82,6 +81,7 @@ object Settings : DataStorePreferences(null) {
|
||||
val detailSize = intPref("detail_size_2", 0)
|
||||
val thumbColumns = intPref("thumb_columns", 3)
|
||||
val showGalleryPages = boolPref("show_gallery_pages", true)
|
||||
val showReadingProgress = boolPref("show_reading_progress", false)
|
||||
val showVoteStatus = boolPref("show_vote_status", false)
|
||||
val showComments = boolPref("show_gallery_comments", true)
|
||||
val commentThreshold = intPref("comment_threshold", -100)
|
||||
@@ -111,7 +111,7 @@ object Settings : DataStorePreferences(null) {
|
||||
val saveCrashLog = boolPref("save_crash_log", true)
|
||||
val readCacheSize = intPref("read_cache_size_2", 640)
|
||||
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 preloadThumbAggressively = boolPref("preload_thumb_aggressively", false)
|
||||
val animateItems = boolPref("animate_items", true)
|
||||
@@ -119,8 +119,8 @@ object Settings : DataStorePreferences(null) {
|
||||
|
||||
// About
|
||||
val backupBeforeUpdate = boolPref("backup_before_update", false)
|
||||
val useCIUpdateChannel = boolPref("ci_update_channel", AppConfig.isSnapshot)
|
||||
val updateIntervalDays = intPref("update_interval_days", 7)
|
||||
val useCIUpdateChannel = boolPref("ci_update_channel", BuildConfig.SNAPSHOT)
|
||||
val updateIntervalDays = intPref("update_interval_days", 0)
|
||||
|
||||
// Misc
|
||||
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()
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import arrow.fx.coroutines.parZip
|
||||
import com.ehviewer.core.i18n.R
|
||||
import com.ehviewer.core.model.BaseGalleryInfo
|
||||
import com.ehviewer.core.model.GalleryInfo
|
||||
import com.ehviewer.core.network.EhCookieStore
|
||||
import com.ehviewer.core.util.logcat
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -31,6 +32,7 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.client.data.fillInfo
|
||||
import com.hippo.ehviewer.client.data.filterComments
|
||||
import com.hippo.ehviewer.client.exception.CloudflareBypassException
|
||||
import com.hippo.ehviewer.client.exception.EhError
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
||||
import com.hippo.ehviewer.client.exception.InsufficientGpException
|
||||
@@ -69,6 +71,7 @@ import io.ktor.client.statement.HttpStatement
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.client.statement.request
|
||||
import io.ktor.http.Url
|
||||
import io.ktor.util.moveToByteArray
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import io.ktor.utils.io.pool.useInstance
|
||||
import io.ktor.utils.io.readAvailable
|
||||
@@ -79,6 +82,8 @@ import kotlin.system.measureTimeMillis
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.addJsonArray
|
||||
import kotlinx.serialization.json.put
|
||||
@@ -119,7 +124,7 @@ private fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuff
|
||||
{ !it.hasRemaining() },
|
||||
)
|
||||
if (empty) {
|
||||
val message = if (EhUtils.isExHentai) {
|
||||
val message = if (response.request.url.host == EhUrl.DOMAIN_EX) {
|
||||
"Sad Panda\n(without panda)"
|
||||
} else {
|
||||
appCtx.getString(R.string.error_ip_banned)
|
||||
@@ -127,27 +132,30 @@ private fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuff
|
||||
throw IpBannedException(message)
|
||||
}
|
||||
|
||||
// Check parse error thrown by rust
|
||||
val cause = e.cause
|
||||
if (e is ParseException && cause is RuntimeException) {
|
||||
when (val message = cause.message) {
|
||||
"0" -> throw NoHitsFoundException()
|
||||
"1" -> throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
"2" -> throw NotLoggedInException()
|
||||
"3" -> throw NoHathClientException()
|
||||
"4" -> throw InsufficientFundsException()
|
||||
is String if message.isNotEmpty() -> {
|
||||
when (message[0]) {
|
||||
'5' -> throw IpBannedException(message.substring(1))
|
||||
'6' -> throw EhException(message.substring(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parse error from rust
|
||||
val error = body.getOrNull()?.runCatching {
|
||||
val array = moveToByteArray()
|
||||
rewind()
|
||||
Cbor.decodeFromByteArray<EhError>(array)
|
||||
}?.getOrNull()
|
||||
|
||||
// Check Gallery Not Available - 404
|
||||
if (error is EhError.GalleryUnavailable) throw EhException(error.message)
|
||||
|
||||
// Check bad response code
|
||||
response.status.ensureSuccess()
|
||||
|
||||
when (error) {
|
||||
EhError.NoHits -> throw NoHitsFoundException()
|
||||
EhError.NoWatched -> throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
EhError.NeedLogin -> throw NotLoggedInException()
|
||||
EhError.NoHathClient -> throw NoHathClientException()
|
||||
EhError.InsufficientFunds -> throw InsufficientFundsException()
|
||||
is EhError.IpBanned -> throw IpBannedException(error.message)
|
||||
is EhError.Error -> throw EhException(error.message)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
if (e is ParseException || e is SerializationException) {
|
||||
body.onLeft { if ("<" !in it) throw IpBannedException(it) }
|
||||
if (Settings.saveParseErrorBody.value) body.saveParseError(e)
|
||||
@@ -186,7 +194,12 @@ private suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline
|
||||
|
||||
object EhEngine {
|
||||
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)
|
||||
|
||||
@@ -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 {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
private const val NAMESPACE_PREFIX = "n"
|
||||
private val PREFIXES = arrayOf(
|
||||
"-",
|
||||
"~",
|
||||
"tag:",
|
||||
"weak:",
|
||||
)
|
||||
private val PREFIXES = ["-", "~", "tag:", "weak:"]
|
||||
private lateinit var tagGroups: Map<String, Map<String, String>>
|
||||
private val updateLock = Mutex()
|
||||
var initialized by mutableStateOf(false)
|
||||
|
||||
@@ -107,14 +107,14 @@ object EhUrl {
|
||||
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 (allComment) addQueryParameter("hc", "1")
|
||||
}.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)
|
||||
}.buildString()
|
||||
|
||||
@@ -143,7 +143,7 @@ object 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,
|
||||
parameters = Parameters.build(builder),
|
||||
)
|
||||
|
||||
@@ -20,15 +20,19 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Environment
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.net.toUri
|
||||
import arrow.core.memoize
|
||||
import com.ehviewer.core.model.GalleryDetail
|
||||
import com.ehviewer.core.model.GalleryInfo
|
||||
import com.ehviewer.core.network.EhCookieStore
|
||||
import com.ehviewer.core.util.withUIContext
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.parser.Archive
|
||||
@@ -39,6 +43,7 @@ import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.materialkolor.hct.Hct
|
||||
import com.materialkolor.ktx.from
|
||||
import com.materialkolor.ktx.toColor
|
||||
import com.materialkolor.utils.ColorUtils.lstarFromArgb
|
||||
import splitties.systemservices.downloadManager
|
||||
|
||||
object EhUtils {
|
||||
@@ -151,7 +156,13 @@ object EhUtils {
|
||||
}
|
||||
}
|
||||
|
||||
val categoryTextColor = Color(0xffe6e0e9)
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
fun getCategoryTextColor(color: Color) = if (isSystemInDarkTheme() == lstarFromArgb(color.toArgb()) > 70) {
|
||||
MaterialTheme.colorScheme.surface
|
||||
} else {
|
||||
LocalContentColor.current
|
||||
}
|
||||
|
||||
val favoriteIconColor = Color(0xffff3040)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ data class ListUrlBuilder(
|
||||
private var jumpTo: String? = null,
|
||||
private var range: Int = 0,
|
||||
var category: Int = EhUtils.NONE,
|
||||
private var mKeyword: String? = null,
|
||||
var keyword: String? = null,
|
||||
var hash: String? = null,
|
||||
var language: Int = -1,
|
||||
var advanceSearch: Int = -1,
|
||||
@@ -69,16 +69,10 @@ data class ListUrlBuilder(
|
||||
jumpTo = date
|
||||
}
|
||||
|
||||
var keyword: String?
|
||||
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
|
||||
set(keyword) {
|
||||
mKeyword = keyword
|
||||
}
|
||||
|
||||
constructor(q: QuickSearch) : this(
|
||||
mode = q.mode,
|
||||
category = q.category,
|
||||
mKeyword = q.keyword,
|
||||
keyword = q.keyword,
|
||||
advanceSearch = q.advanceSearch,
|
||||
minRating = q.minRating,
|
||||
pageFrom = q.pageFrom,
|
||||
@@ -90,7 +84,7 @@ data class ListUrlBuilder(
|
||||
name = name,
|
||||
mode = mode,
|
||||
category = category,
|
||||
keyword = mKeyword,
|
||||
keyword = keyword,
|
||||
advanceSearch = advanceSearch,
|
||||
minRating = minRating,
|
||||
pageFrom = pageFrom,
|
||||
@@ -107,7 +101,7 @@ data class ListUrlBuilder(
|
||||
if (q.category != this.category) {
|
||||
return false
|
||||
}
|
||||
if (q.keyword != mKeyword) {
|
||||
if (q.keyword != keyword) {
|
||||
return false
|
||||
}
|
||||
if (q.advanceSearch != advanceSearch) {
|
||||
@@ -129,7 +123,7 @@ data class ListUrlBuilder(
|
||||
jumpTo = params["seek"],
|
||||
range = params["range"]?.toIntOrNull() ?: 0,
|
||||
category = params["f_cats"]?.toIntOrNull()?.let(EhUtils::invCategory) ?: EhUtils.NONE,
|
||||
mKeyword = params["f_search"],
|
||||
keyword = params["f_search"],
|
||||
) {
|
||||
if (params["advsearch"] == "1") {
|
||||
advanceSearch = 0
|
||||
@@ -160,7 +154,7 @@ data class ListUrlBuilder(
|
||||
if (category > 0) {
|
||||
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) {
|
||||
keyword
|
||||
} else {
|
||||
@@ -212,26 +206,23 @@ data class ListUrlBuilder(
|
||||
}
|
||||
}
|
||||
}.buildString()
|
||||
|
||||
MODE_UPLOADER, MODE_TAG -> {
|
||||
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
||||
ehUrl(listOf(path, requireNotNull(mKeyword))) {
|
||||
ehUrl([path, requireNotNull(keyword)]) {
|
||||
addQueryParameterIfNotBlank("prev", prev)
|
||||
addQueryParameterIfNotBlank("next", next)
|
||||
addQueryParameterIfNotBlank("seek", jumpTo)
|
||||
addQueryParameterIfNotBlank("range", range.takeIf { it > 0 }?.toString())
|
||||
}.buildString()
|
||||
}
|
||||
|
||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||
MODE_IMAGE_SEARCH -> ehUrl {
|
||||
addQueryParameter("f_shash", requireNotNull(hash))
|
||||
}.buildString()
|
||||
MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
||||
addQueryParameter("tl", requireNotNull(mKeyword))
|
||||
addQueryParameter("tl", requireNotNull(keyword))
|
||||
addQueryParameterIfNotBlank("p", jumpTo)
|
||||
}.buildString()
|
||||
|
||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.hippo.ehviewer.client.exception
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.cbor.CborArray
|
||||
|
||||
@Serializable
|
||||
sealed interface EhError {
|
||||
@CborArray
|
||||
@Serializable
|
||||
@SerialName("NoHits")
|
||||
data object NoHits : EhError
|
||||
|
||||
@CborArray
|
||||
@Serializable
|
||||
@SerialName("NoWatched")
|
||||
data object NoWatched : EhError
|
||||
|
||||
@CborArray
|
||||
@Serializable
|
||||
@SerialName("NeedLogin")
|
||||
data object NeedLogin : EhError
|
||||
|
||||
@CborArray
|
||||
@Serializable
|
||||
@SerialName("NoHathClient")
|
||||
data object NoHathClient : EhError
|
||||
|
||||
@CborArray
|
||||
@Serializable
|
||||
@SerialName("InsufficientFunds")
|
||||
data object InsufficientFunds : EhError
|
||||
|
||||
@CborArray
|
||||
@Serializable
|
||||
@SerialName("GalleryUnavailable")
|
||||
data class GalleryUnavailable(val message: String) : EhError
|
||||
|
||||
@CborArray
|
||||
@Serializable
|
||||
@SerialName("IpBanned")
|
||||
data class IpBanned(val message: String) : EhError
|
||||
|
||||
@CborArray
|
||||
@Serializable
|
||||
@SerialName("Error")
|
||||
data class Error(val message: String) : EhError
|
||||
}
|
||||
@@ -13,14 +13,14 @@ object GalleryListParser {
|
||||
throw ParseException("Can't parse gallery list", it)
|
||||
}
|
||||
|
||||
val emptyResult = GalleryListResult(null, null, arrayListOf())
|
||||
val emptyResult = GalleryListResult(null, null, [])
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GalleryListResult(
|
||||
val prev: String?,
|
||||
val next: String?,
|
||||
val galleryInfoList: ArrayList<BaseGalleryInfo>,
|
||||
val galleryInfoList: MutableList<BaseGalleryInfo>,
|
||||
)
|
||||
|
||||
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
|
||||
@@ -26,7 +26,7 @@ object GalleryListUrlParser {
|
||||
fun parse(urlStr: String) = Either.catch {
|
||||
val url = Url(urlStr)
|
||||
if (url.host != EhUrl.DOMAIN_E && url.host != EhUrl.DOMAIN_EX) {
|
||||
return null
|
||||
return@catch null
|
||||
}
|
||||
val segments = url.segments
|
||||
if (segments.isEmpty()) {
|
||||
@@ -42,20 +42,18 @@ object GalleryListUrlParser {
|
||||
mode = if (head == "uploader") ListUrlBuilder.MODE_UPLOADER else ListUrlBuilder.MODE_TAG
|
||||
keyword = segments[1].decodeURLQueryComponent(plusIsSpace = true)
|
||||
}
|
||||
|
||||
"toplist.php" -> {
|
||||
val tl = url.parameters["tl"]
|
||||
if (tl != null && tl in arrayOf("11", "12", "13", "15")) {
|
||||
ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_TOPLIST,
|
||||
jumpTo = url.parameters["p"],
|
||||
mKeyword = tl,
|
||||
keyword = tl,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val category = EhUtils.getCategory(head)
|
||||
if (category != EhUtils.UNKNOWN) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.ehviewer.core.files.isDirectory
|
||||
import com.ehviewer.core.files.isFile
|
||||
import com.ehviewer.core.files.sendTo
|
||||
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.client.getThumbKey
|
||||
import com.hippo.ehviewer.download.downloadLocation
|
||||
@@ -53,7 +53,7 @@ object DownloadThumbInterceptor : Interceptor {
|
||||
// Accessing the recreated file immediately after deleting it throws
|
||||
// FileNotFoundException, so we just overwrite the existing file.
|
||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||
imageCache.read(key) {
|
||||
thumbCache.read(key) {
|
||||
data sendTo thumb
|
||||
}
|
||||
if (thumb != v1Thumb) v1Thumb.delete()
|
||||
|
||||
@@ -148,7 +148,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
mCurrentTask = info
|
||||
with(info) {
|
||||
archiveFile?.let {
|
||||
return onFinish(pages, pages, pages)
|
||||
return onFinish(pages, pages, pages, failed = false)
|
||||
}
|
||||
state = DownloadInfo.STATE_DOWNLOAD
|
||||
speed = -1
|
||||
@@ -682,7 +682,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish(finished: Int, downloaded: Int, total: Int) {
|
||||
override fun onFinish(finished: Int, downloaded: Int, total: Int, failed: Boolean) {
|
||||
launch {
|
||||
mSpeedReminder.onFinish()
|
||||
mCurrentSpider?.let { spider ->
|
||||
@@ -697,11 +697,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
info.downloaded = downloaded
|
||||
info.total = total
|
||||
info.legacy = total - finished
|
||||
if (info.legacy == 0) {
|
||||
info.state = DownloadInfo.STATE_FINISH
|
||||
} else {
|
||||
info.state = DownloadInfo.STATE_FAILED
|
||||
}
|
||||
info.state = if (failed) DownloadInfo.STATE_FAILED else DownloadInfo.STATE_FINISH
|
||||
EhDB.putDownloadInfo(info)
|
||||
mDownloadListener?.onFinish(info)
|
||||
mutableNotifyFlow.emit(info)
|
||||
|
||||
@@ -52,8 +52,6 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DownloadService :
|
||||
@@ -113,14 +111,12 @@ class DownloadService :
|
||||
val label = intent.getStringExtra(KEY_LABEL)
|
||||
deferredMgr.await().startDownload(decodeFromSavedState(gi), label)
|
||||
}
|
||||
|
||||
ACTION_START_RANGE -> {
|
||||
val gidList = intent.getLongArrayExtra(KEY_GID_LIST)
|
||||
if (gidList != null) {
|
||||
deferredMgr.await().startRangeDownload(gidList)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_START_ALL -> deferredMgr.await().startAllDownload()
|
||||
ACTION_STOP_ALL -> deferredMgr.await().stopAllDownload()
|
||||
ACTION_CLEAR -> clear()
|
||||
@@ -377,8 +373,8 @@ class DownloadService :
|
||||
}
|
||||
|
||||
suspend fun run() {
|
||||
channel.receiveAsFlow().sample(DELAY).collect {
|
||||
when (it) {
|
||||
while (true) {
|
||||
when (channel.receive()) {
|
||||
Ops.Notify -> {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
service,
|
||||
@@ -388,10 +384,10 @@ class DownloadService :
|
||||
notifyManager.notify(id, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
Ops.Cancel -> notifyManager.cancel(id)
|
||||
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)
|
||||
}
|
||||
val Default = SortMode(Field.TIME, Order.DESC)
|
||||
val All = listOf(
|
||||
val All = [
|
||||
Default,
|
||||
SortMode(Field.TIME, Order.ASC),
|
||||
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.PAGES, Order.ASC),
|
||||
SortMode(Field.PAGES, Order.DESC),
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,9 @@ import coil3.request.CachePolicy
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.allowHardware
|
||||
import coil3.size.Dimension
|
||||
import coil3.request.maxBitmapSize
|
||||
import coil3.size.Precision
|
||||
import coil3.size.Scale
|
||||
import coil3.size.Size
|
||||
import coil3.size.SizeResolver
|
||||
import com.ehviewer.core.files.openFileDescriptor
|
||||
@@ -90,8 +91,10 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
|
||||
private val sizeResolver = SizeResolver(Size(targetWidth, Dimension.Undefined))
|
||||
private val sizeResolver = with(appCtx.resources.displayMetrics) {
|
||||
val targetSize = minOf(widthPixels, heightPixels) * 4 / 3
|
||||
SizeResolver(Size(targetSize, targetSize))
|
||||
}
|
||||
|
||||
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
|
||||
val request = with(appCtx) {
|
||||
@@ -99,7 +102,9 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
||||
onLeft { data(it.source) }
|
||||
onRight { data(it.source.toUri()) }
|
||||
size(sizeResolver)
|
||||
scale(Scale.FILL)
|
||||
precision(Precision.INEXACT)
|
||||
maxBitmapSize(Size.ORIGINAL)
|
||||
allowHardware(false)
|
||||
hardwareThreshold(Settings.hardwareBitmapThreshold.value)
|
||||
maybeCropBorder(Settings.cropBorder.value)
|
||||
@@ -130,7 +135,6 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
||||
}
|
||||
src.right().decodeCoil(checkExtraneousAds)
|
||||
}
|
||||
|
||||
is ByteBufferSource -> {
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
rewriteGifSource(src.source)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.hippo.ehviewer.ktor
|
||||
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import androidx.webkit.WebViewCompat
|
||||
import com.ehviewer.core.network.EhCookieStore
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
@@ -14,9 +14,12 @@ import io.ktor.client.request.header
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.userAgent
|
||||
import io.ktor.util.appendIfNameAbsent
|
||||
import splitties.init.appCtx
|
||||
|
||||
const val CHROME_MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Mobile Safari/537.36"
|
||||
private const val CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Safari/537.36"
|
||||
// It's safe to assume the WebView package will always be present as we require CookieManager anyway
|
||||
private val WebViewVersion = WebViewCompat.getCurrentWebViewPackage(appCtx)!!.versionName!!.substringBefore('.')
|
||||
val CHROME_MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$WebViewVersion.0.0.0 Mobile Safari/537.36"
|
||||
private val CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$WebViewVersion.0.0.0 Safari/537.36"
|
||||
private const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
||||
private const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.9"
|
||||
|
||||
@@ -26,7 +29,7 @@ fun <T : HttpClientEngineConfig> HttpClientConfig<T>.configureCommon(redirect: B
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
reset()
|
||||
requestTimeoutMillis = 10_000
|
||||
requestTimeoutMillis = 30_000
|
||||
}
|
||||
install(UserAgent)
|
||||
defaultRequest {
|
||||
|
||||
@@ -10,9 +10,6 @@ import android.net.http.UrlRequest
|
||||
import android.net.http.UrlResponseInfo
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresExtension
|
||||
import com.ehviewer.core.preferences.edit
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.util.restartApplication
|
||||
import io.ktor.client.engine.HttpClientEngineBase
|
||||
import io.ktor.client.engine.callContext
|
||||
import io.ktor.client.plugins.HttpTimeoutCapability
|
||||
@@ -35,20 +32,21 @@ import java.nio.ByteBuffer
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
|
||||
// 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)
|
||||
|
||||
private val executor = dispatcher.asExecutor()
|
||||
private val executor = dispatcher.limitedParallelism(1).asExecutor()
|
||||
private val pool = DirectByteBufferPool(32)
|
||||
private val client by lazy {
|
||||
with(config) { HttpEngine.Builder(context).apply(config).build() }
|
||||
@@ -95,7 +93,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
|
||||
}
|
||||
|
||||
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
|
||||
chunkChan.trySend(byteBuffer).getOrThrow()
|
||||
chunkChan.trySendBlocking(byteBuffer).getOrThrow()
|
||||
}
|
||||
|
||||
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
||||
@@ -103,15 +101,6 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
|
||||
}
|
||||
|
||||
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) {
|
||||
// Cronet may crash on some devices, fuck xiaomi
|
||||
// https://github.com/FooIbar/EhViewer/issues/1826
|
||||
val message = error.message.orEmpty()
|
||||
if ("ERR_FILE_NOT_FOUND" in message || "ERR_PROXY_CONNECTION_FAILED" in message) {
|
||||
Settings.edit(true) {
|
||||
it[enableCronet] = false
|
||||
}
|
||||
with(config.context) { restartApplication() }
|
||||
}
|
||||
if (continuation.isActive) {
|
||||
continuation.resumeWithException(error)
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
|
||||
|
||||
package com.hippo.ehviewer.ktor
|
||||
|
||||
import android.net.http.HttpEngine
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresExtension
|
||||
import com.ehviewer.core.util.isAtLeastSExtension7
|
||||
import java.io.File
|
||||
|
||||
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
|
||||
fun CronetConfig.configureClient(enableQuic: Boolean) {
|
||||
config = {
|
||||
setEnableBrotli(true)
|
||||
@@ -24,3 +24,14 @@ fun CronetConfig.configureClient(enableQuic: Boolean) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isCronetAvailable: Boolean
|
||||
get() = isAtLeastSExtension7 && !isDeviceBlocked
|
||||
|
||||
// https://github.com/FooIbar/EhViewer/issues/1826
|
||||
private val isDeviceBlocked = when (Build.VERSION.INCREMENTAL.substringBefore('.')) {
|
||||
"V816", // HyperOS 1
|
||||
"OS2", // HyperOS 2
|
||||
-> true
|
||||
else -> false
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
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 okhttp3.AsyncDns
|
||||
import okhttp3.android.AndroidAsyncDns
|
||||
import java.net.Proxy
|
||||
|
||||
fun OkHttpConfig.configureClient() {
|
||||
config {
|
||||
if (isAtLeastQ) {
|
||||
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
|
||||
dns(EhDns)
|
||||
proxy(Proxy.NO_PROXY)
|
||||
if (!Settings.enableQuic.value) {
|
||||
install(EhSSLSocketFactory)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ private val xml = XML {
|
||||
}
|
||||
xmlDeclMode = XmlDeclMode.Charset
|
||||
xmlVersion = XmlVersion.XML10
|
||||
indent = 2
|
||||
setIndent(2)
|
||||
}
|
||||
|
||||
fun GalleryInfo.getComicInfo(): ComicInfo {
|
||||
@@ -68,7 +68,6 @@ fun GalleryInfo.getComicInfo(): ComicInfo {
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
else -> simpleTags?.forEach { tagString ->
|
||||
val (namespace, tag) = tagString.split(':', limit = 2)
|
||||
.takeIf { it.size == 2 } ?: return@forEach // Ignore temp tags that don't have namespace
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.ehviewer.core.files.sendTo
|
||||
import com.ehviewer.core.model.GalleryDetail
|
||||
import com.ehviewer.core.model.GalleryInfo
|
||||
import com.ehviewer.core.util.logcat
|
||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.Settings
|
||||
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.image.PathSource
|
||||
import com.hippo.ehviewer.jni.archiveFdBatch
|
||||
import com.hippo.ehviewer.ktbuilder.diskCache
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.copyTo
|
||||
import com.hippo.ehviewer.util.sha1
|
||||
@@ -56,8 +56,9 @@ import io.ktor.client.statement.request
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.init.appCtx
|
||||
|
||||
class SpiderDen(val info: GalleryInfo) {
|
||||
private val gid = info.gid
|
||||
@@ -129,11 +130,9 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
SpiderQueen.MODE_READ -> {
|
||||
containInCache(index) || containInDownloadDir(index)
|
||||
}
|
||||
|
||||
SpiderQueen.MODE_DOWNLOAD -> {
|
||||
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
@@ -154,6 +153,10 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
removeTempFile(index)
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
repeat(info.pages, ::removeFromCache)
|
||||
}
|
||||
|
||||
private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) {
|
||||
val name = perFilename(index, extension)
|
||||
read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } }
|
||||
@@ -270,22 +273,18 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun archive() {
|
||||
if (saveAsCbz) {
|
||||
downloadDir?.run {
|
||||
suspend fun archive() = saveAsCbz && downloadDir?.run {
|
||||
resolve(archiveName).let { file ->
|
||||
runCatching {
|
||||
archiveTo(file)
|
||||
}.onFailure {
|
||||
file.delete()
|
||||
if (it is CancellationException) throw it
|
||||
logcat(it)
|
||||
}.isFailure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} == true
|
||||
|
||||
// Postpone this to `SpiderQueen.stop` because files may still be in use by reader
|
||||
suspend fun postArchive(): Boolean {
|
||||
val dir = downloadDir
|
||||
val archived = saveAsCbz && dir?.find(archiveName) != null
|
||||
@@ -335,7 +334,7 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
downloadDir?.run {
|
||||
resolve(COMIC_INFO_FILE).also {
|
||||
if (info !is GalleryDetail && fetchMetadata) {
|
||||
EhEngine.fillGalleryListByApi(listOf(info))
|
||||
EhEngine.fillGalleryListByApi([info])
|
||||
}
|
||||
info.getComicInfo().apply {
|
||||
writeComicInfo(this, it)
|
||||
@@ -349,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 val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
|
||||
private val FileHashRegex = Regex("/h/([0-9a-f]{40})")
|
||||
|
||||
@@ -134,13 +134,14 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyAllPageDownloaded() {
|
||||
private fun notifyAllPageDownloaded(failed: Boolean) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onFinish(
|
||||
mFinishedPages.load(),
|
||||
mDownloadedPages.load(),
|
||||
pageStates.size,
|
||||
failed,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -213,7 +214,10 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
|
||||
private val prepareScope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
private val prepareJob = prepareScope.async { doPrepare() }
|
||||
private val archiveJob = launch(start = CoroutineStart.LAZY) { spiderDen.archive() }
|
||||
private val archiveJob = launch(start = CoroutineStart.LAZY) {
|
||||
val failed = spiderDen.archive()
|
||||
notifyAllPageDownloaded(failed)
|
||||
}
|
||||
|
||||
private suspend fun doPrepare() {
|
||||
spiderDen.initDownloadDirIfExist()
|
||||
@@ -400,8 +404,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
notifyPageSuccess(index)
|
||||
}
|
||||
if (mDownloadedPages.load() == size) {
|
||||
if (mFinishedPages.load() == size) archiveJob.start()
|
||||
notifyAllPageDownloaded()
|
||||
if (mFinishedPages.load() == size) {
|
||||
archiveJob.start()
|
||||
} else {
|
||||
notifyAllPageDownloaded(failed = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,7 +425,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {}
|
||||
fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}
|
||||
fun onPageFailure(index: Int, error: String?, finished: Int, downloaded: Int, total: Int) {}
|
||||
fun onFinish(finished: Int, downloaded: Int, total: Int) {}
|
||||
fun onFinish(finished: Int, downloaded: Int, total: Int, failed: Boolean) {}
|
||||
fun onPageReady(index: Int) {}
|
||||
}
|
||||
|
||||
@@ -587,6 +594,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
previousPToken,
|
||||
)
|
||||
}.getOrElse {
|
||||
if (it is FatalException) throw it
|
||||
forceHtml = true
|
||||
return@repeat
|
||||
}.let {
|
||||
|
||||
@@ -233,7 +233,6 @@ private suspend fun doModifyFavorites(
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
LOCAL_FAVORITED -> {
|
||||
if (localFavorited) {
|
||||
EhDB.removeLocalFavorites(galleryInfo)
|
||||
@@ -248,7 +247,6 @@ private suspend fun doModifyFavorites(
|
||||
}
|
||||
!localFavorited
|
||||
}
|
||||
|
||||
in 0..9 -> {
|
||||
EhEngine.modifyFavorites(gid, token, slot, note)
|
||||
favoriteSlot = slot
|
||||
@@ -256,7 +254,6 @@ private suspend fun doModifyFavorites(
|
||||
favoriteNote = note
|
||||
true
|
||||
}
|
||||
|
||||
else -> throw EhException("Invalid favorite slot!")
|
||||
}
|
||||
FavouriteStatusRouter.notify(galleryInfo)
|
||||
@@ -305,13 +302,11 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
|
||||
EhDB.putHistoryInfo(info)
|
||||
navToReader(info)
|
||||
}
|
||||
|
||||
1 -> if (downloaded) {
|
||||
confirmRemoveDownload(info)
|
||||
} else {
|
||||
startDownload(false, info)
|
||||
}
|
||||
|
||||
2 -> if (favorited) {
|
||||
runSuspendCatching {
|
||||
removeFromFavorites(info)
|
||||
@@ -327,7 +322,6 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
|
||||
tip(R.string.add_to_favorite_failure)
|
||||
}
|
||||
}
|
||||
|
||||
3 -> showMoveDownloadLabel(info)
|
||||
}
|
||||
}
|
||||
@@ -400,7 +394,7 @@ suspend fun showMoveDownloadLabel(info: GalleryInfo) {
|
||||
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
|
||||
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
|
||||
val label = if (selected == 0) null else labels[selected]
|
||||
DownloadManager.changeLabel(listOf(downloadInfo), label)
|
||||
DownloadManager.changeLabel([downloadInfo], label)
|
||||
}
|
||||
|
||||
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.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
@@ -63,7 +66,7 @@ import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
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.rememberDrawerState2
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
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.LocalWindowSizeClass
|
||||
import com.ehviewer.core.util.isAtLeastQ
|
||||
import com.ehviewer.core.util.isAtLeastR
|
||||
import com.ehviewer.core.util.isAtLeastS
|
||||
import com.ehviewer.core.util.withIOContext
|
||||
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.utils.currentDestinationAsState
|
||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -175,7 +181,7 @@ private val navItems = arrayOf<Triple<Direction, Int, ImageVector>>(
|
||||
Triple(SettingsScreenDestination, R.string.settings, Icons.Default.Settings),
|
||||
)
|
||||
|
||||
class MainActivity : EhActivity() {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private var shareUrl: String? = null
|
||||
|
||||
@@ -203,6 +209,15 @@ class MainActivity : EhActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen().setKeepOnScreenCondition { !initialized }
|
||||
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 {
|
||||
val configuration = LocalConfiguration.current
|
||||
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)
|
||||
LaunchedEffect(Unit) {
|
||||
intentFlow.collect { intent ->
|
||||
@@ -276,7 +300,7 @@ class MainActivity : EhActivity() {
|
||||
if ("text/plain" == type) {
|
||||
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (keyword != null && !navWithUrl(keyword)) {
|
||||
navigate(ListUrlBuilder(mKeyword = keyword).asDst())
|
||||
navigate(ListUrlBuilder(keyword = keyword).asDst())
|
||||
}
|
||||
} else if (type != null && type.startsWith("image/")) {
|
||||
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
||||
@@ -356,7 +380,7 @@ class MainActivity : EhActivity() {
|
||||
var snackbarFabPadding by remember { mutableStateOf(0.dp) }
|
||||
val drawerEnabled = drawerHandle.isNotEmpty()
|
||||
val density = LocalDensity.current
|
||||
val adaptiveInfo = currentWindowAdaptiveInfo()
|
||||
val adaptiveInfo = currentWindowAdaptiveInfoV2()
|
||||
val needSignIn by Settings.needSignIn.collectAsState()
|
||||
val launchPage by Settings.launchPage.collectAsState()
|
||||
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.request.crossfade
|
||||
import coil3.size.Scale
|
||||
import coil3.size.Size
|
||||
import com.ehviewer.core.util.toIntOrDefault
|
||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||
import kotlin.math.roundToInt
|
||||
@@ -21,7 +22,7 @@ class CoilImageGetter(private val onSuccess: () -> Unit) : Html.ImageGetter {
|
||||
val srcHeight = groupValues[2].toInt()
|
||||
val dstWidth = groupValues[3].toIntOrDefault(200)
|
||||
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, 200, 300)
|
||||
with(appCtx) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.hippo.ehviewer.ui.login
|
||||
|
||||
import arrow.core.Either.Companion.catch
|
||||
import com.ehviewer.core.network.EhCookieStore
|
||||
import com.ehviewer.core.util.logcat
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.ehviewer.core.network.EhCookieStore
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewState
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.ui.Screen
|
||||
@@ -31,6 +31,7 @@ fun AnimatedVisibilityScope.WebViewSignInScreen(navigator: DestinationsNavigator
|
||||
Settings.desktopSite.value = false
|
||||
}
|
||||
if (EhCookieStore.hasSignedIn()) {
|
||||
EhCookieStore.flush()
|
||||
postLogin()
|
||||
state.webView?.destroy()
|
||||
bgWork { awaitCancellation() }
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DownloadDone
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Card
|
||||
@@ -26,6 +27,7 @@ import androidx.compose.material3.ShapeDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.movableContentOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
@@ -35,7 +37,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -45,7 +46,7 @@ import com.ehviewer.core.ui.component.CrystalCard
|
||||
import com.ehviewer.core.ui.component.GalleryListCardRating
|
||||
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
|
||||
import com.ehviewer.core.ui.util.listThumbGenerator
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
@@ -60,6 +61,7 @@ fun DownloadCard(
|
||||
onStop: () -> Unit,
|
||||
info: DownloadInfo,
|
||||
selectMode: Boolean,
|
||||
showProgress: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) = CrystalCard(modifier = modifier, onClick = onClick, onLongClick = onLongClick, interactionSource = interactionSource) {
|
||||
@@ -99,7 +101,14 @@ fun DownloadCard(
|
||||
DownloadInfo.STATE_NONE -> stringResource(R.string.download_state_none)
|
||||
DownloadInfo.STATE_WAIT -> stringResource(R.string.download_state_wait)
|
||||
DownloadInfo.STATE_FAILED -> if (info.legacy <= 0) stateFailed else stateFailed2
|
||||
DownloadInfo.STATE_FINISH -> stringResource(R.string.download_state_finish)
|
||||
DownloadInfo.STATE_FINISH -> {
|
||||
if (showProgress) {
|
||||
val readProgress by remember { EhDB.getReadProgressFlow(info.gid) }.collectAsState(0)
|
||||
if (readProgress > 0) "${readProgress + 1}/${info.pages}P" else "${info.pages}P"
|
||||
} else {
|
||||
"${info.pages}P"
|
||||
}
|
||||
}
|
||||
else -> null // The item has been removed and this will be disposed soon
|
||||
}
|
||||
ProvideTextStyle(MaterialTheme.typography.labelLarge) {
|
||||
@@ -149,24 +158,24 @@ fun DownloadCard(
|
||||
Text(
|
||||
text = categoryText,
|
||||
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
|
||||
color = if (Settings.harmonizeCategoryColor.value) Color.Unspecified else EhUtils.categoryTextColor,
|
||||
color = EhUtils.getCategoryTextColor(categoryColor),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
val running = downloadState == DownloadInfo.STATE_WAIT || downloadState == DownloadInfo.STATE_DOWNLOAD
|
||||
val icon = remember {
|
||||
movableContentOf<Boolean> {
|
||||
Icon(imageVector = if (it) Icons.Default.Pause else Icons.Default.PlayArrow, contentDescription = null)
|
||||
}
|
||||
val icon = when (downloadState) {
|
||||
DownloadInfo.STATE_WAIT, DownloadInfo.STATE_DOWNLOAD -> Icons.Default.Pause
|
||||
DownloadInfo.STATE_FINISH -> Icons.Default.DownloadDone
|
||||
else -> Icons.Default.PlayArrow
|
||||
}
|
||||
if (selectMode) {
|
||||
Box(modifier = Modifier.offset(4.dp).minimumInteractiveComponentSize()) {
|
||||
icon(running)
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = if (running) onStop else onStart, shapes = IconButtonDefaults.shapes(), modifier = Modifier.offset(4.dp)) {
|
||||
icon(running)
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.ShapeDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -33,7 +33,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -46,7 +45,7 @@ import com.ehviewer.core.ui.component.GalleryListCardRating
|
||||
import com.ehviewer.core.ui.util.SharedElementBox
|
||||
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
|
||||
import com.ehviewer.core.ui.util.listThumbGenerator
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||
@@ -58,6 +57,7 @@ fun GalleryInfoListItem(
|
||||
onLongClick: () -> Unit,
|
||||
info: GalleryInfo,
|
||||
showPages: Boolean,
|
||||
showProgress: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
isInFavScene: Boolean = false,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
@@ -126,7 +126,12 @@ fun GalleryInfoListItem(
|
||||
Text(text = it)
|
||||
}
|
||||
if (info.pages != 0 && showPages) {
|
||||
Text(text = "${info.pages}P")
|
||||
val readProgress = if (showProgress) {
|
||||
remember { EhDB.getReadProgressFlow(info.gid) }.collectAsState(0).value
|
||||
} else {
|
||||
0
|
||||
}
|
||||
Text(text = if (readProgress > 0) "${readProgress + 1}/${info.pages}P" else "${info.pages}P")
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -135,7 +140,7 @@ fun GalleryInfoListItem(
|
||||
Text(
|
||||
text = categoryText,
|
||||
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
|
||||
color = if (Settings.harmonizeCategoryColor.value) Color.Unspecified else EhUtils.categoryTextColor,
|
||||
color = EhUtils.getCategoryTextColor(categoryColor),
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text = info.posted.orEmpty())
|
||||
@@ -154,6 +159,7 @@ fun GalleryInfoGridItem(
|
||||
modifier: Modifier = Modifier,
|
||||
showLanguage: Boolean = true,
|
||||
showPages: Boolean = true,
|
||||
showProgress: Boolean = true,
|
||||
showFavoriteStatus: Boolean = true,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) = ElevatedCard(
|
||||
@@ -187,11 +193,16 @@ fun GalleryInfoGridItem(
|
||||
Badge(
|
||||
modifier = Modifier.align(Alignment.TopEnd).widthIn(min = 32.dp).height(24.dp),
|
||||
containerColor = categoryColor,
|
||||
contentColor = if (Settings.harmonizeCategoryColor.value) contentColorFor(categoryColor) else EhUtils.categoryTextColor,
|
||||
contentColor = EhUtils.getCategoryTextColor(categoryColor),
|
||||
) {
|
||||
val shouldShowLanguage = showLanguage && info.simpleLanguage != null
|
||||
if (showPages && info.pages > 0) {
|
||||
Text(text = "${info.pages}")
|
||||
val readProgress = if (showProgress) {
|
||||
remember { EhDB.getReadProgressFlow(info.gid) }.collectAsState(0).value
|
||||
} else {
|
||||
0
|
||||
}
|
||||
Text(text = if (readProgress > 0) "${readProgress + 1}/${info.pages}" else "${info.pages}")
|
||||
if (shouldShowLanguage) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.plus
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
@@ -196,7 +197,6 @@ fun GalleryList(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is LoadState.Error -> {
|
||||
Surface {
|
||||
ErrorTip(
|
||||
@@ -205,7 +205,6 @@ fun GalleryList(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is LoadState.NotLoading -> if (data.itemCount == 0) {
|
||||
// Only for local favorites as empty gallery lists from network are treated as error
|
||||
ErrorTip(modifier = Modifier.widthIn(max = 228.dp), text = stringResource(id = R.string.gallery_list_empty_hit))
|
||||
|
||||
@@ -22,7 +22,6 @@ fun LoadStateIndicator(
|
||||
is LoadState.Loading -> {
|
||||
LinearWavyProgressIndicator(modifier = modifier)
|
||||
}
|
||||
|
||||
is LoadState.Error -> {
|
||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(text = state.error.displayString())
|
||||
@@ -31,6 +30,5 @@ fun LoadStateIndicator(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is LoadState.NotLoading -> Unit
|
||||
}
|
||||
@@ -47,7 +47,7 @@ import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.awaitResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
private val categoryTable = arrayOf(
|
||||
private val categoryTable = [
|
||||
EhUtils.DOUJINSHI to R.string.doujinshi,
|
||||
EhUtils.MANGA to R.string.manga,
|
||||
EhUtils.ARTIST_CG to R.string.artist_cg,
|
||||
@@ -58,7 +58,7 @@ private val categoryTable = arrayOf(
|
||||
EhUtils.COSPLAY to R.string.cosplay,
|
||||
EhUtils.ASIAN_PORN to R.string.asian_porn,
|
||||
EhUtils.MISC to R.string.misc,
|
||||
)
|
||||
]
|
||||
|
||||
@Composable
|
||||
context(_: DialogState, _: CoroutineScope, _: Context)
|
||||
@@ -193,9 +193,6 @@ fun SearchFilter(
|
||||
}
|
||||
if (error != null) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = error, style = MaterialTheme.typography.bodySmall)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(imageVector = Icons.Default.Info, contentDescription = null)
|
||||
},
|
||||
@@ -204,6 +201,9 @@ fun SearchFilter(
|
||||
headlineColor = 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.Text
|
||||
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.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -23,6 +19,8 @@ import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import com.ehviewer.core.i18n.R
|
||||
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.asMutableState
|
||||
|
||||
@@ -34,12 +32,11 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
|
||||
field = customBrightness,
|
||||
)
|
||||
AnimatedVisibility(visible = customBrightness.value) {
|
||||
val brightness = Settings.customBrightnessValue.asMutableState()
|
||||
val brightness = rememberIntPrefSliderState(Settings.customBrightnessValue, -75..100)
|
||||
SliderChoice(
|
||||
startSlot = { Icon(imageVector = Icons.Default.Brightness5, contentDescription = null) },
|
||||
endSlot = { RollingNumber(number = brightness.value, length = 3) },
|
||||
range = -75..100,
|
||||
field = brightness,
|
||||
state = brightness,
|
||||
)
|
||||
}
|
||||
val colorFilter = Settings.colorFilter.asMutableState()
|
||||
@@ -48,47 +45,39 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
|
||||
field = colorFilter,
|
||||
)
|
||||
AnimatedVisibility(visible = colorFilter.value) {
|
||||
var color by Settings.colorFilterValue.asMutableState()
|
||||
val rf = remember { mutableIntStateOf(color.red) }
|
||||
val r by rf
|
||||
val gf = remember { mutableIntStateOf(color.green) }
|
||||
val g by gf
|
||||
val bf = remember { mutableIntStateOf(color.blue) }
|
||||
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)
|
||||
var color by Settings.colorFilterValue
|
||||
val r = rememberIntSliderState(color.red, 0..255)
|
||||
val g = rememberIntSliderState(color.green, 0..255)
|
||||
val b = rememberIntSliderState(color.blue, 0..255)
|
||||
val a = rememberIntSliderState(color.alpha, 0..255)
|
||||
color = (a.value shl 24) or (r.value shl 16) or (g.value shl 8) or (b.value)
|
||||
Column {
|
||||
SliderChoice(
|
||||
startSlot = { Text(text = "R") },
|
||||
endSlot = { RollingNumber(number = r, length = 3) },
|
||||
range = 0..255,
|
||||
field = rf,
|
||||
endSlot = { RollingNumber(number = r.value, length = 3) },
|
||||
state = r,
|
||||
)
|
||||
SliderChoice(
|
||||
startSlot = { Text(text = "G") },
|
||||
endSlot = { RollingNumber(number = g, length = 3) },
|
||||
range = 0..255,
|
||||
field = gf,
|
||||
endSlot = { RollingNumber(number = g.value, length = 3) },
|
||||
state = g,
|
||||
)
|
||||
SliderChoice(
|
||||
startSlot = { Text(text = "B") },
|
||||
endSlot = { RollingNumber(number = b, length = 3) },
|
||||
range = 0..255,
|
||||
field = bf,
|
||||
endSlot = { RollingNumber(number = b.value, length = 3) },
|
||||
state = b,
|
||||
)
|
||||
SliderChoice(
|
||||
startSlot = { Text(text = "A") },
|
||||
endSlot = { RollingNumber(number = a, length = 3) },
|
||||
range = 0..255,
|
||||
field = af,
|
||||
endSlot = { RollingNumber(number = a.value, length = 3) },
|
||||
state = a,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpinnerChoice(
|
||||
title = stringResource(id = R.string.pref_color_filter_mode),
|
||||
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(),
|
||||
)
|
||||
SwitchChoice(
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.hippo.ehviewer.ui.reader
|
||||
|
||||
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.layout.Box
|
||||
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.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.WavyProgressIndicatorDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
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.image.Image
|
||||
import com.hippo.ehviewer.util.AdsPlaceholderFile
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.CombinedCircularProgressIndicator
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.drop
|
||||
|
||||
@@ -83,7 +89,22 @@ fun PagerItem(
|
||||
modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT),
|
||||
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 -> {
|
||||
|
||||
@@ -204,20 +204,17 @@ private fun PageContainer(
|
||||
moveToNext()
|
||||
}
|
||||
}
|
||||
|
||||
NavigationRegion.PREV -> {
|
||||
val canPan = if (isRtl) panLeft(distance, bounds) else panRight(distance, bounds)
|
||||
if (!canPan) {
|
||||
moveToPrevious()
|
||||
}
|
||||
}
|
||||
|
||||
NavigationRegion.RIGHT -> {
|
||||
if (!panLeft(distance, bounds)) {
|
||||
if (isRtl) moveToPrevious() else moveToNext()
|
||||
}
|
||||
}
|
||||
|
||||
NavigationRegion.LEFT -> {
|
||||
if (!panRight(distance, bounds)) {
|
||||
if (isRtl) moveToNext() else moveToPrevious()
|
||||
|
||||
@@ -58,7 +58,7 @@ private fun PagerSetting() = Column {
|
||||
SpinnerChoice(
|
||||
title = stringResource(id = R.string.pref_viewer_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,
|
||||
)
|
||||
AnimatedVisibility(visible = pagerNav.value != 5) {
|
||||
@@ -66,7 +66,7 @@ private fun PagerSetting() = Column {
|
||||
SpinnerChoice(
|
||||
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
||||
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(),
|
||||
)
|
||||
SwitchChoice(
|
||||
@@ -79,7 +79,7 @@ private fun PagerSetting() = Column {
|
||||
SpinnerChoice(
|
||||
title = stringResource(id = R.string.pref_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,
|
||||
)
|
||||
AnimatedVisibility(visible = scaleType.value == 1) {
|
||||
@@ -91,7 +91,7 @@ private fun PagerSetting() = Column {
|
||||
SpinnerChoice(
|
||||
title = stringResource(id = R.string.pref_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(),
|
||||
)
|
||||
SwitchChoice(
|
||||
@@ -111,14 +111,14 @@ private fun WebtoonSetting() = Column {
|
||||
SpinnerChoice(
|
||||
title = stringResource(id = R.string.pref_viewer_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,
|
||||
)
|
||||
AnimatedVisibility(visible = webtoonNav.value != 5) {
|
||||
SpinnerChoice(
|
||||
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
@@ -26,9 +25,10 @@ import androidx.compose.material3.CircularWavyProgressIndicator
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.setting.ReadingModeType
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.merge
|
||||
@@ -214,9 +215,9 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
||||
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 syncState = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
|
||||
val sync = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
|
||||
var appbarVisible by remember { mutableStateOf(false) }
|
||||
val isWebtoon by rememberUpdatedState(ReadingModeType.isWebtoon(readingMode))
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -231,7 +232,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
syncState.Sync(isWebtoon) { appbarVisible = false }
|
||||
sync.Sync(isWebtoon) { appbarVisible = false }
|
||||
val bgColor by collectBackgroundColorAsState()
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
LaunchedEffect(isDarkTheme) {
|
||||
@@ -251,7 +252,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
||||
val blocked = page.status is PageStatus.Blocked
|
||||
dialog { cont ->
|
||||
fun dispose() = cont.resume(Unit)
|
||||
val state = rememberModalBottomSheetState()
|
||||
val state = rememberBottomSheetState(SheetValue.Hidden)
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { dispose() },
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
|
||||
@@ -292,10 +293,10 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
||||
onNavigationModeChange = { showNavigationOverlay = true },
|
||||
onSelectPage = onSelectPage,
|
||||
onMenuRegionClick = { appbarVisible = !appbarVisible },
|
||||
modifier = Modifier.background(bgColor).pointerInput(syncState) {
|
||||
modifier = Modifier.background(bgColor).pointerInput(sync) {
|
||||
awaitEachGesture {
|
||||
waitForUpOrCancellation()
|
||||
syncState.reset()
|
||||
sync.reset()
|
||||
showNavigationOverlay = false
|
||||
}
|
||||
}.fillMaxSize().windowInsetsPadding(insets),
|
||||
@@ -323,7 +324,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
||||
)
|
||||
if (brightness) {
|
||||
LaunchedEffect(Unit) {
|
||||
Settings.customBrightnessValue.valueFlow().sample(100)
|
||||
Settings.customBrightnessValue.valueFlow().sample(100.milliseconds)
|
||||
.onCompletion { activity.setCustomBrightnessValue(0) }
|
||||
.collect { activity.setCustomBrightnessValue(it) }
|
||||
}
|
||||
@@ -332,7 +333,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
||||
if (showPageNumber && !appbarVisible) {
|
||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
|
||||
PageIndicatorText(
|
||||
currentPage = syncState.sliderValue,
|
||||
currentPage = sync.slider.value,
|
||||
totalPages = pageLoader.size,
|
||||
modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
|
||||
)
|
||||
@@ -343,9 +344,9 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
|
||||
title = pageLoader.title,
|
||||
isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT,
|
||||
showSeekBar = showSeekbar,
|
||||
currentPage = syncState.sliderValue,
|
||||
slider = sync.slider,
|
||||
totalPages = pageLoader.size,
|
||||
onSliderValueChange = syncState::sliderScrollTo,
|
||||
onSliderValueChange = sync::sliderScrollTo,
|
||||
onClickSettings = {
|
||||
launch {
|
||||
dialog { cont ->
|
||||
|
||||
@@ -6,11 +6,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import com.ehviewer.core.ui.component.IntSliderState
|
||||
import com.hippo.ehviewer.gallery.PageLoader
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
@@ -23,12 +23,11 @@ class SliderPagerDoubleSync(
|
||||
private val pageLoader: PageLoader,
|
||||
) {
|
||||
private var sliderFollowPager by mutableStateOf(true)
|
||||
var sliderValue by mutableIntStateOf(pageLoader.startPage + 1)
|
||||
private set
|
||||
val slider = IntSliderState(pageLoader.startPage + 1, 1..pageLoader.size)
|
||||
|
||||
fun sliderScrollTo(index: Int) {
|
||||
sliderFollowPager = false
|
||||
sliderValue = index.coerceIn(1, pageLoader.size)
|
||||
slider.value = index
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
@@ -50,7 +49,7 @@ class SliderPagerDoubleSync(
|
||||
@Composable
|
||||
fun Sync(webtoon: Boolean, onPageSelected: () -> Unit) {
|
||||
val currentIndexFlow = remember(webtoon) {
|
||||
val initialIndex = sliderValue - 1
|
||||
val initialIndex = slider.value - 1
|
||||
sliderFollowPager = if (webtoon) {
|
||||
lazyListState.firstVisibleItemIndex == initialIndex
|
||||
} else {
|
||||
@@ -61,14 +60,14 @@ class SliderPagerDoubleSync(
|
||||
if (sliderFollowPager) {
|
||||
LaunchedEffect(currentIndexFlow) {
|
||||
currentIndexFlow.drop(1).collect { index ->
|
||||
sliderValue = index + 1
|
||||
slider.value = index + 1
|
||||
pageLoader.startPage = index
|
||||
onPageSelected()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(webtoon) {
|
||||
snapshotFlow { sliderValue - 1 }.collectLatest { index ->
|
||||
snapshotFlow { slider.value - 1 }.collectLatest { index ->
|
||||
if (webtoon) {
|
||||
lazyListState.scrollToItem(index)
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -81,6 +82,7 @@ fun WebtoonViewer(
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(horizontal = sidePadding),
|
||||
verticalArrangement = Arrangement.spacedBy(if (withGaps) 15.dp else 0.dp),
|
||||
cacheWindow = LazyLayoutCacheWindow(SCROLL_FRACTION, SCROLL_FRACTION),
|
||||
) {
|
||||
items(items, key = { it.index }) { page ->
|
||||
PagerItem(
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ehviewer.core.ui.component.IntSliderState
|
||||
import com.ehviewer.core.ui.component.Slider
|
||||
|
||||
@Composable
|
||||
@@ -84,20 +85,17 @@ fun SwitchChoice(title: String, field: MutableState<Boolean>) {
|
||||
fun SliderChoice(
|
||||
startSlot: @Composable () -> Unit,
|
||||
endSlot: @Composable () -> Unit,
|
||||
range: IntRange,
|
||||
field: MutableState<Int>,
|
||||
state: IntSliderState,
|
||||
) {
|
||||
var value by field
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
startSlot()
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = { value = it },
|
||||
state = state,
|
||||
onValueChange = { state.value = it },
|
||||
modifier = Modifier.weight(1f).padding(8.dp),
|
||||
valueRange = range,
|
||||
)
|
||||
endSlot()
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.plus
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
@@ -146,6 +147,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
|
||||
var gridView by Settings.gridView.asMutableState()
|
||||
var sortMode by Settings.downloadSortMode.asMutableState()
|
||||
val filterMode by Settings.downloadFilterMode.collectAsState { DownloadsFilterMode.from(it) }
|
||||
val showProgress by Settings.showReadingProgress.collectAsState()
|
||||
var filterState by rememberSerializable { mutableStateOf(DownloadsFilterState(filterMode, Settings.recentDownloadLabel.value)) }
|
||||
var invalidateKey by rememberSaveable { mutableStateOf(false) }
|
||||
var isLoading by rememberSaveable { mutableStateOf(true) }
|
||||
@@ -228,6 +230,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
|
||||
fun switchLabel(label: String?) {
|
||||
Settings.recentDownloadLabel.value = label
|
||||
filterState = filterState.copy(label = label)
|
||||
fabHidden = false
|
||||
}
|
||||
|
||||
LaunchedEffect(filterState) {
|
||||
@@ -594,6 +597,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
|
||||
info = info,
|
||||
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
||||
showLanguage = false,
|
||||
showProgress = showProgress,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -633,6 +637,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
|
||||
onStop = { launchIO { DownloadManager.stopDownload(info.gid) } },
|
||||
info = info,
|
||||
selectMode = selectMode,
|
||||
showProgress = showProgress,
|
||||
modifier = Modifier.height(height),
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
||||
@@ -104,6 +104,8 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
|
||||
var urlBuilder by viewModel.urlBuilder
|
||||
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
||||
var fabExpanded by remember { mutableStateOf(false) }
|
||||
var fabHidden by remember { mutableStateOf(false) }
|
||||
|
||||
// Derived State
|
||||
val keyword = urlBuilder.keyword
|
||||
@@ -157,22 +159,21 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
|
||||
) {
|
||||
faves.forEachIndexed { index, (name, count) ->
|
||||
ListItem(
|
||||
headlineContent = { Text(text = name) },
|
||||
trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) },
|
||||
modifier = Modifier.clip(CardDefaults.shape).clickable {
|
||||
val newCat = index - 2
|
||||
refresh(FavListUrlBuilder(newCat))
|
||||
Settings.recentFavCat = newCat
|
||||
fabHidden = false
|
||||
launch { sheetState.close() }
|
||||
},
|
||||
colors = listItemOnDrawerColor(urlBuilder.favCat == index - 2),
|
||||
content = { Text(text = name) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fabExpanded by remember { mutableStateOf(false) }
|
||||
var fabHidden by remember { mutableStateOf(false) }
|
||||
val checkedInfoMap = remember { mutableStateMapOf<Long, BaseGalleryInfo>() }
|
||||
val selectMode = checkedInfoMap.isNotEmpty()
|
||||
DrawerHandle(!selectMode && !searchBarExpanded)
|
||||
@@ -200,6 +201,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
|
||||
val listMode by Settings.listMode.collectAsState()
|
||||
val height by collectListThumbSizeAsState()
|
||||
val showPages by Settings.showGalleryPages.collectAsState()
|
||||
val showProgress by Settings.showReadingProgress.collectAsState()
|
||||
val searchBarConnection = remember {
|
||||
val slop = ViewConfiguration.get(contextOf<Context>()).scaledTouchSlop
|
||||
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
||||
@@ -244,6 +246,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
|
||||
},
|
||||
info = info,
|
||||
showPages = showPages,
|
||||
showProgress = showProgress,
|
||||
modifier = Modifier.height(height),
|
||||
isInFavScene = true,
|
||||
interactionSource = interactionSource,
|
||||
@@ -273,6 +276,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
|
||||
},
|
||||
info = info,
|
||||
showPages = showPages,
|
||||
showProgress = showProgress,
|
||||
showFavoriteStatus = false,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
||||
@@ -260,12 +260,8 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
|
||||
Column {
|
||||
data.forEach { (name, vote) ->
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = name)
|
||||
},
|
||||
trailingContent = {
|
||||
Text(text = vote)
|
||||
},
|
||||
trailingContent = { Text(text = vote) },
|
||||
content = { Text(text = name) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -409,7 +405,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
|
||||
navigate(
|
||||
ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_UPLOADER,
|
||||
mKeyword = item.user,
|
||||
keyword = item.user,
|
||||
).asDst(),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -215,7 +215,7 @@ fun GalleryDetailContent(
|
||||
if (uploader.isNullOrEmpty() || disowned) {
|
||||
return
|
||||
}
|
||||
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, mKeyword = uploader).asDst())
|
||||
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, keyword = uploader).asDst())
|
||||
}
|
||||
|
||||
fun onGalleryInfoCardClick() {
|
||||
@@ -320,7 +320,6 @@ fun GalleryDetailContent(
|
||||
galleryPreview(galleryDetail, previews) { navToReader(galleryDetail.galleryInfo, it) }
|
||||
}
|
||||
}
|
||||
|
||||
else -> FastScrollLazyVerticalGrid(
|
||||
columns = GridCells.Fixed(thumbColumns),
|
||||
contentPadding = contentPadding,
|
||||
@@ -530,21 +529,21 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
|
||||
navigate(
|
||||
ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_NORMAL,
|
||||
mKeyword = "\"" + keyword + "\"",
|
||||
keyword = "\"" + keyword + "\"",
|
||||
).asDst(),
|
||||
)
|
||||
} else if (artistTag != null) {
|
||||
navigate(
|
||||
ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_TAG,
|
||||
mKeyword = artistTag,
|
||||
keyword = artistTag,
|
||||
).asDst(),
|
||||
)
|
||||
} else if (null != galleryDetail.uploader) {
|
||||
navigate(
|
||||
ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_UPLOADER,
|
||||
mKeyword = galleryDetail.uploader,
|
||||
keyword = galleryDetail.uploader,
|
||||
).asDst(),
|
||||
)
|
||||
}
|
||||
@@ -722,15 +721,17 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
|
||||
val upTag = stringResource(R.string.tag_vote_up)
|
||||
val downTag = stringResource(R.string.tag_vote_down)
|
||||
val withDraw = stringResource(R.string.tag_vote_withdraw)
|
||||
fun search(tag: String) = navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, keyword = tag).asDst())
|
||||
GalleryTags(
|
||||
tagGroups = tags,
|
||||
onTagClick = {
|
||||
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, mKeyword = it).asDst())
|
||||
},
|
||||
onTagClick = ::search,
|
||||
onTagLongClick = { tag, translation, vote ->
|
||||
val rawValue = tag.substringAfter(':')
|
||||
launchIO {
|
||||
awaitSelectAction {
|
||||
onSelect(ctx.getString(R.string.search_bar_hint, tag)) {
|
||||
withUIContext { search(tag) }
|
||||
}
|
||||
onSelect(copy) {
|
||||
addTextToClipboard(tag)
|
||||
}
|
||||
|
||||
@@ -49,14 +49,12 @@ import com.ehviewer.core.ui.util.rememberInVM
|
||||
import com.ehviewer.core.util.launchIO
|
||||
import com.ehviewer.core.util.logcat
|
||||
import com.ehviewer.core.util.withIOContext
|
||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.fillInfo
|
||||
import com.hippo.ehviewer.client.getImageKey
|
||||
import com.hippo.ehviewer.coil.justDownload
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
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))
|
||||
}
|
||||
repeat(gd.pages) {
|
||||
val key = getImageKey(gd.gid, it)
|
||||
imageCache.remove(key)
|
||||
}
|
||||
SpiderDen(gd).clearCache()
|
||||
snackbar(cacheCleared)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -147,7 +147,7 @@ fun AnimatedVisibilityScope.WhatshotScreen(navigator: DestinationsNavigator) = G
|
||||
|
||||
@Destination<RootGraph>
|
||||
@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>
|
||||
@Composable
|
||||
@@ -160,6 +160,9 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
var urlBuilder by viewModel.urlBuilder
|
||||
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
||||
var fabExpanded by remember { mutableStateOf(false) }
|
||||
var fabHidden by remember { mutableStateOf(false) }
|
||||
|
||||
val animateItems by Settings.animateItems.collectAsState()
|
||||
|
||||
var category by rememberMutableStateInDataStore("SearchCategory") { EhUtils.ALL_CATEGORY }
|
||||
@@ -169,16 +172,16 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
|
||||
LaunchedEffect(urlBuilder) {
|
||||
if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category
|
||||
if (urlBuilder.mode != MODE_TOPLIST) {
|
||||
var keyword = urlBuilder.keyword.orEmpty()
|
||||
if (urlBuilder.mode == MODE_TAG) {
|
||||
keyword = wrapTagKeyword(keyword)
|
||||
val keyword = urlBuilder.keyword?.let { keyword ->
|
||||
when (urlBuilder.mode) {
|
||||
MODE_UPLOADER -> "uploader:\"$keyword\""
|
||||
MODE_TAG -> wrapTagKeyword(keyword)
|
||||
MODE_TOPLIST -> ""
|
||||
else -> keyword
|
||||
}
|
||||
if (keyword.isNotBlank()) {
|
||||
}.orEmpty()
|
||||
searchFieldState.setTextAndPlaceCursorAtEnd(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val density = LocalDensity.current
|
||||
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
|
||||
@@ -218,14 +221,13 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
ListItem(
|
||||
modifier = Modifier.padding(horizontal = 4.dp).clip(CardDefaults.shape).clickable {
|
||||
Settings.recentToplist = keyword
|
||||
urlBuilder = ListUrlBuilder(MODE_TOPLIST, mKeyword = keyword)
|
||||
urlBuilder = ListUrlBuilder(MODE_TOPLIST, keyword = keyword)
|
||||
data.refresh()
|
||||
fabHidden = false
|
||||
launch { sheetState.close() }
|
||||
},
|
||||
headlineContent = {
|
||||
Text(text = name)
|
||||
},
|
||||
colors = listItemOnDrawerColor(urlBuilder.keyword == keyword),
|
||||
content = { Text(text = name) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -374,6 +376,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
language = languageFilter
|
||||
}
|
||||
data.refresh()
|
||||
fabHidden = false
|
||||
}
|
||||
launch { sheetState.close() }
|
||||
},
|
||||
@@ -425,9 +428,6 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
var fabExpanded by remember { mutableStateOf(false) }
|
||||
var fabHidden by remember { mutableStateOf(false) }
|
||||
|
||||
val openGalleryKeyword = stringResource(R.string.gallery_list_search_bar_open_gallery)
|
||||
abstract class UrlSuggestion : Suggestion() {
|
||||
override val keyword = openGalleryKeyword
|
||||
@@ -482,9 +482,9 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
searchFieldState = searchFieldState,
|
||||
suggestionProvider = {
|
||||
GalleryDetailUrlParser.parse(it, false)?.run {
|
||||
listOf(GalleryDetailUrlSuggestion(gid, token))
|
||||
[GalleryDetailUrlSuggestion(gid, token)]
|
||||
} ?: GalleryPageUrlParser.parse(it, false)?.run {
|
||||
listOf(GalleryPageUrlSuggestion(gid, pToken, page))
|
||||
[GalleryPageUrlSuggestion(gid, pToken, page)]
|
||||
}.orEmpty()
|
||||
},
|
||||
localSearch = false,
|
||||
@@ -509,6 +509,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
) { contentPadding ->
|
||||
val height by collectListThumbSizeAsState()
|
||||
val showPages by Settings.showGalleryPages.collectAsState()
|
||||
val showProgress by Settings.showReadingProgress.collectAsState()
|
||||
val searchBarConnection = remember {
|
||||
val slop = ViewConfiguration.get(contextOf<Context>()).scaledTouchSlop
|
||||
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
||||
@@ -537,6 +538,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
onLongClick = { launch { doGalleryInfoAction(info) } },
|
||||
info = info,
|
||||
showPages = showPages,
|
||||
showProgress = showProgress,
|
||||
modifier = Modifier.height(height),
|
||||
)
|
||||
},
|
||||
@@ -547,6 +549,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
onLongClick = { launch { doGalleryInfoAction(info) } },
|
||||
info = info,
|
||||
showPages = showPages,
|
||||
showProgress = showProgress,
|
||||
)
|
||||
},
|
||||
searchBarOffsetY = { searchBarOffsetY },
|
||||
@@ -577,7 +580,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
|
||||
onExpandChanged = { fabExpanded = it },
|
||||
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) {
|
||||
urlBuilder.setRange(Random.nextInt(100))
|
||||
data.refresh()
|
||||
@@ -640,6 +643,7 @@ private fun getSuitableTitleForUrlBuilder(urlBuilder: ListUrlBuilder, appName: B
|
||||
val canTranslate = Settings.showTagTranslations.value && EhTagDatabase.translatable && EhTagDatabase.initialized
|
||||
wrapTagKeyword(keyword, canTranslate)
|
||||
}
|
||||
MODE_UPLOADER -> "uploader:\"$keyword\""
|
||||
else -> keyword
|
||||
}
|
||||
} else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) {
|
||||
|
||||
@@ -136,6 +136,7 @@ fun AnimatedVisibilityScope.HistoryScreen(navigator: DestinationsNavigator) = Sc
|
||||
val marginH = dimensionResource(id = com.hippo.ehviewer.R.dimen.gallery_list_margin_h)
|
||||
val cardHeight by collectListThumbSizeAsState()
|
||||
val showPages by Settings.showGalleryPages.collectAsState()
|
||||
val showProgress by Settings.showReadingProgress.collectAsState()
|
||||
FastScrollLazyColumn(
|
||||
modifier = Modifier.nestedScroll(searchBarConnection).fillMaxSize(),
|
||||
contentPadding = paddingValues,
|
||||
@@ -161,6 +162,7 @@ fun AnimatedVisibilityScope.HistoryScreen(navigator: DestinationsNavigator) = Sc
|
||||
onLongClick = { launch { doGalleryInfoAction(info) } },
|
||||
info = info,
|
||||
showPages = showPages,
|
||||
showProgress = showProgress,
|
||||
modifier = Modifier.height(cardHeight).padding(horizontal = marginH),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.plus
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
|
||||
@@ -294,7 +294,6 @@ fun SearchBarScreen(
|
||||
item {}
|
||||
items(mSuggestionList, key = { it.keyword.hashCode() * 31 + it.canDelete.hashCode() }) {
|
||||
ListItem(
|
||||
headlineContent = { Text(text = it.keyword) },
|
||||
supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) },
|
||||
leadingContent = it.canOpenDirectly.ifTrueThen {
|
||||
Icon(
|
||||
@@ -312,6 +311,7 @@ fun SearchBarScreen(
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
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.awaitConfirmationOrCancel
|
||||
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.ReadableTime
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
@@ -49,8 +49,8 @@ import moe.tarsin.navigate
|
||||
import moe.tarsin.snackbar
|
||||
import moe.tarsin.string
|
||||
|
||||
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}"
|
||||
private const val RELEASE_URL = "$REPO_URL/releases"
|
||||
const val REPO_URL = "https://codeberg.org/${BuildConfig.REPO_NAME}"
|
||||
const val RELEASE_URL = "$REPO_URL/releases"
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
@@ -123,8 +123,8 @@ fun AnimatedVisibilityScope.AboutScreen(navigator: DestinationsNavigator) = Scre
|
||||
}
|
||||
}
|
||||
|
||||
context(ctx: Context, _: DialogState)
|
||||
suspend fun showNewVersion(release: Release) {
|
||||
context(_: Context, _: DialogState)
|
||||
suspend fun showNewVersion(release: EhRelease) {
|
||||
awaitConfirmationOrCancel(
|
||||
confirmText = R.string.download,
|
||||
title = R.string.new_version_available,
|
||||
@@ -140,7 +140,7 @@ suspend fun showNewVersion(release: Release) {
|
||||
}
|
||||
if (Settings.backupBeforeUpdate.value) {
|
||||
val time = ReadableTime.getFilenamableTime()
|
||||
EhDB.exportDB(ctx, (downloadLocation / "$time.db"))
|
||||
EhDB.exportDB(downloadLocation / "$time.db")
|
||||
}
|
||||
// TODO: Download in the background and show progress in notification
|
||||
val path = AppConfig.tempDir / "update.apk"
|
||||
|
||||
@@ -35,7 +35,6 @@ import com.ehviewer.core.files.sendTo
|
||||
import com.ehviewer.core.files.toOkioPath
|
||||
import com.ehviewer.core.i18n.R
|
||||
import com.ehviewer.core.util.isAtLeastO
|
||||
import com.ehviewer.core.util.isAtLeastSExtension7
|
||||
import com.ehviewer.core.util.launch
|
||||
import com.ehviewer.core.util.logcat
|
||||
import com.ehviewer.core.util.withIOContext
|
||||
@@ -46,6 +45,7 @@ import com.hippo.ehviewer.asMutableState
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.ktor.isCronetAvailable
|
||||
import com.hippo.ehviewer.ui.Screen
|
||||
import com.hippo.ehviewer.ui.main.NavigationIcon
|
||||
import com.hippo.ehviewer.ui.showRestartDialog
|
||||
@@ -63,6 +63,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import java.io.File
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.merge
|
||||
@@ -94,15 +95,15 @@ private fun dumplog(uri: Uri): Unit = with(ctx) {
|
||||
}
|
||||
|
||||
context(ctx: Context)
|
||||
private fun exportDatabase(uri: Uri) {
|
||||
private suspend fun exportDatabase(uri: Uri) {
|
||||
ctx.grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
EhDB.exportDB(ctx, uri.toOkioPath())
|
||||
EhDB.exportDB(uri.toOkioPath())
|
||||
}
|
||||
|
||||
context(ctx: Context)
|
||||
private suspend fun importDatabase(uri: Uri) {
|
||||
ctx.grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
EhDB.importDB(ctx, uri)
|
||||
EhDB.importDB(uri.toOkioPath())
|
||||
}
|
||||
|
||||
@Destination<RootGraph>
|
||||
@@ -185,7 +186,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
|
||||
title = { Text(stringResource(id = R.string.settings_advanced_app_language_title)) },
|
||||
summary = { Text(languages[currentLanguage].orEmpty()) },
|
||||
)
|
||||
if (isAtLeastSExtension7) {
|
||||
if (isCronetAvailable) {
|
||||
val enableCronet = Settings.enableCronet.asMutableState()
|
||||
if (BuildConfig.DEBUG || !enableCronet.value) {
|
||||
SwitchPreference(
|
||||
@@ -214,7 +215,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
|
||||
step = 3,
|
||||
title = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold),
|
||||
summary = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold_summary),
|
||||
state = Settings.hardwareBitmapThreshold.asMutableState(),
|
||||
pref = Settings.hardwareBitmapThreshold,
|
||||
)
|
||||
}
|
||||
SwitchPreference(
|
||||
@@ -289,7 +290,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
|
||||
EhDB.putLocalFavorites(result.galleryInfoList)
|
||||
launchSnackbar(string(R.string.settings_advanced_backup_favorite_start, status))
|
||||
if (result.next != null) {
|
||||
delay(Settings.downloadDelay.value.toLong())
|
||||
delay(Settings.downloadDelay.value.milliseconds)
|
||||
favListUrlBuilder.setIndex(result.next, true)
|
||||
doBackup()
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
|
||||
maxValue = 10,
|
||||
minValue = MIN_SPEED_LEVEL,
|
||||
title = stringResource(id = R.string.settings_download_timeout_speed),
|
||||
state = Settings.timeoutSpeed.asMutableState(),
|
||||
pref = Settings.timeoutSpeed,
|
||||
display = ::speedLevelToSpeed,
|
||||
)
|
||||
val preloadImage = Settings.preloadImage.asMutableState()
|
||||
|
||||
@@ -29,11 +29,11 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import com.ehviewer.core.i18n.R
|
||||
import com.ehviewer.core.network.EhCookieStore
|
||||
import com.ehviewer.core.util.isAtLeastT
|
||||
import com.ehviewer.core.util.launch
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.asMutableState
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
@@ -193,7 +193,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
|
||||
minValue = 20,
|
||||
step = 7,
|
||||
title = stringResource(id = R.string.list_tile_thumb_size),
|
||||
state = Settings.listThumbSize.asMutableState(),
|
||||
pref = Settings.listThumbSize,
|
||||
)
|
||||
SimpleMenuPreferenceInt(
|
||||
title = stringResource(id = R.string.settings_eh_detail_size),
|
||||
@@ -207,13 +207,18 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
|
||||
maxValue = 10,
|
||||
minValue = 1,
|
||||
title = stringResource(id = R.string.settings_eh_thumb_columns),
|
||||
state = Settings.thumbColumns.asMutableState(),
|
||||
pref = Settings.thumbColumns,
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.settings_eh_show_gallery_pages),
|
||||
summary = stringResource(id = R.string.settings_eh_show_gallery_pages_summary),
|
||||
state = Settings.showGalleryPages.asMutableState(),
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.settings_eh_show_reading_progress),
|
||||
summary = stringResource(id = R.string.settings_eh_show_reading_progress_summary),
|
||||
state = Settings.showReadingProgress.asMutableState(),
|
||||
)
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.settings_eh_show_vote_status),
|
||||
state = Settings.showVoteStatus.asMutableState(),
|
||||
@@ -230,7 +235,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
|
||||
minValue = -101,
|
||||
title = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold),
|
||||
summary = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold_summary),
|
||||
state = Settings.commentThreshold.asMutableState(),
|
||||
pref = Settings.commentThreshold,
|
||||
)
|
||||
}
|
||||
if (EhTagDatabase.translatable) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExposedDropdownMenu
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.hippo.ehviewer.ui.settings
|
||||
|
||||
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.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -12,14 +10,15 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ehviewer.core.i18n.R
|
||||
import com.hippo.ehviewer.ui.Screen
|
||||
import com.hippo.ehviewer.ui.main.NavigationIcon
|
||||
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.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.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
@@ -42,10 +41,19 @@ fun AnimatedVisibilityScope.LicenseScreen(navigator: DestinationsNavigator) = Sc
|
||||
libraries = libraries,
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
contentPadding = paddingValues,
|
||||
padding = LibraryDefaults.libraryPadding(licensePadding = LibraryDefaults.chipPadding(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp))),
|
||||
textStyles = LibraryDefaults.libraryTextStyles(licensesTextStyle = MaterialTheme.typography.labelSmall),
|
||||
onLibraryClick = { library ->
|
||||
library.website?.let { openBrowser(it) }
|
||||
badges = LibraryBadges(funding = false),
|
||||
variant = LibrariesVariant.Refined,
|
||||
onActionClick = { lib, kind ->
|
||||
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.stringResource
|
||||
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.rememberIntPrefSliderState
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.util.ProgressDialog
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
@@ -71,19 +73,16 @@ fun SwitchPreference(title: String, summary: String? = null, state: MutableState
|
||||
}
|
||||
|
||||
@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 }) {
|
||||
val sliderState = remember { mutableIntStateOf(state.value) }
|
||||
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 state = rememberIntPrefSliderState(pref, minValue..maxValue, step)
|
||||
IntSliderPreference(
|
||||
state = state,
|
||||
title = { Text(title) },
|
||||
valueRange = minValue..maxValue,
|
||||
valueSteps = step,
|
||||
sliderState = sliderState,
|
||||
enabled = enabled,
|
||||
summary = summary?.let { { Text(it) } },
|
||||
valueText = {
|
||||
RollingNumber(
|
||||
number = display(sliderState.intValue),
|
||||
number = display(state.value),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
length = 5,
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
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.Settings
|
||||
import com.hippo.ehviewer.asMutableState
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.ui.Screen
|
||||
import com.hippo.ehviewer.ui.isAuthenticationSupported
|
||||
import com.hippo.ehviewer.ui.main.NavigationIcon
|
||||
@@ -48,17 +50,17 @@ fun AnimatedVisibilityScope.PrivacyScreen(navigator: DestinationsNavigator) = Sc
|
||||
enabled = isAuthenticationSupported(),
|
||||
)
|
||||
AnimatedVisibility(visible = security.value) {
|
||||
val securityDelay = Settings.securityDelay.asMutableState()
|
||||
val summary = if (securityDelay.value == 0) {
|
||||
val delay by Settings.securityDelay.collectAsState()
|
||||
val summary = if (delay == 0) {
|
||||
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary_immediately)
|
||||
} 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(
|
||||
maxValue = 30,
|
||||
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
|
||||
summary = summary,
|
||||
state = securityDelay,
|
||||
pref = Settings.securityDelay,
|
||||
enabled = isAuthenticationSupported(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.ehviewer.core.i18n.R
|
||||
import com.ehviewer.core.network.EhCookieStore
|
||||
import com.ehviewer.core.util.launch
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewNavigator
|
||||
import com.google.accompanist.web.rememberWebViewState
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.ui.Screen
|
||||
import com.hippo.ehviewer.ui.main.NavigationIcon
|
||||
|
||||
@@ -94,7 +94,7 @@ fun AnnotatedString.normalizeSpan(): AnnotatedString {
|
||||
val italic = filterMerged(SpanStyle(fontStyle = FontStyle.Italic)) { it.fontStyle == FontStyle.Italic }
|
||||
val underline = filterMerged(SpanStyle(textDecoration = TextDecoration.Underline)) { it.textDecoration == TextDecoration.Underline }
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import androidx.compose.material3.DatePickerDefaults
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.DisplayMode
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExposedDropdownMenu
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
@@ -684,10 +685,10 @@ suspend fun awaitSelectItemWithIcon(
|
||||
}
|
||||
itemsIndexed(items) { index, (icon, text) ->
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) },
|
||||
modifier = Modifier.clickable { resume(index) }.padding(horizontal = 8.dp),
|
||||
leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) },
|
||||
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 checkedColor = MaterialTheme.colorScheme.primary
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = text,
|
||||
style = if (checked) textStyle.copy(color = checkedColor) else textStyle,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
trailingContent = checked.ifTrueThen {
|
||||
Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = checkedColor)
|
||||
},
|
||||
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 io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.accept
|
||||
import io.ktor.client.request.bearerAuth
|
||||
import io.ktor.client.request.prepareGet
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.http.ContentType
|
||||
@@ -21,17 +20,18 @@ import kotlin.time.Instant
|
||||
import kotlinx.io.asSource
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.Path
|
||||
import tachiyomi.data.release.GithubArtifacts
|
||||
import tachiyomi.data.release.GithubCommitComparison
|
||||
import tachiyomi.data.release.GithubRelease
|
||||
import tachiyomi.data.release.GithubRepo
|
||||
import tachiyomi.data.release.GithubWorkflowRuns
|
||||
import tachiyomi.data.release.Artifacts
|
||||
import tachiyomi.data.release.CommitComparison
|
||||
import tachiyomi.data.release.Release
|
||||
import tachiyomi.data.release.Repository
|
||||
import tachiyomi.data.release.WorkflowRuns
|
||||
import tachiyomi.data.release.downloadLink
|
||||
|
||||
private const val API_URL = "https://api.github.com/repos/${BuildConfig.REPO_NAME}"
|
||||
private const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
|
||||
const val API_URL = "https://codeberg.org/api/v1/repos/${BuildConfig.REPO_NAME}"
|
||||
const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
|
||||
|
||||
object AppUpdater {
|
||||
suspend fun checkForUpdate(forceCheck: Boolean = false): Release? {
|
||||
suspend fun checkForUpdate(forceCheck: Boolean = false): EhRelease? {
|
||||
val now = Clock.System.now()
|
||||
val last = Instant.fromEpochSeconds(Settings.lastUpdateTime)
|
||||
val interval = Settings.updateIntervalDays.value
|
||||
@@ -39,31 +39,30 @@ object AppUpdater {
|
||||
Settings.lastUpdateTime = now.epochSeconds
|
||||
if (Settings.useCIUpdateChannel.value) {
|
||||
val curSha = BuildConfig.COMMIT_SHA
|
||||
val branch = ghStatement(API_URL).executeAndParseAs<GithubRepo>().defaultBranch
|
||||
val workflowRunsUrl = "$API_URL/actions/workflows/ci.yml/runs?branch=$branch&event=push&status=success&per_page=1"
|
||||
val workflowRun = ghStatement(workflowRunsUrl).executeAndParseAs<GithubWorkflowRuns>().workflowRuns[0]
|
||||
val shortSha = workflowRun.headSha.take(7)
|
||||
val branch = statement(API_URL).executeAndParseAs<Repository>().defaultBranch
|
||||
val run = statement("$API_URL/actions/runs?event=push&status=success&ref=refs/heads/$branch").executeAndParseAs<WorkflowRuns>().workflowRuns[0]
|
||||
val shortSha = run.headSha.take(7)
|
||||
if (shortSha != curSha) {
|
||||
val artifacts = ghStatement(workflowRun.artifactsUrl).executeAndParseAs<GithubArtifacts>()
|
||||
val archiveUrl = artifacts.getDownloadLink()
|
||||
val artifacts = statement("$API_URL/actions/runs/${run.id}/artifacts").executeAndParseAs<Artifacts>()
|
||||
val archiveUrl = artifacts.downloadLink
|
||||
val changelog = runSuspendCatching {
|
||||
val commitComparisonUrl = "$API_URL/compare/$curSha...$shortSha"
|
||||
val result = ghStatement(commitComparisonUrl).executeAndParseAs<GithubCommitComparison>()
|
||||
val result = statement(commitComparisonUrl).executeAndParseAs<CommitComparison>()
|
||||
// TODO: Prettier format, Markdown?
|
||||
result.commits.joinToString("\n") { commit ->
|
||||
"${commit.commit.message.takeWhile { it != '\n' }} (@${commit.commit.author.name})"
|
||||
}
|
||||
}.getOrDefault(workflowRun.title)
|
||||
return Release(shortSha, changelog, archiveUrl)
|
||||
}.getOrDefault(run.title)
|
||||
return EhRelease(shortSha, changelog, archiveUrl)
|
||||
}
|
||||
} else {
|
||||
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 description = release.info
|
||||
val downloadUrl = release.getDownloadLink()
|
||||
if (latestVersion != curVersion) {
|
||||
return Release(latestVersion, description, downloadUrl)
|
||||
return EhRelease(latestVersion, description, downloadUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +74,7 @@ object AppUpdater {
|
||||
timeoutBySpeed(
|
||||
url,
|
||||
{
|
||||
ghStatement(url) {
|
||||
statement(url) {
|
||||
// https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#get-a-release-asset
|
||||
if (!isZip) accept(ContentType.Application.OctetStream)
|
||||
it()
|
||||
@@ -98,22 +97,14 @@ object AppUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun ghStatement(
|
||||
suspend inline fun statement(
|
||||
url: String,
|
||||
builder: HttpRequestBuilder.() -> Unit = {},
|
||||
) = ktorClient.prepareGet(url) {
|
||||
bearerAuth(GithubTokenParts.joinToString("_"))
|
||||
apply(builder)
|
||||
}
|
||||
|
||||
private val GithubTokenParts = arrayOf(
|
||||
"github",
|
||||
"pat",
|
||||
"11A4H2ACI0iGDuL1O6wPYW",
|
||||
"OTFg8xaCNUwR1NHaJE1AT3LoYPfz6bouI7E7ReLf8GjIRFHCL5UsHL9EnWP",
|
||||
)
|
||||
|
||||
data class Release(
|
||||
data class EhRelease(
|
||||
val version: String,
|
||||
val changelog: String,
|
||||
val downloadLink: String,
|
||||
|
||||
@@ -39,18 +39,14 @@ object AppConfig {
|
||||
private const val CRASH = "crash"
|
||||
private const val TAG_TRANSLATIONS = "tag-translations"
|
||||
|
||||
private val abi = Build.SUPPORTED_ABIS[0].takeIf {
|
||||
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a")
|
||||
} ?: "universal"
|
||||
private val abi = Build.SUPPORTED_ABIS[0].takeIf { it in ["arm64-v8a", "x86_64", "armeabi-v7a"] } ?: "universal"
|
||||
|
||||
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 isSnapshot = "SNAPSHOT" in BuildConfig.VERSION_NAME
|
||||
|
||||
private val externalAppDir: File?
|
||||
get() {
|
||||
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
|
||||
|
||||
@@ -22,8 +22,8 @@ import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PersistableBundle
|
||||
import android.view.textclassifier.TextClassifier
|
||||
import androidx.core.os.persistableBundleOf
|
||||
import com.ehviewer.core.i18n.R
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
import moe.tarsin.tip
|
||||
@@ -33,7 +33,9 @@ fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText(null, text).apply {
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -79,9 +79,9 @@ object FileUtils {
|
||||
*/
|
||||
// From https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/FileUtils.java;l=1142;drc=7b647e4ea0e92f33c19b315eaed364ee067ba0aa
|
||||
fun sanitizeFilename(name: String): String {
|
||||
if (name.isEmpty() || "." == name || ".." == name) {
|
||||
return "(invalid)"
|
||||
}
|
||||
// Windows does not support filenames ending with a space or a period
|
||||
val name = name.trimEnd(' ', '.')
|
||||
check(name.isNotEmpty()) { "Invalid filename" }
|
||||
return buildString(name.length) {
|
||||
name.forEach {
|
||||
if (isValidFatFilenameChar(it)) {
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalLocale
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.ehviewer.core.i18n.R
|
||||
import com.ehviewer.core.util.toLocalDateTime
|
||||
import java.util.Locale
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
@@ -127,7 +127,7 @@ object ReadableTime {
|
||||
val timeDate = time.toLocalDateTime(timeZone).date
|
||||
val nowYear = nowDate.year
|
||||
val timeYear = timeDate.year
|
||||
val isZh = Locale.getDefault().language == "zh"
|
||||
val isZh = LocalLocale.current.language == "zh"
|
||||
if (nowYear == timeYear) {
|
||||
if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExposedDropdownMenu
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
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.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.unit.LayoutDirection
|
||||
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.defaultMaxTickCount
|
||||
import com.ehviewer.core.ui.util.HapticFeedbackType
|
||||
@@ -29,7 +31,7 @@ import com.ehviewer.core.ui.util.rememberHapticFeedback
|
||||
@Composable
|
||||
fun ChapterNavigator(
|
||||
isRtl: Boolean,
|
||||
currentPage: Int,
|
||||
slider: IntSliderState,
|
||||
totalPages: Int,
|
||||
onSliderValueChange: (Int) -> Unit,
|
||||
containerColor: Color,
|
||||
@@ -40,14 +42,13 @@ fun ChapterNavigator(
|
||||
modifier = Modifier.padding(horizontal = horizontalPadding).clip(CircleShape).background(containerColor).padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = "$currentPage")
|
||||
val steps = totalPages - 2
|
||||
RollingNumber(slider.value, length = "$totalPages".length)
|
||||
val maxTickCount = defaultMaxTickCount()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
if (steps < maxTickCount) {
|
||||
if (slider.state.steps < maxTickCount) {
|
||||
val sliderDragged by interactionSource.collectIsDraggedAsState()
|
||||
val hapticFeedback = rememberHapticFeedback()
|
||||
LaunchedEffect(currentPage) {
|
||||
LaunchedEffect(slider.value) {
|
||||
if (sliderDragged) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
|
||||
}
|
||||
@@ -55,13 +56,11 @@ fun ChapterNavigator(
|
||||
}
|
||||
Slider(
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
|
||||
value = currentPage,
|
||||
valueRange = 1..totalPages,
|
||||
steps = steps,
|
||||
state = slider,
|
||||
onValueChange = onSliderValueChange,
|
||||
maxTickCount = maxTickCount,
|
||||
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.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ehviewer.core.ui.component.IntSliderState
|
||||
import com.hippo.ehviewer.ui.main.NavigationIcon
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@@ -35,7 +36,7 @@ fun BoxScope.ReaderAppBars(
|
||||
title: String,
|
||||
isRtl: Boolean,
|
||||
showSeekBar: Boolean,
|
||||
currentPage: Int,
|
||||
slider: IntSliderState,
|
||||
totalPages: Int,
|
||||
onSliderValueChange: (Int) -> Unit,
|
||||
onClickSettings: () -> Unit,
|
||||
@@ -65,7 +66,7 @@ fun BoxScope.ReaderAppBars(
|
||||
if (showSeekBar && totalPages > 1) {
|
||||
ChapterNavigator(
|
||||
isRtl = isRtl,
|
||||
currentPage = currentPage,
|
||||
slider = slider,
|
||||
totalPages = totalPages,
|
||||
onSliderValueChange = onSliderValueChange,
|
||||
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() {
|
||||
override val originalRegions = listOf(
|
||||
override val originalRegions = [
|
||||
Region(
|
||||
rect = Rect(0f, 0f, 0.33f, 1f),
|
||||
type = NavigationRegion.NEXT,
|
||||
@@ -27,5 +27,5 @@ object EdgeNavigation : ViewerNavigation() {
|
||||
rect = Rect(0.66f, 0f, 1f, 1f),
|
||||
type = NavigationRegion.NEXT,
|
||||
),
|
||||
)
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||
* +---+---+---+
|
||||
*/
|
||||
object KindlishNavigation : ViewerNavigation() {
|
||||
override val originalRegions = listOf(
|
||||
override val originalRegions = [
|
||||
Region(
|
||||
rect = Rect(0.33f, 0.33f, 1f, 1f),
|
||||
type = NavigationRegion.NEXT,
|
||||
@@ -23,5 +23,5 @@ object KindlishNavigation : ViewerNavigation() {
|
||||
rect = Rect(0f, 0.33f, 0.33f, 1f),
|
||||
type = NavigationRegion.PREV,
|
||||
),
|
||||
)
|
||||
]
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||
* +---+---+---+
|
||||
*/
|
||||
object LNavigation : ViewerNavigation() {
|
||||
override val originalRegions = listOf(
|
||||
override val originalRegions = [
|
||||
Region(
|
||||
rect = Rect(0f, 0.33f, 0.33f, 0.66f),
|
||||
type = NavigationRegion.PREV,
|
||||
@@ -31,5 +31,5 @@ object LNavigation : ViewerNavigation() {
|
||||
rect = Rect(0f, 0.66f, 1f, 1f),
|
||||
type = NavigationRegion.NEXT,
|
||||
),
|
||||
)
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||
* +---+---+---+
|
||||
*/
|
||||
object RightAndLeftNavigation : ViewerNavigation() {
|
||||
override val originalRegions = listOf(
|
||||
override val originalRegions = [
|
||||
Region(
|
||||
rect = Rect(0f, 0f, 0.33f, 1f),
|
||||
type = NavigationRegion.LEFT,
|
||||
@@ -23,5 +23,5 @@ object RightAndLeftNavigation : ViewerNavigation() {
|
||||
rect = Rect(0.66f, 0f, 1f, 1f),
|
||||
type = NavigationRegion.RIGHT,
|
||||
),
|
||||
)
|
||||
]
|
||||
}
|
||||
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user