Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3cb6d2782 | ||
|
|
45be957df5 | ||
|
|
8e45014e3f | ||
|
|
d84e948352 | ||
|
|
68bd835038 | ||
|
|
c297bebea1 | ||
|
|
6c0a823216 | ||
|
|
067a701bdc | ||
|
|
c3fb008c92 | ||
|
|
bc4321da2e | ||
|
|
8dec040803 | ||
|
|
29e4e77c44 | ||
|
|
11429ef7fe | ||
|
|
f5fcd96a1e | ||
|
|
19120038c5 | ||
|
|
9021f28ead | ||
|
|
62934f72e3 | ||
|
|
6c534e1049 | ||
|
|
f80cd90792 | ||
|
|
3999c0c114 | ||
|
|
59399453c4 | ||
|
|
694bb19e2a | ||
|
|
9928ecd4d9 | ||
|
|
2c5293af6e | ||
|
|
9569ca774d | ||
|
|
3a04460926 | ||
|
|
acb4fb6166 | ||
|
|
7d137bb016 | ||
|
|
8c2ed37b72 | ||
|
|
749b230250 | ||
|
|
23531cb6e0 | ||
|
|
0ac11dbe0b | ||
|
|
9b0e017542 | ||
|
|
f2f5fcacf9 | ||
|
|
52ded265f3 | ||
|
|
e77f905c4a | ||
|
|
2d14d34d53 | ||
|
|
78facda18a | ||
|
|
1fe1b43015 | ||
|
|
63164dc70a | ||
|
|
0d39092a7f | ||
|
|
c50db8a327 | ||
|
|
7e107cc5e2 | ||
|
|
260949e3a7 | ||
|
|
d9ec108721 | ||
|
|
1e97407936 | ||
|
|
73afe05a18 | ||
|
|
23475de932 | ||
|
|
6fb2172c2c | ||
|
|
9226b71119 | ||
|
|
58e38b58bf | ||
|
|
db0d110627 | ||
|
|
08812bf7ff | ||
|
|
daf7f8ffca | ||
|
|
74564fa07f | ||
|
|
fc51d3fc81 | ||
|
|
c5d5b8abd2 | ||
|
|
193badccb8 | ||
|
|
2653d0568a | ||
|
|
adf5a65371 | ||
|
|
5b59dabc66 | ||
|
|
f7259c5b40 | ||
|
|
6f9fceaef8 | ||
|
|
6a64ff51ed | ||
|
|
ea9d9cfba7 | ||
|
|
75c6561896 | ||
|
|
8f8db82e9d | ||
|
|
a114a8ac72 | ||
|
|
2c6216057f | ||
|
|
c5cbcf7418 | ||
|
|
5746e3bb57 | ||
|
|
fd3c87be22 | ||
|
|
866fff3e45 | ||
|
|
06df7befb6 | ||
|
|
8021f86473 | ||
|
|
c9919a8b39 | ||
|
|
386015a9ec | ||
|
|
cbce98b34e | ||
|
|
f69521fc1a | ||
|
|
01b4054368 | ||
|
|
e07aab4434 | ||
|
|
18caa87512 | ||
|
|
5676de24da | ||
|
|
57bbb1fe38 | ||
|
|
dcbfbc2e5f | ||
|
|
e1f473d301 | ||
|
|
a33256d29f | ||
|
|
6a81877b09 |
No files matched your search
+42
-199
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Spotless Check
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
oss:
|
||||
default:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -80,123 +80,46 @@ jobs:
|
||||
key: cmake-default-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-default-
|
||||
|
||||
- name: Build OSS
|
||||
run: ./gradlew assembleDefaultOssRelease
|
||||
- name: Build
|
||||
run: ./gradlew assembleDefaultRelease
|
||||
|
||||
- name: Upload OSS ARM64
|
||||
- name: Upload ARM64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk
|
||||
name: default-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload OSS x86_64
|
||||
- name: Upload x86_64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk
|
||||
name: default-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
|
||||
|
||||
- name: Upload OSS ARM32
|
||||
- name: Upload ARM32
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk
|
||||
name: default-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload OSS x86
|
||||
- name: Upload x86
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk
|
||||
name: default-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-x86-release.apk
|
||||
|
||||
- name: Upload OSS mapping
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultOssRelease/mapping.txt
|
||||
name: default-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultRelease/mapping.txt
|
||||
|
||||
- name: Upload OSS native debug symbols
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip
|
||||
name: default-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
|
||||
|
||||
gms:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./app/src/main/rust"
|
||||
shared-key: ${{ runner.os }}
|
||||
save-if: "false"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: app/.cxx
|
||||
key: cmake-default-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-default-
|
||||
|
||||
- name: Install Build Tools
|
||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "build-tools;35.0.0-rc1"
|
||||
|
||||
- name: Build GMS
|
||||
run: ./gradlew assembleDefaultGmsRelease
|
||||
|
||||
- name: Upload GMS ARM64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload GMS x86_64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk
|
||||
|
||||
- name: Upload GMS ARM32
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload GMS x86
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk
|
||||
|
||||
- name: Upload GMS mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultGmsRelease/mapping.txt
|
||||
|
||||
- name: Upload GMS native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip
|
||||
|
||||
marshmallow-oss:
|
||||
marshmallow:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -231,121 +154,41 @@ jobs:
|
||||
key: cmake-marshmallow-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-marshmallow-
|
||||
|
||||
- name: Install Build Tools
|
||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "build-tools;35.0.0-rc1"
|
||||
- name: Build
|
||||
run: ./gradlew assembleMarshmallowRelease
|
||||
|
||||
- name: Build Marshmallow OSS
|
||||
run: ./gradlew assembleMarshmallowOssRelease
|
||||
|
||||
- name: Upload Marshmallow OSS ARM64
|
||||
- name: Upload ARM64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk
|
||||
name: marshmallow-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS x86_64
|
||||
- name: Upload x86_64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk
|
||||
name: marshmallow-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS ARM32
|
||||
- name: Upload ARM32
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk
|
||||
name: marshmallow-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS x86
|
||||
- name: Upload x86
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk
|
||||
name: marshmallow-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS mapping
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowOssRelease/mapping.txt
|
||||
name: marshmallow-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowRelease/mapping.txt
|
||||
|
||||
- name: Upload Marshmallow OSS native debug symbols
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip
|
||||
|
||||
marshmallow-gms:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./app/src/main/rust"
|
||||
shared-key: ${{ runner.os }}
|
||||
save-if: "false"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: app/.cxx
|
||||
key: cmake-marshmallow-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-marshmallow-
|
||||
|
||||
- name: Install Build Tools
|
||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "build-tools;35.0.0-rc1"
|
||||
|
||||
- name: Build Marshmallow GMS
|
||||
run: ./gradlew assembleMarshmallowGmsRelease
|
||||
|
||||
- name: Upload Marshmallow GMS ARM64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-gms-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload Marshmallow GMS x86_64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-gms-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86_64-release.apk
|
||||
|
||||
- name: Upload Marshmallow GMS ARM32
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-gms-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload Marshmallow GMS x86
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-gms-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86-release.apk
|
||||
|
||||
- name: Upload Marshmallow GMS mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-gms-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowGmsRelease/mapping.txt
|
||||
|
||||
- name: Upload Marshmallow GMS native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-gms-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip
|
||||
name: marshmallow-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
|
||||
@@ -6,8 +6,7 @@ on:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -30,11 +29,9 @@ jobs:
|
||||
- name: CMake Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
app/.cxx
|
||||
app/src/main/cpp/nettle/nettle
|
||||
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: ${{ runner.os }}-cmake-
|
||||
path: app/.cxx
|
||||
key: cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
@@ -43,71 +40,39 @@ jobs:
|
||||
|
||||
- name: Rename Apks
|
||||
run: |
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-universal-release.apk EhViewer-${{ github.ref_name }}-default-oss-universal.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/defaultOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
|
||||
mv app/build/outputs/apk/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-x86-release.apk EhViewer-${{ github.ref_name }}-default-x86.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/defaultGms/release/app-default-gms-universal-release.apk EhViewer-${{ github.ref_name }}-default-gms-universal.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/defaultGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
|
||||
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/marshmallowOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
|
||||
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/marshmallowGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
|
||||
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-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-x86.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-oss-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
|
||||
EhViewer-${{ github.ref_name }}-default-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-default-x86.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 }}-default-gms-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
|
||||
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
|
||||
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
|
||||
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-x86.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
|
||||
+59
-1
@@ -1,5 +1,63 @@
|
||||
# 变更日志
|
||||
|
||||
## [1.11.3] - 2024-04-06
|
||||
|
||||
### 改进
|
||||
|
||||
* 优化预加载逻辑和图片加载顺序
|
||||
* 优化画廊详情和评论页面 UI
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 某些模式下搜索结果不显示画廊语言 [#923](https://github.com/FooIbar/EhViewer/issues/923)
|
||||
* 某些情况下在搜索结果中直接阅读画廊后崩溃 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
||||
* 更新至最新版本后仍提示有新版本 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
||||
* 从旧版本升级时包含不兼容数据导致崩溃/无法导出数据 [#940](https://github.com/FooIbar/EhViewer/issues/940) [#974](https://github.com/FooIbar/EhViewer/issues/974)
|
||||
* Android 10 以下的设备打开 设置-隐私 时崩溃 [#953](https://github.com/FooIbar/EhViewer/issues/953)
|
||||
* 下载标签无法拖动
|
||||
* 刷新 和 刷新(原图) 无法在已下载的图片上使用
|
||||
* 临时 IP 封禁错误显示为解析失败
|
||||
* 某些情况下下载时闪退 [#980](https://github.com/FooIbar/EhViewer/issues/980)
|
||||
* 某些设备上下载列表中的缩略图无法显示 [#977](https://github.com/FooIbar/EhViewer/issues/977)
|
||||
|
||||
|
||||
## [1.11.2] - 2024-03-17
|
||||
|
||||
### 重大变更
|
||||
|
||||
* Default 变种最低支持版本更改为 Android 8.0,重新添加了最低支持 Android 6.0 的 Marshmallow 变种
|
||||
|
||||
### 改进
|
||||
|
||||
* 重新添加了删除快速搜索/下载标签时的确认对话框
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 收藏页面 搜索栏中文标签转英文异常 [#902](https://github.com/FooIbar/EhViewer/issues/902)
|
||||
* 在收藏或者取消收藏之后,收藏按钮会无反应 [#908](https://github.com/FooIbar/EhViewer/issues/908)
|
||||
* 横屏页面下阅读设置UI重合 [#909](https://github.com/FooIbar/EhViewer/issues/909)
|
||||
* Search bar does not work properly in Japanese [#906](https://github.com/FooIbar/EhViewer/issues/906)
|
||||
* 搜索结果中的 Disowned 画廊不显示上传者
|
||||
* 使用 CI 频道更新时,有时会获取到错误的更新链接
|
||||
|
||||
|
||||
## [1.11.1] - 2024-03-12
|
||||
|
||||
### 重大变更
|
||||
|
||||
* 由于 E 站强制要求人机验证,仅支持通过网页登录
|
||||
* 默认支持 Android 6,不再提供 Marshmallow 变种
|
||||
* 集成 Cronet 库,不再提供 GMS 变种
|
||||
|
||||
### 改进
|
||||
|
||||
* 支持在排序下载列表前按下载标签分组
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 下载画廊时崩溃 [#893](https://github.com/FooIbar/EhViewer/issues/893)
|
||||
|
||||
|
||||
## [1.11.0] - 2024-03-09
|
||||
|
||||
### 重大变更
|
||||
@@ -7,6 +65,7 @@
|
||||
* 由于 E 站 Cookie 策略变更,不再支持 Cookie 登录
|
||||
* 登录时自动选择 exhentai (如果可用)
|
||||
* 不再提供默认下载路径
|
||||
* 手动排序更改为以添加时间/上传时间/标题/下载标签/页数对下载列表排序
|
||||
* 由于数据库结构变更,此版本导出的数据将无法被旧版本导入
|
||||
|
||||
### 新功能
|
||||
@@ -19,7 +78,6 @@
|
||||
* 随机打开本地收藏/下载中的画廊 [#695](https://github.com/FooIbar/EhViewer/issues/695)
|
||||
* 随机在搜索结果中跳转
|
||||
* 以网格模式查看下载列表
|
||||
* 以添加时间/上传时间/标题/下载标签/页数对下载列表排序
|
||||
* 本地收藏/历史/下载中的标签搜索 [#860](https://github.com/FooIbar/EhViewer/issues/860)
|
||||
|
||||
### 改进
|
||||
|
||||
@@ -66,16 +66,10 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
|
||||
|
||||
# Download
|
||||
|
||||
| Flavor | Feature |
|
||||
|-------------|-----------------------------------|
|
||||
| Default | Android 9+, full support |
|
||||
| Marshmallow | Android 6.0-8.1, limited support* |
|
||||
| OSS | FOSS dependencies** |
|
||||
| GMS | Proprietary GMS component |
|
||||
|
||||
\* May not support some archives
|
||||
|
||||
** Fall back to OkHttp on devices that don't support [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine)
|
||||
| Flavor | Feature |
|
||||
|-------------|----------------------------------|
|
||||
| Default | Android 8.0+, full support |
|
||||
| Marshmallow | Android 6.0-7.1, limited support |
|
||||
|
||||
<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"/>
|
||||
|
||||
+30
-31
@@ -60,10 +60,10 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 180051
|
||||
versionName = "1.11.0"
|
||||
versionCode = 180054
|
||||
versionName = "1.11.3"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
@@ -80,6 +80,7 @@ android {
|
||||
"nb-rNO",
|
||||
),
|
||||
)
|
||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||
ndk {
|
||||
@@ -87,27 +88,14 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += listOf("api", "oss")
|
||||
flavorDimensions += "api"
|
||||
|
||||
productFlavors {
|
||||
create("default") {
|
||||
dimension = "api"
|
||||
}
|
||||
create("default")
|
||||
create("marshmallow") {
|
||||
dimension = "api"
|
||||
minSdk = 23
|
||||
applicationIdSuffix = ".m"
|
||||
versionNameSuffix = "-M"
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
}
|
||||
create("oss") {
|
||||
dimension = "oss"
|
||||
}
|
||||
create("gms") {
|
||||
dimension = "oss"
|
||||
versionNameSuffix = "-gms"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +105,10 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
@@ -147,16 +139,12 @@ android {
|
||||
|
||||
lint {
|
||||
disable += setOf("MissingTranslation", "MissingQuantity")
|
||||
fatal += setOf("NewApi", "InlineApi")
|
||||
fatal += setOf("NewApi", "InlinedApi")
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/**"
|
||||
excludes += "/okhttp3/**"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
dex {
|
||||
useLegacyPackaging = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +177,17 @@ android {
|
||||
namespace = "com.hippo.ehviewer"
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().withBuildType("release")) {
|
||||
it.packaging.resources.excludes.addAll(
|
||||
"/META-INF/**",
|
||||
"/kotlin/**",
|
||||
"**.txt",
|
||||
"**.bin",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@@ -230,10 +229,6 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.splitties)
|
||||
|
||||
// https://square.github.io/okhttp/changelogs/changelog/
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.bundles.okhttp)
|
||||
|
||||
implementation(libs.okio.jvm)
|
||||
|
||||
implementation(libs.logcat)
|
||||
@@ -257,7 +252,7 @@ dependencies {
|
||||
|
||||
implementation(libs.telephoto.zoomable)
|
||||
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(libs.ktor.client.core)
|
||||
|
||||
implementation(libs.bundles.kotlinx.serialization)
|
||||
|
||||
@@ -267,7 +262,10 @@ dependencies {
|
||||
|
||||
coreLibraryDesugaring(libs.desugar)
|
||||
|
||||
"gmsImplementation"(libs.bundles.cronet)
|
||||
implementation(libs.cronet.embedded)
|
||||
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -277,6 +275,7 @@ kotlin {
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.generateKotlin", "true")
|
||||
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
|
||||
Vendored
+7
@@ -11,10 +11,17 @@
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
# Workaround for https://issuetracker.google.com/331556916
|
||||
-keepclassmembers class androidx.biometric.BiometricManager {
|
||||
public int canAuthenticate(int);
|
||||
}
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
|
||||
-keepattributes LineNumberTable
|
||||
|
||||
-allowaccessmodification
|
||||
-repackageclasses
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import com.google.android.gms.net.CronetProviderInstaller
|
||||
import org.chromium.net.CronetProvider
|
||||
import splitties.init.appCtx
|
||||
|
||||
val isCronetAvailable: Boolean
|
||||
get() = CronetProvider.getAllProviders(appCtx).any { it.isEnabled }.also {
|
||||
if (!it) {
|
||||
CronetProviderInstaller.installProvider(appCtx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
From d1db03ebd9989d10a5a905b792b712c6487a0057 Mon Sep 17 00:00:00 2001
|
||||
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||
Date: Sat, 24 Feb 2024 12:03:15 +0000
|
||||
Subject: [PATCH] Don't build other binaries
|
||||
|
||||
---
|
||||
CMakeLists.txt | 8 ++++----
|
||||
1 file changed, 4 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index a346dd02..52b5d0d8 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -1567,7 +1567,7 @@ endif()
|
||||
# xzdec and lzmadec
|
||||
#############################################################################
|
||||
|
||||
-if(HAVE_DECODERS AND (NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900))
|
||||
+if(0)
|
||||
foreach(XZDEC xzdec lzmadec)
|
||||
add_executable("${XZDEC}"
|
||||
src/common/sysdefs.h
|
||||
@@ -1624,7 +1624,7 @@ endif()
|
||||
# lzmainfo
|
||||
#############################################################################
|
||||
|
||||
-if(HAVE_DECODERS AND (NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900))
|
||||
+if(0)
|
||||
add_executable(lzmainfo
|
||||
src/common/sysdefs.h
|
||||
src/common/tuklib_common.h
|
||||
@@ -1680,7 +1680,7 @@ endif()
|
||||
# xz
|
||||
#############################################################################
|
||||
|
||||
-if(NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900)
|
||||
+if(0)
|
||||
add_executable(xz
|
||||
src/common/mythread.h
|
||||
src/common/sysdefs.h
|
||||
@@ -1914,7 +1914,7 @@ endif()
|
||||
# Scripts
|
||||
#############################################################################
|
||||
|
||||
-if(UNIX)
|
||||
+if(0)
|
||||
# NOTE: This isn't as sophisticated as in the Autotools build which
|
||||
# uses posix-shell.m4 but hopefully this doesn't need to be either.
|
||||
# CMake likely won't be used on as many (old) obscure systems as the
|
||||
--
|
||||
2.34.1
|
||||
|
||||
@@ -20,8 +20,9 @@ option(BUILD_TESTING OFF)
|
||||
|
||||
FetchContent_Declare(
|
||||
liblzma
|
||||
GIT_REPOSITORY https://github.com/FooIbar/xz.git
|
||||
GIT_REPOSITORY https://git.tukaani.org/xz.git
|
||||
GIT_TAG v5.6.1
|
||||
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch || cd .
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(liblzma)
|
||||
|
||||
@@ -127,8 +127,6 @@ static long archive_map_entries_index(archive_ctx *ctx, bool sort) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
if (sort)
|
||||
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||
return count;
|
||||
@@ -171,8 +169,6 @@ static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
if (archive_entry_is_playable(ctx->entry))
|
||||
count++;
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -306,6 +302,11 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
} else {
|
||||
entryCount = archive_list_all_entries(ctx);
|
||||
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
|
||||
if (!entryCount) {
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
archive_release_ctx(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We must read through the file|vm then we can know whether it is encrypted
|
||||
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.SearchBarDefaults.InputFieldHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun SearchBarInputField(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val textColor = LocalTextStyle.current.color.takeOrElse {
|
||||
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
state = state,
|
||||
modifier = modifier.sizeIn(
|
||||
minWidth = SearchBarMinWidth,
|
||||
maxWidth = SearchBarMaxWidth,
|
||||
minHeight = InputFieldHeight,
|
||||
).focusRequester(focusRequester).onFocusChanged {
|
||||
if (it.isFocused) onExpandedChange(true)
|
||||
}.fillMaxWidth(),
|
||||
enabled = enabled,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
|
||||
cursorBrush = SolidColor(colors.cursorColor),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
onKeyboardAction = { onSearch() },
|
||||
interactionSource = interactionSource,
|
||||
decorator = @Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
innerTextField = innerTextField,
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon?.let { leading -> { Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() } } },
|
||||
trailingIcon = trailingIcon?.let { trailing -> { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } } },
|
||||
shape = SearchBarDefaults.inputFieldShape,
|
||||
colors = colors,
|
||||
contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
|
||||
container = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val shouldClearFocus = !expanded && focused
|
||||
LaunchedEffect(expanded) {
|
||||
if (shouldClearFocus) {
|
||||
// Not strictly needed according to the motion spec, but since the animation
|
||||
// already has a delay, this works around b/261632544.
|
||||
delay(AnimationDelayMillis.toLong())
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val SearchBarIconOffsetX = 4.dp
|
||||
val SearchBarMinWidth = 360.dp
|
||||
val SearchBarMaxWidth = 720.dp
|
||||
val SearchBarHorizontalPadding = 16.dp
|
||||
const val AnimationDelayMillis = 100
|
||||
@@ -1,591 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.MutatorMutex
|
||||
import androidx.compose.foundation.interaction.Interaction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.MutableWindowInsets
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.GenericShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.SearchBarDefaults.InputFieldHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableFloatState
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.geometry.toRect
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.semantics.onClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.lerp
|
||||
import androidx.compose.ui.unit.offset
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.compose.ui.zIndex
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sign
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* <a href="https://m3.material.io/components/search/overview" class="external" target="_blank">Material Design search</a>.
|
||||
*
|
||||
* A search bar represents a floating search field that allows users to enter a keyword or phrase
|
||||
* and get relevant information. It can be used as a way to navigate through an app via search
|
||||
* queries.
|
||||
*
|
||||
* An active search bar expands into a search "view" and can be used to display dynamic suggestions.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* A [SearchBar] expands to occupy the entirety of its allowed size when active. For full-screen
|
||||
* behavior as specified by Material guidelines, parent layouts of the [SearchBar] must not pass
|
||||
* any [Constraints] that limit its size, and the host activity should set
|
||||
* `WindowCompat.setDecorFitsSystemWindows(window, false)`.
|
||||
*
|
||||
* If this expansion behavior is undesirable, for example on large tablet screens, [DockedSearchBar]
|
||||
* can be used instead.
|
||||
*
|
||||
* @param state the query text to be shown in the search bar's input field
|
||||
* @param onSearch the callback to be invoked when the input service triggers the [ImeAction.Search]
|
||||
* action.
|
||||
* @param active whether this search bar is active
|
||||
* @param onActiveChange the callback to be invoked when this search bar's active state is changed
|
||||
* @param modifier the [Modifier] to be applied to this search bar
|
||||
* @param enabled controls the enabled state of this search bar. When `false`, this component will
|
||||
* not respond to user input, and it will appear visually disabled and disabled to accessibility
|
||||
* services.
|
||||
* @param placeholder the placeholder to be displayed when the search bar's query is empty.
|
||||
* @param leadingIcon the leading icon to be displayed at the beginning of the search bar container
|
||||
* @param trailingIcon the trailing icon to be displayed at the end of the search bar container
|
||||
* @param shape the shape of this search bar when it is not [active]. When [active], the shape will
|
||||
* always be [SearchBarDefaults.fullScreenShape].
|
||||
* @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
|
||||
* in different states. See [SearchBarDefaults.colors].
|
||||
* @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
|
||||
* translucent primary color overlay is applied on top of the container. A higher tonal elevation
|
||||
* value will result in a darker color in light theme and lighter color in dark theme. See also:
|
||||
* [Surface].
|
||||
* @param shadowElevation the elevation for the shadow below the search bar
|
||||
* @param windowInsets the window insets that the search bar will respect
|
||||
* @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
|
||||
* emitting [Interaction]s for this search bar. You can use this to change the search bar's
|
||||
* appearance or preview the search bar in different states. Note that if `null` is provided,
|
||||
* interactions will still happen internally.
|
||||
* @param content the content of this search bar that will be displayed below the input field
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
active: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
||||
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
|
||||
shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val animationProgress = remember { Animatable(initialValue = if (active) 1f else 0f) }
|
||||
val finalBackProgress = remember { mutableFloatStateOf(Float.NaN) }
|
||||
val firstBackEvent = remember { mutableStateOf<BackEventCompat?>(null) }
|
||||
val currentBackEvent = remember { mutableStateOf<BackEventCompat?>(null) }
|
||||
|
||||
LaunchedEffect(active) {
|
||||
val animationInProgress = animationProgress.value > 0 && animationProgress.value < 1
|
||||
val animationSpec =
|
||||
if (animationInProgress) {
|
||||
AnimationPredictiveBackExitFloatSpec
|
||||
} else if (active) {
|
||||
AnimationEnterFloatSpec
|
||||
} else {
|
||||
AnimationExitFloatSpec
|
||||
}
|
||||
val targetValue = if (active) 1f else 0f
|
||||
if (animationProgress.value != targetValue) {
|
||||
animationProgress.animateTo(targetValue, animationSpec)
|
||||
}
|
||||
if (!active) {
|
||||
finalBackProgress.floatValue = Float.NaN
|
||||
firstBackEvent.value = null
|
||||
currentBackEvent.value = null
|
||||
}
|
||||
}
|
||||
|
||||
val mutatorMutex = remember { MutatorMutex() }
|
||||
PredictiveBackHandler(enabled = active) { progress ->
|
||||
mutatorMutex.mutate {
|
||||
try {
|
||||
finalBackProgress.floatValue = Float.NaN
|
||||
progress.collect { backEvent ->
|
||||
if (firstBackEvent.value == null) {
|
||||
firstBackEvent.value = backEvent
|
||||
}
|
||||
currentBackEvent.value = backEvent
|
||||
val interpolatedProgress = EaseOut.transform(backEvent.progress)
|
||||
animationProgress.snapTo(targetValue = 1 - interpolatedProgress)
|
||||
}
|
||||
finalBackProgress.floatValue = animationProgress.value
|
||||
onActiveChange(false)
|
||||
} catch (e: CancellationException) {
|
||||
animationProgress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = AnimationPredictiveBackExitFloatSpec,
|
||||
)
|
||||
finalBackProgress.floatValue = Float.NaN
|
||||
firstBackEvent.value = null
|
||||
currentBackEvent.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchBarInternal(
|
||||
animationProgress = animationProgress,
|
||||
finalBackProgress = finalBackProgress,
|
||||
firstBackEvent = firstBackEvent,
|
||||
currentBackEvent = currentBackEvent,
|
||||
state = state,
|
||||
onSearch = onSearch,
|
||||
active = active,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
title = title,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation,
|
||||
windowInsets = windowInsets,
|
||||
interactionSource = interactionSource,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalMaterial3Api
|
||||
@Composable
|
||||
internal fun SearchBarInternal(
|
||||
animationProgress: Animatable<Float, AnimationVector1D>,
|
||||
finalBackProgress: MutableFloatState,
|
||||
firstBackEvent: MutableState<BackEventCompat?>,
|
||||
currentBackEvent: MutableState<BackEventCompat?>,
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
active: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
||||
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
|
||||
shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val defaultInputFieldShape = SearchBarDefaults.inputFieldShape
|
||||
val defaultFullScreenShape = SearchBarDefaults.fullScreenShape
|
||||
val useFullScreenShape by remember {
|
||||
derivedStateOf(structuralEqualityPolicy()) { animationProgress.value == 1f }
|
||||
}
|
||||
val animatedShape = remember(useFullScreenShape, shape) {
|
||||
when {
|
||||
shape == defaultInputFieldShape ->
|
||||
// The shape can only be animated if it's the default spec value
|
||||
GenericShape { size, _ ->
|
||||
val radius = with(density) {
|
||||
(SearchBarCornerRadius * (1 - animationProgress.value)).toPx()
|
||||
}
|
||||
addRoundRect(RoundRect(size.toRect(), CornerRadius(radius)))
|
||||
}
|
||||
useFullScreenShape -> defaultFullScreenShape
|
||||
else -> shape
|
||||
}
|
||||
}
|
||||
|
||||
// The main animation complexity is allowing the component to smoothly expand while keeping the
|
||||
// input field at the same relative location on screen. `Modifier.windowInsetsPadding` does not
|
||||
// support animation and thus is not suitable. Instead, we convert the insets to a padding
|
||||
// applied to the Surface, which gradually becomes padding applied to the input field as the
|
||||
// animation proceeds.
|
||||
val unconsumedInsets = remember { MutableWindowInsets() }
|
||||
val topPadding = remember(density) {
|
||||
derivedStateOf {
|
||||
SearchBarVerticalPadding +
|
||||
unconsumedInsets.asPaddingValues(density).calculateTopPadding()
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
shape = animatedShape,
|
||||
color = colors.containerColor,
|
||||
contentColor = contentColorFor(colors.containerColor),
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation,
|
||||
modifier = modifier
|
||||
.padding(horizontal = lerp(SearchBarHorizontalPadding, 0.dp, animationProgress.value))
|
||||
.zIndex(1f)
|
||||
.onConsumedWindowInsetsChanged { consumedInsets ->
|
||||
unconsumedInsets.insets = windowInsets.exclude(consumedInsets)
|
||||
}
|
||||
.consumeWindowInsets(unconsumedInsets)
|
||||
.layout { measurable, constraints ->
|
||||
val animatedTopPadding =
|
||||
lerp(topPadding.value, 0.dp, animationProgress.value).roundToPx()
|
||||
|
||||
val defaultStartWidth = max(constraints.minWidth, SearchBarMinWidth.roundToPx())
|
||||
.coerceAtMost(min(constraints.maxWidth, SearchBarMaxWidth.roundToPx()))
|
||||
val defaultStartHeight = max(constraints.minHeight, InputFieldHeight.roundToPx())
|
||||
.coerceAtMost(constraints.maxHeight)
|
||||
val predictiveBackStartWidth =
|
||||
(constraints.maxWidth * SearchBarPredictiveBackMinScale).roundToInt()
|
||||
val predictiveBackStartHeight =
|
||||
(constraints.maxHeight * SearchBarPredictiveBackMinScale).roundToInt()
|
||||
val predictiveBackMultiplier = calculatePredictiveBackMultiplier(
|
||||
currentBackEvent.value,
|
||||
animationProgress.value,
|
||||
finalBackProgress.floatValue,
|
||||
)
|
||||
val startWidth =
|
||||
lerp(defaultStartWidth, predictiveBackStartWidth, predictiveBackMultiplier)
|
||||
val startHeight =
|
||||
lerp(defaultStartHeight, predictiveBackStartHeight, predictiveBackMultiplier)
|
||||
|
||||
val endWidth = constraints.maxWidth
|
||||
val endHeight = constraints.maxHeight
|
||||
|
||||
val width = lerp(startWidth, endWidth, animationProgress.value)
|
||||
val height =
|
||||
lerp(startHeight, endHeight, animationProgress.value) + animatedTopPadding
|
||||
|
||||
val minOffsetMargin = SearchBarPredictiveBackMinMargin.roundToPx()
|
||||
val predictiveBackOffsetX = calculatePredictiveBackOffsetX(
|
||||
constraints,
|
||||
minOffsetMargin,
|
||||
currentBackEvent.value,
|
||||
animationProgress.value,
|
||||
predictiveBackMultiplier,
|
||||
)
|
||||
val predictiveBackOffsetY = calculatePredictiveBackOffsetY(
|
||||
constraints,
|
||||
minOffsetMargin,
|
||||
currentBackEvent.value,
|
||||
firstBackEvent.value,
|
||||
height,
|
||||
SearchBarPredictiveBackMaxOffsetY.roundToPx(),
|
||||
predictiveBackMultiplier,
|
||||
)
|
||||
|
||||
val placeable = measurable.measure(
|
||||
Constraints
|
||||
.fixed(width, height)
|
||||
.offset(
|
||||
vertical = -animatedTopPadding,
|
||||
),
|
||||
)
|
||||
layout(width, height) {
|
||||
placeable.placeRelative(
|
||||
predictiveBackOffsetX,
|
||||
animatedTopPadding + predictiveBackOffsetY,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
val animatedInputFieldPadding = remember {
|
||||
AnimatedPaddingValues2(animationProgress.asState(), topPadding)
|
||||
}
|
||||
SearchBarInputField(
|
||||
state = state,
|
||||
onSearch = onSearch,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = Modifier.padding(paddingValues = animatedInputFieldPadding),
|
||||
enabled = enabled,
|
||||
label = title.takeUnless { active || animationProgress.value > 0 || state.text.isNotEmpty() },
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
colors = colors.inputFieldColors,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
||||
val showResults by remember {
|
||||
derivedStateOf(structuralEqualityPolicy()) { animationProgress.value > 0 }
|
||||
}
|
||||
if (showResults) {
|
||||
Column(Modifier.graphicsLayer { alpha = animationProgress.value }) {
|
||||
HorizontalDivider(color = colors.dividerColor)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
val shouldClearFocus = !active && isFocused
|
||||
LaunchedEffect(active) {
|
||||
if (shouldClearFocus) {
|
||||
// Not strictly needed according to the motion spec, but since the animation already has
|
||||
// a delay, this works around b/261632544.
|
||||
delay(AnimationDelayMillis.toLong())
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackMultiplier(
|
||||
currentBackEvent: BackEventCompat?,
|
||||
progress: Float,
|
||||
finalBackProgress: Float,
|
||||
) = when {
|
||||
currentBackEvent == null -> 0f // Not in predictive back at all.
|
||||
finalBackProgress.isNaN() -> 1f // User is currently swiping predictive back.
|
||||
finalBackProgress <= 0 -> 0f // Safety check for divide by zero.
|
||||
else -> progress / finalBackProgress // User has released predictive back swipe.
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackOffsetX(
|
||||
constraints: Constraints,
|
||||
minMargin: Int,
|
||||
currentBackEvent: BackEventCompat?,
|
||||
progress: Float,
|
||||
predictiveBackMultiplier: Float,
|
||||
): Int {
|
||||
if (currentBackEvent == null || predictiveBackMultiplier == 0f) {
|
||||
return 0
|
||||
}
|
||||
val directionMultiplier = if (currentBackEvent.swipeEdge == BackEventCompat.EDGE_LEFT) 1 else -1
|
||||
val maxOffsetX =
|
||||
(constraints.maxWidth * SearchBarPredictiveBackMaxOffsetXRatio) - minMargin
|
||||
val interpolatedOffsetX = maxOffsetX * (1 - progress)
|
||||
return (interpolatedOffsetX * predictiveBackMultiplier * directionMultiplier).roundToInt()
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackOffsetY(
|
||||
constraints: Constraints,
|
||||
minMargin: Int,
|
||||
currentBackEvent: BackEventCompat?,
|
||||
firstBackEvent: BackEventCompat?,
|
||||
height: Int,
|
||||
maxOffsetY: Int,
|
||||
predictiveBackMultiplier: Float,
|
||||
): Int {
|
||||
if (firstBackEvent == null || currentBackEvent == null || predictiveBackMultiplier == 0f) {
|
||||
return 0
|
||||
}
|
||||
val availableVerticalSpace = max(0, (constraints.maxHeight - height) / 2 - minMargin)
|
||||
val adjustedMaxOffsetY = min(availableVerticalSpace, maxOffsetY)
|
||||
val yDelta = currentBackEvent.touchY - firstBackEvent.touchY
|
||||
val yProgress = abs(yDelta) / constraints.maxHeight
|
||||
val directionMultiplier = sign(yDelta)
|
||||
val interpolatedOffsetY = lerp(0, adjustedMaxOffsetY, yProgress)
|
||||
return (interpolatedOffsetY * predictiveBackMultiplier * directionMultiplier).roundToInt()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchBarInputField(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val textColor = LocalTextStyle.current.color.takeOrElse {
|
||||
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
.height(InputFieldHeight)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { if (it.isFocused) onActiveChange(true) }
|
||||
.semantics {
|
||||
onClick {
|
||||
focusRequester.requestFocus()
|
||||
true
|
||||
}
|
||||
},
|
||||
enabled = enabled,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
|
||||
cursorBrush = SolidColor(colors.cursorColor),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { onSearch() }),
|
||||
interactionSource = interactionSource,
|
||||
decorator = @Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
innerTextField = innerTextField,
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon?.let { leading ->
|
||||
{
|
||||
Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() }
|
||||
}
|
||||
},
|
||||
trailingIcon = trailingIcon?.let { trailing ->
|
||||
{
|
||||
Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() }
|
||||
}
|
||||
},
|
||||
shape = SearchBarDefaults.inputFieldShape,
|
||||
colors = colors,
|
||||
contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
|
||||
container = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
private class AnimatedPaddingValues2(
|
||||
val animationProgress: State<Float>,
|
||||
val topPadding: State<Dp>,
|
||||
) : PaddingValues {
|
||||
override fun calculateTopPadding(): Dp = topPadding.value * animationProgress.value
|
||||
override fun calculateBottomPadding(): Dp = SearchBarVerticalPadding * animationProgress.value
|
||||
|
||||
override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = 0.dp
|
||||
override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = 0.dp
|
||||
}
|
||||
|
||||
// Measurement specs
|
||||
private val SearchBarCornerRadius: Dp = InputFieldHeight / 2
|
||||
internal val SearchBarMinWidth: Dp = 360.dp
|
||||
private val SearchBarMaxWidth: Dp = 720.dp
|
||||
internal val SearchBarVerticalPadding: Dp = 8.dp
|
||||
internal val SearchBarHorizontalPadding: Dp = 16.dp
|
||||
|
||||
// Search bar has 16dp padding between icons and start/end, while by default text field has 12dp.
|
||||
private val SearchBarIconOffsetX: Dp = 4.dp
|
||||
private const val SearchBarPredictiveBackMinScale: Float = 9f / 10f
|
||||
private val SearchBarPredictiveBackMinMargin: Dp = 8.dp
|
||||
private const val SearchBarPredictiveBackMaxOffsetXRatio: Float = 1f / 20f
|
||||
private val SearchBarPredictiveBackMaxOffsetY: Dp = 24.dp
|
||||
|
||||
// Animation specs
|
||||
private const val AnimationEnterDurationMillis: Int = 600
|
||||
private const val AnimationExitDurationMillis: Int = 350
|
||||
private const val AnimationDelayMillis: Int = 100
|
||||
private val AnimationEnterEasing = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1.0f)
|
||||
private val AnimationExitEasing = CubicBezierEasing(0.0f, 1.0f, 0.0f, 1.0f)
|
||||
private val AnimationEnterFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationEnterDurationMillis,
|
||||
delayMillis = AnimationDelayMillis,
|
||||
easing = AnimationEnterEasing,
|
||||
)
|
||||
private val AnimationExitFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationExitDurationMillis,
|
||||
delayMillis = AnimationDelayMillis,
|
||||
easing = AnimationExitEasing,
|
||||
)
|
||||
private val AnimationPredictiveBackExitFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationExitDurationMillis,
|
||||
easing = AnimationExitEasing,
|
||||
)
|
||||
+3
-140
@@ -236,7 +236,7 @@ class DrawerState2(
|
||||
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDrawerState(
|
||||
fun rememberDrawerState2(
|
||||
initialValue: DrawerValue,
|
||||
confirmStateChange: (DrawerValue) -> Boolean = { true },
|
||||
): DrawerState2 {
|
||||
@@ -246,147 +246,10 @@ fun rememberDrawerState(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalNavigationDrawer(
|
||||
fun ModalSideDrawer(
|
||||
drawerContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
drawerState: DrawerState2 = rememberDrawerState(DrawerValue.Closed),
|
||||
gesturesEnabled: Boolean = true,
|
||||
scrimColor: Color = DrawerDefaults.scrimColor,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
var minValue by remember { mutableFloatStateOf(-with(density) { ContainerWidth.toPx() }) }
|
||||
val maxValue = 0f
|
||||
|
||||
SideEffect {
|
||||
drawerState.density = density
|
||||
drawerState.anchoredDraggableState.updateAnchors(
|
||||
DraggableAnchors {
|
||||
DrawerValue.Closed at minValue
|
||||
DrawerValue.Open at maxValue
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
val velocityTracker = remember { VelocityTracker() }
|
||||
val dragModifier = if (gesturesEnabled) {
|
||||
Modifier.pointerInput(isRtl) {
|
||||
val multiplier = if (isRtl) -1 else 1
|
||||
awaitEachGesture {
|
||||
fun canConsume(slop: Float) = (slop > 0 && drawerState.isClosed) || (slop < 0 && drawerState.isOpen)
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
var ignore = false
|
||||
val drag = awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
|
||||
val overSlop = over * multiplier
|
||||
val canConsume = canConsume(overSlop)
|
||||
if (canConsume && !ignore) {
|
||||
velocityTracker.addPointerInputChange(change)
|
||||
change.consume()
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(overSlop)
|
||||
} else {
|
||||
ignore = true
|
||||
}
|
||||
}
|
||||
if (drag != null) {
|
||||
horizontalDrag(drag.id) {
|
||||
velocityTracker.addPointerInputChange(it)
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(it.positionChange().x * multiplier)
|
||||
}
|
||||
val velocity = velocityTracker.calculateVelocity()
|
||||
velocityTracker.resetTracking()
|
||||
scope.launch {
|
||||
drawerState.anchoredDraggableState.settle(velocity.x * multiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(modifier.fillMaxSize().then(dragModifier)) {
|
||||
val radius by remember {
|
||||
snapshotFlow {
|
||||
val step = calculateFraction(minValue, maxValue, drawerState.currentOffset)
|
||||
lerp(0, 10, step).dp
|
||||
}
|
||||
}.collectAsState(0.dp)
|
||||
val blurModifier = Modifier.graphicsLayer {
|
||||
if (radius != 0.dp) {
|
||||
renderEffect = BlurEffect(radius.toPx(), radius.toPx(), TileMode.Clamp)
|
||||
shape = RectangleShape
|
||||
clip = true
|
||||
}
|
||||
}
|
||||
Box(modifier = blurModifier) {
|
||||
content()
|
||||
}
|
||||
Scrim(
|
||||
open = drawerState.isOpen,
|
||||
onClose = {
|
||||
if (gesturesEnabled && drawerState.confirmStateChange(DrawerValue.Closed)) {
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
},
|
||||
fraction = {
|
||||
calculateFraction(minValue, maxValue, drawerState.requireOffset())
|
||||
},
|
||||
color = scrimColor,
|
||||
)
|
||||
val predictiveState by animateFloatMergeOneWayPredictiveBackAsState(drawerState.isOpen) {
|
||||
drawerState.close()
|
||||
}
|
||||
val absoluteElevation = LocalAbsoluteTonalElevation.current + DrawerDefaults.ModalDrawerElevation
|
||||
val predictiveModifier = if (drawerState.isOpen) {
|
||||
if (predictiveState > 0) {
|
||||
Modifier.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
val multiplierX = lerp(1f, 1.05f, predictiveState)
|
||||
val multiplierY = lerp(1f, 0.95f, predictiveState)
|
||||
val scaledWidth = (multiplierX * placeable.width).roundToInt()
|
||||
val scaledHeight = (multiplierY * placeable.height).roundToInt()
|
||||
val reMeasured = measurable.measure(Constraints.fixed(scaledWidth, scaledHeight))
|
||||
layout(scaledWidth, scaledHeight) {
|
||||
reMeasured.placeRelative(scaledWidth - placeable.width, 0)
|
||||
}
|
||||
}.background(
|
||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
|
||||
shape = DrawerDefaults.shape,
|
||||
)
|
||||
} else {
|
||||
Modifier.graphicsLayer {
|
||||
val scale = lerp(1f, 0.95f, -predictiveState)
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}.offset {
|
||||
IntOffset(lerp(0f, minValue * 0.05f, -predictiveState).roundToInt(), 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier.onGloballyPositioned {
|
||||
val w = it.size.width
|
||||
if (w != 0) {
|
||||
minValue = -it.size.width.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(drawerState.requireOffset().roundToInt(), 0)
|
||||
} then predictiveModifier.align(Alignment.CenterStart),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
drawerContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SideDrawer(
|
||||
drawerContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
drawerState: DrawerState2 = rememberDrawerState(DrawerValue.Closed),
|
||||
drawerState: DrawerState2 = rememberDrawerState2(DrawerValue.Closed),
|
||||
gesturesEnabled: Boolean = true,
|
||||
scrimColor: Color = DrawerDefaults.scrimColor,
|
||||
content: @Composable () -> Unit,
|
||||
+2
-21
@@ -1336,8 +1336,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener allowing notification of load and error events. Extend [DefaultOnImageEventListener]
|
||||
* to simplify implementation.
|
||||
* Add a listener allowing notification of load and error events.
|
||||
*
|
||||
* @param onImageEventListener an [OnImageEventListener] instance.
|
||||
*/
|
||||
@@ -1391,7 +1390,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
/**
|
||||
* An event listener, allowing subclasses and activities to be notified of significant events.
|
||||
*/
|
||||
interface OnImageEventListener {
|
||||
fun interface OnImageEventListener {
|
||||
/**
|
||||
* Called when the dimensions of the image and view are known, and either a preview image,
|
||||
* the full size image, or base layer tiles are loaded. This indicates the scale and translate
|
||||
@@ -1399,16 +1398,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* graphic, or inform a subclass that it is safe to draw overlays.
|
||||
*/
|
||||
fun onReady()
|
||||
|
||||
/**
|
||||
* Indicates an error initiliasing the decoder when using a tiling, or when loading the full
|
||||
* size bitmap when tiling is disabled. This method cannot be relied upon; certain encoding
|
||||
* types of supported image formats can result in corrupt or blank images being loaded and
|
||||
* displayed with no detectable error.
|
||||
*
|
||||
* @param e The exception thrown. This error is also logged by the view.
|
||||
*/
|
||||
fun onImageLoadError(e: Exception)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1467,14 +1456,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
|
||||
private data class ScaleAndTranslate(var scale: Float, val vTranslate: PointF)
|
||||
|
||||
/**
|
||||
* Default implementation of [OnImageEventListener] for extension. This does nothing in any method.
|
||||
*/
|
||||
open class DefaultOnImageEventListener : OnImageEventListener {
|
||||
override fun onReady() {}
|
||||
override fun onImageLoadError(e: Exception) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class used to set additional options for a scale animation. Create an instance using [.animateScale],
|
||||
* then set your options and call [.start].
|
||||
|
||||
@@ -25,11 +25,14 @@ import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.snapshots.Snapshot
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import coil3.EventListener
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.asCoilImage
|
||||
import coil3.gif.AnimatedImageDecoder
|
||||
import coil3.gif.GifDecoder
|
||||
import coil3.network.ktor.KtorNetworkFetcherFactory
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import coil3.serviceLoaderEnabled
|
||||
import coil3.util.DebugLogger
|
||||
@@ -54,22 +57,17 @@ import com.hippo.ehviewer.util.Crash
|
||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.isAtLeastP
|
||||
import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.isAtLeastS
|
||||
import com.hippo.ehviewer.util.isCronetAvailable
|
||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.cookies.HttpCookies
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import okhttp3.AsyncDns
|
||||
import okhttp3.android.AndroidAsyncDns
|
||||
import logcat.asLog
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.arch.room.roomDb
|
||||
import splitties.init.appCtx
|
||||
@@ -180,36 +178,30 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
crossfade(300)
|
||||
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
||||
if (drawable != null) error(drawable.asCoilImage(true))
|
||||
if (BuildConfig.DEBUG) logger(DebugLogger())
|
||||
if (BuildConfig.DEBUG) {
|
||||
logger(DebugLogger())
|
||||
} else {
|
||||
eventListener(object : EventListener() {
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
logcat("ImageLoader", LogPriority.ERROR) {
|
||||
"🚨 Failed - ${request.data}\n${result.throwable.asLog()}"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ktorClient by lazy {
|
||||
if (Settings.enableCronet && isCronetAvailable) {
|
||||
HttpClient(CronetEngine) {
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HttpClient(OkHttp) {
|
||||
engine {
|
||||
config {
|
||||
if (isAtLeastQ) {
|
||||
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
|
||||
}
|
||||
addInterceptor(UncaughtExceptionInterceptor())
|
||||
}
|
||||
}
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
HttpClient(CronetEngine) {
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val noRedirectKtorClient by lazy {
|
||||
HttpClient(ktorClient.engine) {
|
||||
HttpClient(CronetEngine) {
|
||||
followRedirects = false
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.hippo.ehviewer.dao.HistoryInfo
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||
import com.hippo.ehviewer.dao.ProgressInfo
|
||||
import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.dao.Schema17to18
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.unifile.asUniFile
|
||||
@@ -36,7 +37,10 @@ import kotlinx.coroutines.flow.Flow
|
||||
import splitties.arch.room.roomDb
|
||||
|
||||
object EhDB {
|
||||
private val db = roomDb<EhDatabase>("eh.db")
|
||||
private const val DB_NAME = "eh.db"
|
||||
private val db = roomDb<EhDatabase>(DB_NAME) {
|
||||
addMigrations(Schema17to18())
|
||||
}
|
||||
|
||||
private suspend fun putGalleryInfo(galleryInfo: BaseGalleryInfo) {
|
||||
db.galleryDao().upsert(galleryInfo)
|
||||
@@ -256,35 +260,20 @@ object EhDB {
|
||||
db.filterDao().update(filter)
|
||||
}
|
||||
|
||||
suspend fun exportDB(context: Context, uri: Uri) {
|
||||
val ehExportName = "eh.export.db"
|
||||
resource {
|
||||
context.deleteDatabase(ehExportName)
|
||||
roomDb<EhDatabase>(ehExportName)
|
||||
} release {
|
||||
it.close()
|
||||
context.deleteDatabase(ehExportName)
|
||||
} use { newDb ->
|
||||
db.galleryDao().list().let { newDb.galleryDao().insertOrIgnore(it) }
|
||||
db.progressDao().list().let { newDb.progressDao().insertOrIgnore(it) }
|
||||
db.downloadLabelDao().list().let { newDb.downloadLabelDao().insert(it) }
|
||||
db.downloadDirnameDao().list().let { newDb.downloadDirnameDao().insertOrIgnore(it) }
|
||||
db.downloadsDao().list().let { newDb.downloadsDao().insert(it) }
|
||||
db.historyDao().list().let { newDb.historyDao().insertOrIgnore(it) }
|
||||
db.quickSearchDao().list().let { newDb.quickSearchDao().insert(it) }
|
||||
db.localFavoritesDao().list().let { newDb.localFavoritesDao().insertOrIgnore(it) }
|
||||
db.filterDao().list().let { newDb.filterDao().insert(it) }
|
||||
newDb.close()
|
||||
val dbFile = context.getDatabasePath(ehExportName)
|
||||
dbFile.asUniFile() sendTo uri.asUniFile()
|
||||
}
|
||||
fun exportDB(context: Context, uri: Uri) {
|
||||
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
|
||||
val dbFile = context.getDatabasePath(DB_NAME)
|
||||
dbFile.asUniFile() sendTo uri.asUniFile()
|
||||
}
|
||||
|
||||
suspend fun importDB(context: Context, uri: Uri) {
|
||||
val tempDBName = "tmp.db"
|
||||
resource {
|
||||
context.deleteDatabase(tempDBName)
|
||||
roomDb<EhDatabase>(tempDBName) { createFromInputStream { context.contentResolver.openInputStream(uri) } }
|
||||
roomDb<EhDatabase>(tempDBName) {
|
||||
createFromInputStream { context.contentResolver.openInputStream(uri) }
|
||||
addMigrations(Schema17to18())
|
||||
}
|
||||
} release {
|
||||
it.close()
|
||||
context.deleteDatabase(tempDBName)
|
||||
|
||||
@@ -115,6 +115,7 @@ object Settings : DataStorePreferences(null) {
|
||||
val gridView = boolPref("grid_view", false)
|
||||
val showGalleryPages = boolPref("show_gallery_pages", true)
|
||||
val qSSaveProgress = boolPref("qs_save_progress", true)
|
||||
val security = boolPref("require_unlock", false)
|
||||
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
|
||||
|
||||
var downloadScheme by stringOrNullPref("image_scheme", null)
|
||||
@@ -146,7 +147,6 @@ object Settings : DataStorePreferences(null) {
|
||||
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
|
||||
var saveParseErrorBody by boolPref("save_parse_error_body", true)
|
||||
var saveCrashLog by boolPref("save_crash_log", true)
|
||||
var security by boolPref("require_unlock", false)
|
||||
var removeImageFiles by boolPref("include_pic", true)
|
||||
var needSignIn by boolPref("need_sign_in", true)
|
||||
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
||||
@@ -154,7 +154,6 @@ object Settings : DataStorePreferences(null) {
|
||||
var downloadOriginImage by boolPref("download_origin_image", false)
|
||||
var saveAsCbz by boolPref("save_as_cbz", false)
|
||||
var archiveMetadata by boolPref("archive_metadata", true)
|
||||
var enableCronet by boolPref("enable_cronet", true)
|
||||
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
|
||||
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
||||
var securityDelay by intPref("require_unlock_delay", 0)
|
||||
|
||||
@@ -44,7 +44,6 @@ import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.client.parser.ProfileParser
|
||||
import com.hippo.ehviewer.client.parser.RateGalleryResult
|
||||
import com.hippo.ehviewer.client.parser.SignInParser
|
||||
import com.hippo.ehviewer.client.parser.TorrentParser
|
||||
import com.hippo.ehviewer.client.parser.TorrentResult
|
||||
import com.hippo.ehviewer.client.parser.VoteCommentResult
|
||||
@@ -61,7 +60,6 @@ import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import io.ktor.utils.io.pool.useInstance
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.ceil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -72,6 +70,8 @@ import kotlinx.serialization.json.addJsonArray
|
||||
import kotlinx.serialization.json.put
|
||||
import moe.tarsin.coroutines.removeAllSuspend
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import org.jsoup.Jsoup
|
||||
import splitties.init.appCtx
|
||||
|
||||
@@ -85,11 +85,11 @@ private const val U_CONFIG_TEXT = "Selected Profile"
|
||||
fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
|
||||
val dir = AppConfig.externalParseErrorDir ?: return
|
||||
val file = File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt")
|
||||
RandomAccessFile(file, "rw").use { randomAccessFile ->
|
||||
with(randomAccessFile) {
|
||||
writeUTF(e.message + "\n")
|
||||
onLeft { writeUTF(it) }
|
||||
onRight { channel.write(it) }
|
||||
file.sink().buffer().use { sink ->
|
||||
with(sink) {
|
||||
writeUtf8(e.message + "\n")
|
||||
onLeft { writeUtf8(it) }
|
||||
onRight { write(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
// Check sad panda (without panda)
|
||||
val empty = body.fold(
|
||||
{ it.isEmpty() },
|
||||
{ it.limit() == 0 },
|
||||
{ !it.hasRemaining() },
|
||||
)
|
||||
if (empty) {
|
||||
if (EhUtils.isExHentai) {
|
||||
@@ -144,7 +144,7 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
|
||||
val httpContentPool = DirectByteBufferPool(8, 0x80000)
|
||||
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = execute { response ->
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = executeSafely { response ->
|
||||
val body = response.bodyAsUtf8Text()
|
||||
runSuspendCatching {
|
||||
block(body)
|
||||
@@ -153,20 +153,21 @@ suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = execute { response ->
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = executeSafely { response ->
|
||||
httpContentPool.useInstance { buffer ->
|
||||
with(response.bodyAsChannel()) { while (!isClosedForRead) readAvailable(buffer) }
|
||||
buffer.flip()
|
||||
runSuspendCatching {
|
||||
block(buffer)
|
||||
}.onFailure {
|
||||
buffer.rewind()
|
||||
rethrowExactly(response.status.value, buffer.right(), it)
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
object EhEngine {
|
||||
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).execute { response ->
|
||||
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
|
||||
val location = response.headers["Location"] ?: throw InsufficientFundsException()
|
||||
location.takeIf { "bounce_login" !in it } ?: throw NotLoggedInException()
|
||||
}
|
||||
@@ -233,22 +234,6 @@ object EhEngine {
|
||||
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsByteBuffer(FavoritesParser::parse)
|
||||
.apply { galleryInfoList.fillInfo(url) }
|
||||
|
||||
suspend fun signIn(username: String, password: String): String {
|
||||
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
|
||||
val url = EhUrl.API_SIGN_IN
|
||||
val origin = "https://forums.e-hentai.org"
|
||||
return ehRequest(url, referer, origin) {
|
||||
formBody {
|
||||
append("referer", referer)
|
||||
append("b", "")
|
||||
append("bt", "")
|
||||
append("UserName", username)
|
||||
append("PassWord", password)
|
||||
append("CookieDate", "1")
|
||||
}
|
||||
}.fetchUsingAsText(SignInParser::parse)
|
||||
}
|
||||
|
||||
suspend fun commentGallery(url: String, comment: String, id: Long = -1) = ehRequest(url, url, EhUrl.origin) {
|
||||
formBody {
|
||||
if (id == -1L) {
|
||||
@@ -258,7 +243,7 @@ object EhEngine {
|
||||
append("edit_comment", id.toString())
|
||||
}
|
||||
}
|
||||
}.execute { response ->
|
||||
}.executeSafely { response ->
|
||||
// Ktor does not handle POST redirect, we need to do it manually
|
||||
// https://youtrack.jetbrains.com/issue/KTOR-478
|
||||
val location = response.headers["Location"] ?: url
|
||||
@@ -287,7 +272,7 @@ object EhEngine {
|
||||
append("apply", "Apply Changes")
|
||||
append("update", "1")
|
||||
}
|
||||
}.execute { }
|
||||
}.executeSafely { }
|
||||
}
|
||||
|
||||
suspend fun downloadArchive(gid: Long, token: String, or: String, res: String, isHAtH: Boolean): String? {
|
||||
|
||||
@@ -21,13 +21,11 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.util.bodyAsUtf8Text
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.forms.FormBuilder
|
||||
import io.ktor.client.request.forms.FormDataContent
|
||||
import io.ktor.client.request.forms.MultiPartFormDataContent
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.prepareRequest
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.HttpStatement
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
@@ -35,6 +33,7 @@ import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.ParametersBuilder
|
||||
import io.ktor.http.content.TextContent
|
||||
import io.ktor.http.userAgent
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArrayBuilder
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
@@ -45,7 +44,7 @@ import okio.BufferedSource
|
||||
|
||||
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
|
||||
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = execute { it.bodyAsUtf8Text().parseAs<T>() }
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = executeSafely { it.bodyAsUtf8Text().parseAs<T>() }
|
||||
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
|
||||
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
|
||||
|
||||
@@ -85,6 +84,16 @@ suspend inline fun noRedirectEhRequest(
|
||||
apply(builder)
|
||||
}.also { logcat("EhRequest") { url } }
|
||||
|
||||
suspend inline fun <T> HttpStatement.executeSafely(
|
||||
crossinline block: suspend (response: HttpResponse) -> T,
|
||||
) = execute { resp ->
|
||||
try {
|
||||
block(resp)
|
||||
} finally {
|
||||
resp.coroutineContext.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun HttpRequestBuilder.formBody(builder: ParametersBuilder.() -> Unit) {
|
||||
method = HttpMethod.Post
|
||||
val parameters = ParametersBuilder().apply(builder).build()
|
||||
@@ -96,8 +105,3 @@ inline fun HttpRequestBuilder.jsonBody(builder: JsonObjectBuilder.() -> Unit) {
|
||||
val json = buildJsonObject(builder).toString()
|
||||
setBody(TextContent(text = json, contentType = ContentType.Application.Json))
|
||||
}
|
||||
|
||||
fun HttpRequestBuilder.multipartBody(builder: FormBuilder.() -> Unit) {
|
||||
method = HttpMethod.Post
|
||||
setBody(MultiPartFormDataContent(formData(builder)))
|
||||
}
|
||||
@@ -152,7 +152,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
|
||||
private suspend fun save(client: HttpClient, url: String, file: File) {
|
||||
runCatching {
|
||||
client.prepareGet(url).execute {
|
||||
client.prepareGet(url).executeSafely {
|
||||
if (it.status.isSuccess()) {
|
||||
it.bodyAsChannel().copyTo(file)
|
||||
} else {
|
||||
|
||||
@@ -36,7 +36,6 @@ object EhUrl {
|
||||
const val API_E = "https://api.e-hentai.org/api.php"
|
||||
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
|
||||
const val URL_MY_TAGS_E = HOST_E + "mytags"
|
||||
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
|
||||
const val URL_POPULAR_E = "https://e-hentai.org/popular"
|
||||
const val URL_POPULAR_EX = "https://exhentai.org/popular"
|
||||
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
|
||||
|
||||
@@ -25,7 +25,6 @@ import androidx.core.graphics.ColorUtils
|
||||
import arrow.core.memoize
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.abs
|
||||
|
||||
object EhUtils {
|
||||
@@ -58,14 +57,14 @@ object EhUtils {
|
||||
private const val BG_COLOR_UNKNOWN = 0xff000000u
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||
private val PATTERN_TITLE_PREFIX = Pattern.compile(
|
||||
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*",
|
||||
private val PATTERN_TITLE_PREFIX = Regex(
|
||||
"""^(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*""",
|
||||
)
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
|
||||
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
private val PATTERN_TITLE_SUFFIX = Regex(
|
||||
"""(?:\s+ch.[\s\d-]+)?(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*$""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
private val CATEGORY_VALUES = hashMapOf(
|
||||
@@ -170,16 +169,12 @@ object EhUtils {
|
||||
|
||||
fun extractTitle(fullTitle: String?): String? {
|
||||
var title: String = fullTitle ?: return null
|
||||
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("")
|
||||
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("")
|
||||
title = PATTERN_TITLE_PREFIX.replaceFirst(title, "")
|
||||
title = PATTERN_TITLE_SUFFIX.replaceFirst(title, "")
|
||||
// Sometimes title is combined by romaji and english translation.
|
||||
// Only need romaji.
|
||||
// TODO But not sure every '|' means that
|
||||
val index = title.indexOf('|')
|
||||
if (index >= 0) {
|
||||
title = title.substring(0, index)
|
||||
}
|
||||
return title.ifEmpty { null }
|
||||
return title.substringBeforeLast('|').trim().ifEmpty { null }
|
||||
}
|
||||
|
||||
fun handleThumbUrlResolution(url: String?): String? {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import java.util.regex.Pattern
|
||||
import moe.tarsin.kt.unreachable
|
||||
|
||||
interface GalleryInfo {
|
||||
@@ -62,36 +61,36 @@ interface GalleryInfo {
|
||||
"NL",
|
||||
)
|
||||
private val S_LANG_PATTERNS = arrayOf(
|
||||
Pattern.compile(
|
||||
Regex(
|
||||
"[(\\[]eng(?:lish)?[)\\]]|英訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
// [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
|
||||
Pattern.compile(
|
||||
Regex(
|
||||
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile(
|
||||
Regex(
|
||||
"[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]|スペイン翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile("[(\\[]korean?[)\\]]|韓国翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]portuguese|ポルトガル翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile(
|
||||
Regex("[(\\[]korean?[)\\]]|韓国翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]portuguese|ポルトガル翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex(
|
||||
"[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย|タイ翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile("[(\\[]german[)\\]]|ドイツ翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]italiano?[)\\]]|イタリア翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile(
|
||||
Regex("[(\\[]german[)\\]]|ドイツ翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]italiano?[)\\]]|イタリア翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex(
|
||||
"[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]|ベトナム翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile("[(\\[]polish[)\\]]|ポーランド翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Regex("[(\\[]polish[)\\]]|ポーランド翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]dutch[)\\]]|オランダ翻訳", RegexOption.IGNORE_CASE),
|
||||
)
|
||||
val S_LANG_TAGS = arrayOf(
|
||||
"language:english",
|
||||
@@ -123,7 +122,7 @@ interface GalleryInfo {
|
||||
|
||||
private fun generateSLangFromTitle(title: String): String? {
|
||||
for (i in S_LANGS.indices) {
|
||||
if (S_LANG_PATTERNS[i].matcher(title).find()) {
|
||||
if (S_LANG_PATTERNS[i].containsMatchIn(title)) {
|
||||
return S_LANGS[i]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,27 +16,21 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.util.toLongOrDefault
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Like http://exhentai.org/g/1234567/a1b2c3d4e5<br></br>
|
||||
*/
|
||||
object GalleryDetailUrlParser {
|
||||
private val URL_STRICT_PATTERN = Pattern.compile(
|
||||
private val URL_STRICT_PATTERN = Regex(
|
||||
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/(?:g|mpv)/(\\d+)/([0-9a-f]{10})",
|
||||
)
|
||||
private val URL_PATTERN = Pattern.compile("(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)")
|
||||
private val URL_PATTERN = Regex("(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)")
|
||||
|
||||
fun parse(url: String?, strict: Boolean = true): Result? {
|
||||
url ?: return null
|
||||
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
||||
val m = pattern.matcher(url)
|
||||
return if (m.find()) {
|
||||
val gid = m.group(1)!!.toLongOrDefault(0).takeIf { it > 0 }
|
||||
gid?.let { Result(it, m.group(2)!!) }
|
||||
} else {
|
||||
null
|
||||
return pattern.find(url)?.destructured?.let { (gid, token) ->
|
||||
Result(gid.toLong(), token)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,34 +16,21 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import com.hippo.ehviewer.util.toLongOrDefault
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Like http://exhentai.org/s/91ea4b6d89/901103-12
|
||||
*/
|
||||
object GalleryPageUrlParser {
|
||||
private val URL_STRICT_PATTERN = Pattern.compile(
|
||||
private val URL_STRICT_PATTERN = Regex(
|
||||
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/s/([0-9a-f]{10})/(\\d+)-(\\d+)",
|
||||
)
|
||||
private val URL_PATTERN = Pattern.compile("([0-9a-f]{10})/(\\d+)-(\\d+)")
|
||||
private val URL_PATTERN = Regex("([0-9a-f]{10})/(\\d+)-(\\d+)")
|
||||
|
||||
fun parse(url: String?, strict: Boolean = true): Result? {
|
||||
url ?: return null
|
||||
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
||||
val m = pattern.matcher(url)
|
||||
return if (m.find()) {
|
||||
val gid = m.group(2)!!.toLongOrDefault(-1L)
|
||||
val pToken = m.group(1)!!
|
||||
val page = m.group(3)!!.toIntOrDefault(0) - 1
|
||||
if (gid < 0 || page < 0) {
|
||||
null
|
||||
} else {
|
||||
Result(gid, pToken, page)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
return pattern.find(url)?.destructured?.let { (pToken, gid, page) ->
|
||||
Result(gid.toLong(), pToken, page.toInt() - 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +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.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object SignInParser {
|
||||
private val NAME_PATTERN = Pattern.compile("<p>You are now logged in as: (.+?)<")
|
||||
private val ERROR_PATTERN = Pattern.compile(
|
||||
"<h4>The error returned was:</h4>\\s*<p>(.+?)</p>" +
|
||||
"|<span class=\"postcolor\">(.+?)</span>",
|
||||
)
|
||||
|
||||
fun parse(body: String): String {
|
||||
var m = NAME_PATTERN.matcher(body)
|
||||
return if (m.find()) {
|
||||
m.group(1)!!
|
||||
} else {
|
||||
m = ERROR_PATTERN.matcher(body)
|
||||
if (m.find()) {
|
||||
throw EhException(m.group(1) ?: m.group(2))
|
||||
} else {
|
||||
throw ParseException("Can't parse sign in")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import com.hippo.ehviewer.image.detectBorder
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
|
||||
private const val CROP_THRESHOLD = 0.75f
|
||||
private const val WEBTOON_THRESHOLD = 3
|
||||
private const val RATIO_THRESHOLD = 2
|
||||
|
||||
private val maybeCropBorderKey = Extras.Key(default = false)
|
||||
|
||||
@@ -48,7 +48,12 @@ object CropBorderInterceptor : Interceptor {
|
||||
val image = result.image
|
||||
if (image is BitmapImage) {
|
||||
val bitmap = image.bitmap
|
||||
val rect = if (image.height / image.width < WEBTOON_THRESHOLD) {
|
||||
val ratio = if (image.height > image.width) {
|
||||
image.height / image.width
|
||||
} else {
|
||||
image.width / image.height
|
||||
}
|
||||
val rect = if (ratio < RATIO_THRESHOLD) {
|
||||
val array = detectBorder(bitmap)
|
||||
val minWidth = image.width * CROP_THRESHOLD
|
||||
val minHeight = image.height * CROP_THRESHOLD
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
||||
@@ -30,24 +29,24 @@ object DownloadThumbInterceptor : Interceptor {
|
||||
}
|
||||
if (withIOContext { thumb.isFile }) {
|
||||
val new = chain.request.newBuilder().data(thumb.uri.toString()).build()
|
||||
when (val result = chain.withRequest(new).proceed()) {
|
||||
is SuccessResult -> return result
|
||||
is ErrorResult -> withIOContext { thumb.delete() }
|
||||
}
|
||||
val result = chain.withRequest(new).proceed()
|
||||
if (result is SuccessResult) return result
|
||||
}
|
||||
val new = chain.request.newBuilder().data(url).build()
|
||||
return chain.withRequest(new).proceed().also {
|
||||
if (it is SuccessResult) {
|
||||
withIOContext {
|
||||
if (thumb.ensureFile()) {
|
||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||
imageCache.read(key) {
|
||||
data.asUniFile() sendTo thumb
|
||||
}
|
||||
val result = chain.withRequest(new).proceed()
|
||||
if (result is SuccessResult) {
|
||||
withIOContext {
|
||||
// Accessing the recreated file immediately after deleting it throws
|
||||
// FileNotFoundException, so we just overwrite the existing file.
|
||||
if (thumb.ensureFile()) {
|
||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||
imageCache.read(key) {
|
||||
data.asUniFile() sendTo thumb
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
return chain.proceed()
|
||||
|
||||
File renamed without changes.
@@ -28,7 +28,6 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17),
|
||||
AutoMigration(from = 17, to = 18),
|
||||
AutoMigration(from = 18, to = 19, spec = Schema18to19::class),
|
||||
AutoMigration(from = 19, to = 20),
|
||||
AutoMigration(from = 20, to = 21, spec = Schema20to21::class),
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.room.DeleteTable
|
||||
import androidx.room.RenameColumn
|
||||
import androidx.room.RenameTable
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
|
||||
@@ -103,6 +104,15 @@ class Schema12to13 : AutoMigrationSpec {
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "SIMPLE_LANGUAGE")
|
||||
class Schema13to14 : AutoMigrationSpec
|
||||
|
||||
class Schema17to18 : Migration(17, 18) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `_new_GALLERIES` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))")
|
||||
db.execSQL("INSERT INTO `_new_GALLERIES` (`GID`,`TOKEN`,`TITLE`,`TITLE_JPN`,`THUMB`,`CATEGORY`,`POSTED`,`UPLOADER`,`RATING`,`SIMPLE_LANGUAGE`,`FAVORITE_SLOT`) SELECT `GID`,`TOKEN`,`TITLE`,`TITLE_JPN`,`THUMB`,`CATEGORY`,`POSTED`,`UPLOADER`,`RATING`,`SIMPLE_LANGUAGE`,`FAVORITE_SLOT` FROM `GALLERIES` WHERE `TOKEN` IS NOT NULL")
|
||||
db.execSQL("DROP TABLE `GALLERIES`")
|
||||
db.execSQL("ALTER TABLE `_new_GALLERIES` RENAME TO `GALLERIES`")
|
||||
}
|
||||
}
|
||||
|
||||
@RenameColumn(tableName = "DOWNLOADS", fromColumnName = "POSITION", toColumnName = "TIME")
|
||||
class Schema18to19 : AutoMigrationSpec
|
||||
|
||||
|
||||
@@ -94,12 +94,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
if (label == null) {
|
||||
return false
|
||||
}
|
||||
for ((label1) in labelList) {
|
||||
if (label == label1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return labelList.any { it.label == label }
|
||||
}
|
||||
|
||||
fun containDownloadInfo(gid: Long) = mAllInfoMap.containsKey(gid)
|
||||
@@ -518,12 +513,14 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
}
|
||||
|
||||
suspend fun deleteLabel(label: String) {
|
||||
labelList.run {
|
||||
with(labelList) {
|
||||
val index = indexOfFirst { it.label == label }
|
||||
val item = get(index)
|
||||
EhDB.removeDownloadLabel(item)
|
||||
subList(index + 1, size).forEach {
|
||||
it.position--
|
||||
}
|
||||
EhDB.removeDownloadLabel(removeAt(index))
|
||||
removeAt(index)
|
||||
}
|
||||
allInfoList.forEach {
|
||||
if (it.label == label) {
|
||||
@@ -686,6 +683,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
private val mSpeedReminder = object {
|
||||
private val mContentLengthMap = SparseLongArray()
|
||||
private val mReceivedSizeMap = SparseLongArray()
|
||||
private val mutex = Mutex()
|
||||
private var mBytesRead = 0L
|
||||
private var oldSpeed = -1L
|
||||
private var currentJob: Job? = null
|
||||
@@ -699,29 +697,37 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
suspend fun stop() {
|
||||
mBytesRead = 0
|
||||
oldSpeed = -1
|
||||
mContentLengthMap.clear()
|
||||
mReceivedSizeMap.clear()
|
||||
mutex.withLock {
|
||||
mContentLengthMap.clear()
|
||||
mReceivedSizeMap.clear()
|
||||
}
|
||||
currentJob?.cancel()
|
||||
currentJob = null
|
||||
}
|
||||
|
||||
fun onDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {
|
||||
mContentLengthMap.put(index, contentLength)
|
||||
mReceivedSizeMap.put(index, receivedSize)
|
||||
suspend fun onDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {
|
||||
mutex.withLock {
|
||||
mContentLengthMap.put(index, contentLength)
|
||||
mReceivedSizeMap.put(index, receivedSize)
|
||||
}
|
||||
mBytesRead += bytesRead.toLong()
|
||||
}
|
||||
|
||||
fun onDone(index: Int) {
|
||||
mContentLengthMap.delete(index)
|
||||
mReceivedSizeMap.delete(index)
|
||||
suspend fun onDone(index: Int) {
|
||||
mutex.withLock {
|
||||
mContentLengthMap.delete(index)
|
||||
mReceivedSizeMap.delete(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun onFinish() {
|
||||
mContentLengthMap.clear()
|
||||
mReceivedSizeMap.clear()
|
||||
suspend fun onFinish() {
|
||||
mutex.withLock {
|
||||
mContentLengthMap.clear()
|
||||
mReceivedSizeMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runInternal() {
|
||||
@@ -742,12 +748,14 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
var downloadingCount = 0
|
||||
var downloadingContentLengthSum = 0L
|
||||
var totalSize = 0L
|
||||
for (i in 0..<mContentLengthMap.size()) {
|
||||
val contentLength = mContentLengthMap.valueAt(i)
|
||||
val receivedSize = mReceivedSizeMap.valueAt(i)
|
||||
downloadingCount++
|
||||
downloadingContentLengthSum += contentLength
|
||||
totalSize += contentLength - receivedSize
|
||||
mutex.withLock {
|
||||
for (i in 0..<mContentLengthMap.size()) {
|
||||
val contentLength = mContentLengthMap.valueAt(i)
|
||||
val receivedSize = mReceivedSizeMap.valueAt(i)
|
||||
downloadingCount++
|
||||
downloadingContentLengthSum += contentLength
|
||||
totalSize += contentLength - receivedSize
|
||||
}
|
||||
}
|
||||
if (downloadingCount != 0) {
|
||||
totalSize += downloadingContentLengthSum * (info.total - info.downloaded - downloadingCount) / downloadingCount
|
||||
|
||||
@@ -148,9 +148,9 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
|
||||
ACTION_CLEAR -> {
|
||||
clear()
|
||||
checkStopSelf()
|
||||
}
|
||||
}
|
||||
checkStopSelf()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = error("No bindService")
|
||||
|
||||
@@ -3,9 +3,9 @@ package com.hippo.ehviewer.download
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
|
||||
data class SortMode(val field: Field, val order: Order) {
|
||||
data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel: Boolean = false) {
|
||||
val flag
|
||||
get() = field.value shl 1 or order.value
|
||||
get() = groupByDownloadLabel.compareTo(false) shl 4 or (field.value shl 1) or order.value
|
||||
|
||||
enum class Field(val value: Int) {
|
||||
GID(0),
|
||||
@@ -21,22 +21,28 @@ data class SortMode(val field: Field, val order: Order) {
|
||||
}
|
||||
|
||||
fun comparator(): Comparator<DownloadInfo> {
|
||||
val comparator = when (field) {
|
||||
var comparator = when (field) {
|
||||
Field.GID -> compareBy { it.gid }
|
||||
Field.TITLE -> compareBy { EhUtils.getSuitableTitle(it) }
|
||||
Field.TITLE, Field.LABEL -> compareBy { EhUtils.getSuitableTitle(it) }
|
||||
Field.TIME -> compareBy { it.time }
|
||||
Field.LABEL -> compareBy<DownloadInfo> { it.label }.thenBy { EhUtils.getSuitableTitle(it) }
|
||||
Field.PAGES -> compareBy<DownloadInfo> { it.pages }.thenBy { EhUtils.getSuitableTitle(it) }
|
||||
}
|
||||
return if (order == Order.ASC) comparator else comparator.reversed()
|
||||
if (order == Order.DESC) {
|
||||
comparator = comparator.reversed()
|
||||
}
|
||||
if (groupByDownloadLabel) {
|
||||
comparator = compareBy<DownloadInfo> { it.label }.then(comparator)
|
||||
}
|
||||
return comparator
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(flag: Int): SortMode {
|
||||
val field = flag shr 1
|
||||
val order = flag and 1
|
||||
val field = flag and 0xF shr 1
|
||||
val order = flag and 0x1
|
||||
val groupByDownloadLabel = flag shr 4 == 1
|
||||
require(field in Field.entries.indices && order in Order.entries.indices)
|
||||
return SortMode(Field.entries[field], Order.entries[order])
|
||||
return SortMode(Field.entries[field], Order.entries[order], groupByDownloadLabel)
|
||||
}
|
||||
val Default = SortMode(Field.TIME, Order.DESC)
|
||||
val All = listOf(
|
||||
@@ -46,8 +52,6 @@ data class SortMode(val field: Field, val order: Order) {
|
||||
SortMode(Field.GID, Order.ASC),
|
||||
SortMode(Field.TITLE, Order.ASC),
|
||||
SortMode(Field.TITLE, Order.DESC),
|
||||
SortMode(Field.LABEL, Order.ASC),
|
||||
SortMode(Field.LABEL, Order.DESC),
|
||||
SortMode(Field.PAGES, Order.ASC),
|
||||
SortMode(Field.PAGES, Order.DESC),
|
||||
)
|
||||
|
||||
@@ -14,6 +14,8 @@ object EhIcons {
|
||||
val Default = Filled
|
||||
}
|
||||
|
||||
object Reader
|
||||
|
||||
val Default = Filled
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.hippo.ehviewer.icons.filled
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@@ -37,3 +40,10 @@ val EhIcons.Filled.Crop by unsafeLazy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconCropPreview() {
|
||||
Image(imageVector = EhIcons.Default.Crop, contentDescription = null)
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.hippo.ehviewer.icons.filled
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@Suppress("BooleanLiteralArgument")
|
||||
val EhIcons.Filled.CropOff by unsafeLazy {
|
||||
materialIcon(name = "CropOff") {
|
||||
materialPath {
|
||||
@@ -58,3 +62,10 @@ val EhIcons.Filled.CropOff by unsafeLazy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconCropOffPreview() {
|
||||
Image(imageVector = EhIcons.Default.CropOff, contentDescription = null)
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.hippo.ehviewer.icons.filled
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@@ -31,3 +34,10 @@ val EhIcons.Filled.GoTo by unsafeLazy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconGoToPreview() {
|
||||
Image(imageVector = EhIcons.Default.GoTo, contentDescription = null)
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.hippo.ehviewer.icons.filled
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@@ -45,3 +48,10 @@ val EhIcons.Filled.SadPanda by unsafeLazy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconSadPandaPreview() {
|
||||
Image(imageVector = EhIcons.Default.SadPanda, contentDescription = null)
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.hippo.ehviewer.icons.filled
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@@ -59,3 +62,10 @@ val EhIcons.Filled.Subscriptions by unsafeLazy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconSubscriptionsPreview() {
|
||||
Image(imageVector = EhIcons.Default.Subscriptions, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@Suppress("BooleanLiteralArgument")
|
||||
val EhIcons.Reader.ContinuousVertical by unsafeLazy {
|
||||
materialIcon(name = "ContinuousVertical") {
|
||||
materialPath {
|
||||
moveTo(17.3159F, 1.0F)
|
||||
horizontalLineToRelative(-10.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, -2.0F, 2.0F)
|
||||
lineTo(5.3159F, 21.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, 2.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, -2.0F)
|
||||
lineTo(19.3159F, 3.0F)
|
||||
arcTo(2.0059F, 2.0059F, 0.0F, false, false, 17.3159F, 1.0F)
|
||||
close()
|
||||
moveTo(17.3159F, 21.0F)
|
||||
horizontalLineToRelative(-10.0F)
|
||||
lineTo(7.3159F, 3.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
close()
|
||||
moveTo(11.3083F, 5.0F)
|
||||
horizontalLineToRelative(2.0F)
|
||||
verticalLineToRelative(5.5F)
|
||||
horizontalLineToRelative(-2.0F)
|
||||
close()
|
||||
moveTo(15.308F, 16.0F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
lineToRelative(0.0F, -4.5F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
lineToRelative(0.0F, 4.5F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
lineToRelative(3.0F, 3.0F)
|
||||
lineToRelative(3.0F, -3.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconContinuousVerticalPreview() {
|
||||
Image(imageVector = EhIcons.Reader.ContinuousVertical, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@Suppress("BooleanLiteralArgument")
|
||||
val EhIcons.Reader.Default by unsafeLazy {
|
||||
materialIcon(name = "Default") {
|
||||
materialPath {
|
||||
moveTo(6.9906F, 18.0F)
|
||||
lineTo(6.9906F, 6.0F)
|
||||
horizontalLineToRelative(12.0F)
|
||||
lineTo(18.9906F, 3.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, -2.0F, -2.0F)
|
||||
horizontalLineToRelative(-10.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, -2.0F, 2.0F)
|
||||
lineTo(4.9906F, 21.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, 2.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, -2.0F)
|
||||
lineTo(18.9906F, 18.0F)
|
||||
horizontalLineToRelative(-12.0F)
|
||||
close()
|
||||
moveTo(6.9906F, 3.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
lineTo(16.9906F, 4.0F)
|
||||
horizontalLineToRelative(-10.0F)
|
||||
close()
|
||||
moveTo(16.9906F, 21.0F)
|
||||
horizontalLineToRelative(-10.0F)
|
||||
lineTo(6.9906F, 20.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
close()
|
||||
moveTo(21.7706F, 12.44F)
|
||||
lineToRelative(-0.02F, 0.03F)
|
||||
arcToRelative(3.5546F, 3.5546F, 0.0F, false, false, 0.05F, -0.47F)
|
||||
arcToRelative(3.9031F, 3.9031F, 0.0F, false, false, -0.05F, -0.48F)
|
||||
lineToRelative(0.02F, 0.03F)
|
||||
lineToRelative(0.85F, -0.68F)
|
||||
arcToRelative(0.5068F, 0.5068F, 0.0F, false, false, 0.12F, -0.64F)
|
||||
lineToRelative(-0.84F, -1.46F)
|
||||
arcToRelative(0.4986F, 0.4986F, 0.0F, false, false, -0.61F, -0.22F)
|
||||
lineToRelative(-1.01F, 0.39F)
|
||||
lineToRelative(0.0099F, 0.04F)
|
||||
arcToRelative(3.6135F, 3.6135F, 0.0F, false, false, -0.8F, -0.48F)
|
||||
lineToRelative(-0.17F, -1.07F)
|
||||
arcToRelative(0.4879F, 0.4879F, 0.0F, false, false, -0.49F, -0.43F)
|
||||
horizontalLineToRelative(-1.68F)
|
||||
arcToRelative(0.4876F, 0.4876F, 0.0F, false, false, -0.49F, 0.42F)
|
||||
lineToRelative(-0.17F, 1.08F)
|
||||
arcToRelative(3.6149F, 3.6149F, 0.0F, false, false, -0.8F, 0.48F)
|
||||
lineToRelative(0.02F, -0.03F)
|
||||
lineToRelative(-1.01F, -0.39F)
|
||||
arcToRelative(0.4878F, 0.4878F, 0.0F, false, false, -0.61F, 0.22F)
|
||||
lineToRelative(-0.84F, 1.46F)
|
||||
arcToRelative(0.5068F, 0.5068F, 0.0F, false, false, 0.12F, 0.64F)
|
||||
lineToRelative(0.85F, 0.68F)
|
||||
lineToRelative(0.02F, -0.03F)
|
||||
arcToRelative(3.9148F, 3.9148F, 0.0F, false, false, -0.05F, 0.48F)
|
||||
arcToRelative(3.915F, 3.915F, 0.0F, false, false, 0.05F, 0.48F)
|
||||
lineToRelative(-0.02F, -0.03F)
|
||||
lineToRelative(-0.85F, 0.68F)
|
||||
arcToRelative(0.5068F, 0.5068F, 0.0F, false, false, -0.12F, 0.64F)
|
||||
lineToRelative(0.84F, 1.46F)
|
||||
arcToRelative(0.4985F, 0.4985F, 0.0F, false, false, 0.61F, 0.22F)
|
||||
lineToRelative(1.01F, -0.39F)
|
||||
lineToRelative(-0.02F, -0.03F)
|
||||
arcToRelative(4.4991F, 4.4991F, 0.0F, false, false, 0.8F, 0.46F)
|
||||
lineToRelative(0.17F, 1.08F)
|
||||
arcToRelative(0.4876F, 0.4876F, 0.0F, false, false, 0.49F, 0.42F)
|
||||
horizontalLineToRelative(1.68F)
|
||||
arcToRelative(0.4876F, 0.4876F, 0.0F, false, false, 0.49F, -0.42F)
|
||||
lineToRelative(0.17F, -1.08F)
|
||||
arcToRelative(3.6138F, 3.6138F, 0.0F, false, false, 0.8F, -0.48F)
|
||||
lineToRelative(-0.02F, 0.03F)
|
||||
lineToRelative(1.02F, 0.39F)
|
||||
arcToRelative(0.4879F, 0.4879F, 0.0F, false, false, 0.61F, -0.22F)
|
||||
lineToRelative(0.84F, -1.46F)
|
||||
arcToRelative(0.5068F, 0.5068F, 0.0F, false, false, -0.12F, -0.64F)
|
||||
close()
|
||||
moveTo(17.9906F, 14.0F)
|
||||
arcToRelative(2.0F, 2.0F, 0.0F, true, true, 2.0F, -2.0F)
|
||||
arcTo(2.0059F, 2.0059F, 0.0F, false, true, 17.9906F, 14.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconDefaultPreview() {
|
||||
Image(imageVector = EhIcons.Reader.Default, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
val EhIcons.Reader.Landscape by unsafeLazy {
|
||||
materialIcon(name = "Landscape") {
|
||||
materialPath {
|
||||
moveTo(1.01F, 7.0F)
|
||||
lineTo(1.0F, 17.0F)
|
||||
curveToRelative(0.0F, 1.1F, 0.9F, 2.0F, 2.0F, 2.0F)
|
||||
horizontalLineToRelative(18.0F)
|
||||
curveToRelative(1.1F, 0.0F, 2.0F, -0.9F, 2.0F, -2.0F)
|
||||
verticalLineTo(7.0F)
|
||||
curveToRelative(0.0F, -1.1F, -0.9F, -2.0F, -2.0F, -2.0F)
|
||||
horizontalLineTo(3.0F)
|
||||
curveToRelative(-1.1F, 0.0F, -1.99F, 0.9F, -1.99F, 2.0F)
|
||||
close()
|
||||
moveTo(19.0F, 7.0F)
|
||||
verticalLineToRelative(10.0F)
|
||||
horizontalLineTo(5.0F)
|
||||
verticalLineTo(7.0F)
|
||||
horizontalLineToRelative(14.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconLandscapePreview() {
|
||||
Image(imageVector = EhIcons.Reader.Landscape, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
val EhIcons.Reader.LandscapeLocked by unsafeLazy {
|
||||
materialIcon(name = "LandscapeLocked") {
|
||||
materialPath {
|
||||
moveTo(21.0F, 5.0F)
|
||||
lineTo(3.0F, 5.0F)
|
||||
curveToRelative(-1.1F, 0.0F, -2.0F, 0.9F, -2.0F, 2.0F)
|
||||
verticalLineToRelative(10.0F)
|
||||
curveToRelative(0.0F, 1.1F, 0.9F, 2.0F, 2.0F, 2.0F)
|
||||
horizontalLineToRelative(18.0F)
|
||||
curveToRelative(1.1F, 0.0F, 2.0F, -0.9F, 2.0F, -2.0F)
|
||||
lineTo(23.0F, 7.0F)
|
||||
curveToRelative(0.0F, -1.1F, -0.9F, -2.0F, -2.0F, -2.0F)
|
||||
close()
|
||||
moveTo(19.0F, 17.0F)
|
||||
lineTo(5.0F, 17.0F)
|
||||
lineTo(5.0F, 7.0F)
|
||||
horizontalLineToRelative(14.0F)
|
||||
verticalLineToRelative(10.0F)
|
||||
close()
|
||||
moveTo(10.0F, 16.0F)
|
||||
horizontalLineToRelative(4.0F)
|
||||
curveToRelative(0.55F, 0.0F, 1.0F, -0.45F, 1.0F, -1.0F)
|
||||
verticalLineToRelative(-3.0F)
|
||||
curveToRelative(0.0F, -0.55F, -0.45F, -1.0F, -1.0F, -1.0F)
|
||||
verticalLineToRelative(-1.0F)
|
||||
curveToRelative(0.0F, -1.11F, -0.9F, -2.0F, -2.0F, -2.0F)
|
||||
curveToRelative(-1.11F, 0.0F, -2.0F, 0.9F, -2.0F, 2.0F)
|
||||
verticalLineToRelative(1.0F)
|
||||
curveToRelative(-0.55F, 0.0F, -1.0F, 0.45F, -1.0F, 1.0F)
|
||||
verticalLineToRelative(3.0F)
|
||||
curveToRelative(0.0F, 0.55F, 0.45F, 1.0F, 1.0F, 1.0F)
|
||||
close()
|
||||
moveTo(10.8F, 10.0F)
|
||||
curveToRelative(0.0F, -0.66F, 0.54F, -1.2F, 1.2F, -1.2F)
|
||||
curveToRelative(0.66F, 0.0F, 1.2F, 0.54F, 1.2F, 1.2F)
|
||||
verticalLineToRelative(1.0F)
|
||||
horizontalLineToRelative(-2.4F)
|
||||
verticalLineToRelative(-1.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconLandscapeLockedPreview() {
|
||||
Image(imageVector = EhIcons.Reader.LandscapeLocked, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@Suppress("BooleanLiteralArgument")
|
||||
val EhIcons.Reader.LeftToRight by unsafeLazy {
|
||||
materialIcon(name = "LeftToRight") {
|
||||
materialPath {
|
||||
moveTo(17.3159F, 18.0F)
|
||||
horizontalLineToRelative(-10.0F)
|
||||
lineTo(7.3159F, 16.0F)
|
||||
horizontalLineToRelative(-2.0F)
|
||||
verticalLineToRelative(5.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, 2.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, -2.0F)
|
||||
lineTo(19.3159F, 16.0F)
|
||||
horizontalLineToRelative(-2.0F)
|
||||
close()
|
||||
moveTo(17.3159F, 21.0F)
|
||||
horizontalLineToRelative(-10.0F)
|
||||
lineTo(7.3159F, 20.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
close()
|
||||
moveTo(7.3159F, 6.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
lineTo(17.3159F, 8.0F)
|
||||
horizontalLineToRelative(2.0F)
|
||||
lineTo(19.3159F, 3.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, -2.0F, -2.0F)
|
||||
horizontalLineToRelative(-10.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, -2.0F, 2.0F)
|
||||
lineTo(5.3159F, 8.0F)
|
||||
horizontalLineToRelative(2.0F)
|
||||
close()
|
||||
moveTo(7.3159F, 3.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
lineTo(17.3159F, 4.0F)
|
||||
horizontalLineToRelative(-10.0F)
|
||||
close()
|
||||
moveTo(22.311F, 12.0F)
|
||||
lineToRelative(-3.0F, -3.0F)
|
||||
lineToRelative(0.0F, 2.0F)
|
||||
lineToRelative(-11.99F, 0.0F)
|
||||
lineToRelative(0.0F, 2.0F)
|
||||
lineToRelative(11.99F, 0.0F)
|
||||
lineToRelative(0.0F, 2.0F)
|
||||
lineToRelative(3.0F, -3.0F)
|
||||
close()
|
||||
moveTo(2.3206F, 11.0F)
|
||||
horizontalLineToRelative(1.5F)
|
||||
verticalLineToRelative(2.0F)
|
||||
horizontalLineToRelative(-1.5F)
|
||||
close()
|
||||
moveTo(4.8206F, 11.0F)
|
||||
horizontalLineToRelative(1.5F)
|
||||
verticalLineToRelative(2.0F)
|
||||
horizontalLineToRelative(-1.5F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconLeftToRightPreview() {
|
||||
Image(imageVector = EhIcons.Reader.LeftToRight, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
val EhIcons.Reader.Portrait by unsafeLazy {
|
||||
materialIcon(name = "Portrait") {
|
||||
materialPath {
|
||||
moveTo(17.0F, 1.01F)
|
||||
lineTo(7.0F, 1.0F)
|
||||
curveToRelative(-1.1F, 0.0F, -1.99F, 0.9F, -1.99F, 2.0F)
|
||||
verticalLineToRelative(18.0F)
|
||||
curveToRelative(0.0F, 1.1F, 0.89F, 2.0F, 1.99F, 2.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
curveToRelative(1.1F, 0.0F, 2.0F, -0.9F, 2.0F, -2.0F)
|
||||
verticalLineTo(3.0F)
|
||||
curveToRelative(0.0F, -1.1F, -0.9F, -1.99F, -2.0F, -1.99F)
|
||||
close()
|
||||
moveTo(17.0F, 19.0F)
|
||||
horizontalLineTo(7.0F)
|
||||
verticalLineTo(5.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
verticalLineToRelative(14.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconPortraitPreview() {
|
||||
Image(imageVector = EhIcons.Reader.Portrait, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
val EhIcons.Reader.PortraitLocked by unsafeLazy {
|
||||
materialIcon(name = "PortraitLocked") {
|
||||
materialPath {
|
||||
moveTo(10.0F, 16.0F)
|
||||
horizontalLineToRelative(4.0F)
|
||||
curveToRelative(0.55F, 0.0F, 1.0F, -0.45F, 1.0F, -1.0F)
|
||||
verticalLineToRelative(-3.0F)
|
||||
curveToRelative(0.0F, -0.55F, -0.45F, -1.0F, -1.0F, -1.0F)
|
||||
verticalLineToRelative(-1.0F)
|
||||
curveToRelative(0.0F, -1.11F, -0.9F, -2.0F, -2.0F, -2.0F)
|
||||
curveToRelative(-1.11F, 0.0F, -2.0F, 0.9F, -2.0F, 2.0F)
|
||||
verticalLineToRelative(1.0F)
|
||||
curveToRelative(-0.55F, 0.0F, -1.0F, 0.45F, -1.0F, 1.0F)
|
||||
verticalLineToRelative(3.0F)
|
||||
curveToRelative(0.0F, 0.55F, 0.45F, 1.0F, 1.0F, 1.0F)
|
||||
close()
|
||||
moveTo(10.8F, 10.0F)
|
||||
curveToRelative(0.0F, -0.66F, 0.54F, -1.2F, 1.2F, -1.2F)
|
||||
curveToRelative(0.66F, 0.0F, 1.2F, 0.54F, 1.2F, 1.2F)
|
||||
verticalLineToRelative(1.0F)
|
||||
horizontalLineToRelative(-2.4F)
|
||||
verticalLineToRelative(-1.0F)
|
||||
close()
|
||||
moveTo(17.0F, 1.0F)
|
||||
lineTo(7.0F, 1.0F)
|
||||
curveToRelative(-1.1F, 0.0F, -2.0F, 0.9F, -2.0F, 2.0F)
|
||||
verticalLineToRelative(18.0F)
|
||||
curveToRelative(0.0F, 1.1F, 0.9F, 2.0F, 2.0F, 2.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
curveToRelative(1.1F, 0.0F, 2.0F, -0.9F, 2.0F, -2.0F)
|
||||
lineTo(19.0F, 3.0F)
|
||||
curveToRelative(0.0F, -1.1F, -0.9F, -2.0F, -2.0F, -2.0F)
|
||||
close()
|
||||
moveTo(17.0F, 19.0F)
|
||||
lineTo(7.0F, 19.0F)
|
||||
lineTo(7.0F, 5.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
verticalLineToRelative(14.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconPortraitLockedPreview() {
|
||||
Image(imageVector = EhIcons.Reader.PortraitLocked, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@Suppress("BooleanLiteralArgument")
|
||||
val EhIcons.Reader.RightToLeft by unsafeLazy {
|
||||
materialIcon(name = "RightToLeft") {
|
||||
materialPath {
|
||||
moveTo(7.0F, 6.0F)
|
||||
horizontalLineTo(17.0F)
|
||||
verticalLineTo(8.0F)
|
||||
horizontalLineToRelative(2.0F)
|
||||
verticalLineTo(3.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, -2.0F, -2.0F)
|
||||
horizontalLineTo(7.0F)
|
||||
arcTo(2.0059F, 2.0059F, 0.0F, false, false, 5.0F, 3.0F)
|
||||
verticalLineTo(8.0F)
|
||||
horizontalLineTo(7.0F)
|
||||
close()
|
||||
moveTo(7.0F, 3.0F)
|
||||
horizontalLineTo(17.0F)
|
||||
verticalLineTo(4.0F)
|
||||
horizontalLineTo(7.0F)
|
||||
close()
|
||||
moveTo(17.0F, 18.0F)
|
||||
lineTo(7.0F, 18.0F)
|
||||
lineTo(7.0F, 16.0F)
|
||||
lineTo(5.0F, 16.0F)
|
||||
verticalLineToRelative(5.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, 2.0F)
|
||||
lineTo(17.0F, 23.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, -2.0F)
|
||||
lineTo(19.0F, 16.0F)
|
||||
lineTo(17.0F, 16.0F)
|
||||
close()
|
||||
moveTo(17.0F, 21.0F)
|
||||
lineTo(7.0F, 21.0F)
|
||||
lineTo(7.0F, 20.0F)
|
||||
lineTo(17.0F, 20.0F)
|
||||
close()
|
||||
moveTo(5.005F, 13.0F)
|
||||
lineToRelative(11.99F, 0.0F)
|
||||
lineToRelative(0.0F, -2.0F)
|
||||
lineToRelative(-11.99F, 0.0F)
|
||||
lineToRelative(0.0F, -2.0F)
|
||||
lineToRelative(-3.0F, 3.0F)
|
||||
lineToRelative(3.0F, 3.0F)
|
||||
lineToRelative(0.0F, -2.0F)
|
||||
close()
|
||||
moveTo(20.4953F, 11.0F)
|
||||
horizontalLineToRelative(1.5F)
|
||||
verticalLineToRelative(2.0F)
|
||||
horizontalLineToRelative(-1.5F)
|
||||
close()
|
||||
moveTo(17.9953F, 11.0F)
|
||||
horizontalLineToRelative(1.5F)
|
||||
verticalLineToRelative(2.0F)
|
||||
horizontalLineToRelative(-1.5F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconRightToLeftPreview() {
|
||||
Image(imageVector = EhIcons.Reader.RightToLeft, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
val EhIcons.Reader.ScreenRotation by unsafeLazy {
|
||||
materialIcon(name = "ScreenRotation") {
|
||||
materialPath {
|
||||
moveTo(16.48F, 2.52F)
|
||||
curveToRelative(3.27F, 1.55F, 5.61F, 4.72F, 5.97F, 8.48F)
|
||||
horizontalLineToRelative(1.5F)
|
||||
curveTo(23.44F, 4.84F, 18.29F, 0.0F, 12.0F, 0.0F)
|
||||
lineToRelative(-0.66F, 0.03F)
|
||||
lineToRelative(3.81F, 3.81F)
|
||||
lineToRelative(1.33F, -1.32F)
|
||||
close()
|
||||
moveTo(10.23F, 1.75F)
|
||||
curveToRelative(-0.59F, -0.59F, -1.54F, -0.59F, -2.12F, 0.0F)
|
||||
lineTo(1.75F, 8.11F)
|
||||
curveToRelative(-0.59F, 0.59F, -0.59F, 1.54F, 0.0F, 2.12F)
|
||||
lineToRelative(12.02F, 12.02F)
|
||||
curveToRelative(0.59F, 0.59F, 1.54F, 0.59F, 2.12F, 0.0F)
|
||||
lineToRelative(6.36F, -6.36F)
|
||||
curveToRelative(0.59F, -0.59F, 0.59F, -1.54F, 0.0F, -2.12F)
|
||||
lineTo(10.23F, 1.75F)
|
||||
close()
|
||||
moveTo(14.83F, 21.19F)
|
||||
lineTo(2.81F, 9.17F)
|
||||
lineToRelative(6.36F, -6.36F)
|
||||
lineToRelative(12.02F, 12.02F)
|
||||
lineToRelative(-6.36F, 6.36F)
|
||||
close()
|
||||
moveTo(7.52F, 21.48F)
|
||||
curveTo(4.25F, 19.94F, 1.91F, 16.76F, 1.55F, 13.0F)
|
||||
lineTo(0.05F, 13.0F)
|
||||
curveTo(0.56F, 19.16F, 5.71F, 24.0F, 12.0F, 24.0F)
|
||||
lineToRelative(0.66F, -0.03F)
|
||||
lineToRelative(-3.81F, -3.81F)
|
||||
lineToRelative(-1.33F, 1.32F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconScreenRotationPreview() {
|
||||
Image(imageVector = EhIcons.Reader.ScreenRotation, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@Suppress("BooleanLiteralArgument")
|
||||
val EhIcons.Reader.Vertical by unsafeLazy {
|
||||
materialIcon(name = "Vertical") {
|
||||
materialPath {
|
||||
moveTo(17.0F, 1.0F)
|
||||
lineTo(7.0F, 1.0F)
|
||||
arcTo(2.0059F, 2.0059F, 0.0F, false, false, 5.0F, 3.0F)
|
||||
lineTo(5.0F, 21.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, 2.0F)
|
||||
lineTo(17.0F, 23.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, -2.0F)
|
||||
lineTo(19.0F, 3.0F)
|
||||
arcTo(2.0059F, 2.0059F, 0.0F, false, false, 17.0F, 1.0F)
|
||||
close()
|
||||
moveTo(17.0F, 3.0F)
|
||||
verticalLineToRelative(1.0462F)
|
||||
lineTo(7.0F, 4.0462F)
|
||||
lineTo(7.0F, 3.0F)
|
||||
close()
|
||||
moveTo(17.0F, 6.0462F)
|
||||
verticalLineToRelative(12.0F)
|
||||
lineTo(7.0F, 18.0462F)
|
||||
verticalLineToRelative(-12.0F)
|
||||
close()
|
||||
moveTo(7.0F, 21.0F)
|
||||
verticalLineToRelative(-0.9538F)
|
||||
lineTo(17.0F, 20.0462F)
|
||||
lineTo(17.0F, 21.0F)
|
||||
close()
|
||||
moveTo(9.0F, 13.0F)
|
||||
lineToRelative(3.0F, 3.0F)
|
||||
lineToRelative(3.0F, -3.0F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
lineToRelative(0.0F, -5.0F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
lineToRelative(0.0F, 5.0F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconVerticalPreview() {
|
||||
Image(imageVector = EhIcons.Reader.Vertical, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.hippo.ehviewer.icons.reader
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@Suppress("BooleanLiteralArgument")
|
||||
val EhIcons.Reader.Webtoon by unsafeLazy {
|
||||
materialIcon(name = "Webtoon") {
|
||||
materialPath {
|
||||
moveTo(11.992F, 19.0F)
|
||||
lineToRelative(3.0F, -3.0F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
lineToRelative(0.0F, -11.0F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
lineToRelative(0.0F, 11.0F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
lineToRelative(3.0F, 3.0F)
|
||||
close()
|
||||
moveTo(17.0F, 1.0F)
|
||||
lineTo(7.0F, 1.0F)
|
||||
arcTo(2.0059F, 2.0059F, 0.0F, false, false, 5.0F, 3.0F)
|
||||
lineTo(5.0F, 21.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, 2.0F)
|
||||
lineTo(17.0F, 23.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, -2.0F)
|
||||
lineTo(19.0F, 3.0F)
|
||||
arcTo(2.0059F, 2.0059F, 0.0F, false, false, 17.0F, 1.0F)
|
||||
close()
|
||||
moveTo(17.0F, 21.0F)
|
||||
lineTo(7.0F, 21.0F)
|
||||
lineTo(7.0F, 3.0F)
|
||||
lineTo(17.0F, 3.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconWebtoonPreview() {
|
||||
Image(imageVector = EhIcons.Reader.Webtoon, contentDescription = null)
|
||||
}
|
||||
@@ -30,8 +30,8 @@ import coil3.imageLoader
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.size.Dimension
|
||||
import coil3.size.Precision
|
||||
import coil3.size.Scale
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.coil.BitmapImageWithRect
|
||||
import com.hippo.ehviewer.coil.maybeCropBorder
|
||||
@@ -73,15 +73,13 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 2
|
||||
private val targetHeight = appCtx.resources.displayMetrics.heightPixels * 2
|
||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
|
||||
|
||||
private suspend fun Either<ByteBufferSource, UniFileSource>.decodeCoil(): CoilImage {
|
||||
val req = appCtx.imageRequest {
|
||||
onLeft { data(it.source) }
|
||||
onRight { data(it.source.uri) }
|
||||
size(targetWidth, targetHeight)
|
||||
scale(Scale.FILL)
|
||||
size(Dimension(targetWidth), Dimension.Undefined)
|
||||
precision(Precision.INEXACT)
|
||||
maybeCropBorder(Settings.cropBorder.value)
|
||||
memoryCachePolicy(CachePolicy.DISABLED)
|
||||
@@ -94,7 +92,7 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
|
||||
suspend fun decode(src: ImageSource): Image? {
|
||||
return runCatching {
|
||||
when (src) {
|
||||
val image = when (src) {
|
||||
is UniFileSource -> {
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
src.source.openFileDescriptor("rw").use {
|
||||
@@ -112,20 +110,19 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
}
|
||||
}
|
||||
}
|
||||
val image = src.right().decodeCoil()
|
||||
if (image is BitmapImage) src.close()
|
||||
Image(image, src)
|
||||
src.right().decodeCoil()
|
||||
}
|
||||
|
||||
is ByteBufferSource -> {
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
rewriteGifSource(src.source)
|
||||
}
|
||||
val image = src.left().decodeCoil()
|
||||
if (image is BitmapImage) src.close()
|
||||
Image(image, src)
|
||||
src.left().decodeCoil()
|
||||
}
|
||||
}
|
||||
Image(image, src).apply {
|
||||
if (innerImage is BitmapImage) src.close()
|
||||
}
|
||||
}.onFailure {
|
||||
src.close()
|
||||
logcat(it)
|
||||
|
||||
+44
-84
@@ -24,39 +24,30 @@ import java.nio.ByteBuffer
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.chromium.net.CronetException
|
||||
import org.chromium.net.UploadDataProvider
|
||||
import org.chromium.net.UploadDataSink
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
import org.chromium.net.apihelpers.UploadDataProviders
|
||||
|
||||
object CronetEngine : HttpClientEngineBase("Cronet") {
|
||||
override val config = HttpClientEngineConfig()
|
||||
override val dispatcher = cronetDispatcher
|
||||
|
||||
@InternalAPI
|
||||
override suspend fun execute(data: HttpRequestData): HttpResponseData {
|
||||
val callContext = callContext()
|
||||
override suspend fun execute(data: HttpRequestData) = executeHttpRequest(callContext(), data)
|
||||
|
||||
return executeHttpRequest(callContext, data)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private suspend fun executeHttpRequest(
|
||||
callContext: CoroutineContext,
|
||||
data: HttpRequestData,
|
||||
): HttpResponseData = suspendCancellableCoroutine { continuation ->
|
||||
) = suspendCancellableCoroutine { continuation ->
|
||||
val requestTime = GMTDate()
|
||||
|
||||
val callback = object : UrlRequest.Callback() {
|
||||
lateinit var chunkChan: Channel<ByteBuffer>
|
||||
val chunkChan = Channel<ByteBuffer>()
|
||||
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
|
||||
continuation.resume(
|
||||
info.toHttpResponseData(
|
||||
@@ -67,23 +58,21 @@ object CronetEngine : HttpClientEngineBase("Cronet") {
|
||||
}
|
||||
|
||||
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
|
||||
val channel = GlobalScope.writer(callContext) {
|
||||
pool.useInstance {
|
||||
chunkChan = Channel(1)
|
||||
request.read(it)
|
||||
chunkChan.consumeEach { buffer ->
|
||||
buffer.flip()
|
||||
channel.writeFully(buffer)
|
||||
buffer.clear()
|
||||
request.read(buffer)
|
||||
}
|
||||
}
|
||||
}.channel
|
||||
continuation.resume(
|
||||
info.toHttpResponseData(
|
||||
requestTime = requestTime,
|
||||
callContext = callContext,
|
||||
responseBody = channel,
|
||||
responseBody = writer(callContext.job) {
|
||||
pool.useInstance {
|
||||
request.read(it)
|
||||
chunkChan.consumeEach { buffer ->
|
||||
buffer.flip()
|
||||
channel.writeFully(buffer)
|
||||
buffer.clear()
|
||||
request.read(buffer)
|
||||
}
|
||||
}
|
||||
}.channel,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -97,24 +86,24 @@ object CronetEngine : HttpClientEngineBase("Cronet") {
|
||||
}
|
||||
|
||||
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
|
||||
if (::chunkChan.isInitialized) {
|
||||
chunkChan.close(error)
|
||||
} else {
|
||||
if (continuation.isActive) {
|
||||
continuation.resumeWithException(error)
|
||||
} else {
|
||||
chunkChan.close(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val request = cronetHttpClient.newUrlRequestBuilder(data.url.toString(), callback, cronetHttpClientExecutor).apply {
|
||||
cronetHttpClient.newUrlRequestBuilder(data.url.toString(), callback, cronetHttpClientExecutor).apply {
|
||||
setHttpMethod(data.method.value)
|
||||
data.headers.flattenForEach { key, value -> addHeader(key, value) }
|
||||
data.body.contentType?.let { addHeader(HttpHeaders.ContentType, it.toString()) }
|
||||
data.body.contentLength?.let { addHeader(HttpHeaders.ContentLength, it.toString()) }
|
||||
data.body.toUploadDataProvider(callContext)?.let { setUploadDataProvider(it, cronetHttpClientExecutor) }
|
||||
}.build()
|
||||
request.start()
|
||||
callContext[Job]!!.invokeOnCompletion { request.cancel() }
|
||||
continuation.invokeOnCancellation { request.cancel() }
|
||||
data.body.toUploadDataProvider()?.let { setUploadDataProvider(it, cronetHttpClientExecutor) }
|
||||
}.build().apply {
|
||||
start()
|
||||
callContext.job.invokeOnCompletion { cancel() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,55 +111,26 @@ private fun UrlResponseInfo.toHttpResponseData(
|
||||
requestTime: GMTDate,
|
||||
callContext: CoroutineContext,
|
||||
responseBody: ByteReadChannel = ByteReadChannel.Empty,
|
||||
): HttpResponseData {
|
||||
return HttpResponseData(
|
||||
statusCode = HttpStatusCode.fromValue(httpStatusCode),
|
||||
requestTime = requestTime,
|
||||
headers = headers {
|
||||
allHeaders.forEach { (key, value) ->
|
||||
appendAll(key, value)
|
||||
}
|
||||
},
|
||||
version = when (negotiatedProtocol) {
|
||||
"h2" -> HttpProtocolVersion.HTTP_2_0
|
||||
"h3" -> HttpProtocolVersion.QUIC
|
||||
"quic/1+spdy/3" -> HttpProtocolVersion.SPDY_3
|
||||
else -> HttpProtocolVersion.HTTP_1_1
|
||||
},
|
||||
body = responseBody,
|
||||
callContext = callContext,
|
||||
)
|
||||
}
|
||||
) = HttpResponseData(
|
||||
statusCode = HttpStatusCode.fromValue(httpStatusCode),
|
||||
requestTime = requestTime,
|
||||
headers = headers {
|
||||
allHeaders.forEach { (key, value) ->
|
||||
appendAll(key, value)
|
||||
}
|
||||
},
|
||||
version = when (negotiatedProtocol) {
|
||||
"h2" -> HttpProtocolVersion.HTTP_2_0
|
||||
"h3" -> HttpProtocolVersion.QUIC
|
||||
"quic/1+spdy/3" -> HttpProtocolVersion.SPDY_3
|
||||
else -> HttpProtocolVersion.HTTP_1_1
|
||||
},
|
||||
body = responseBody,
|
||||
callContext = callContext,
|
||||
)
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun OutgoingContent.toUploadDataProvider(callContext: CoroutineContext): UploadDataProvider? = when (this) {
|
||||
private fun OutgoingContent.toUploadDataProvider() = when (this) {
|
||||
is OutgoingContent.NoContent -> null
|
||||
is OutgoingContent.ByteArrayContent -> object : UploadDataProvider() {
|
||||
val buffer = ByteBuffer.wrap(bytes()).slice()
|
||||
override fun getLength() = buffer.limit().toLong()
|
||||
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
|
||||
byteBuffer.put(buffer)
|
||||
uploadDataSink.onReadSucceeded(false)
|
||||
}
|
||||
override fun rewind(uploadDataSink: UploadDataSink) {
|
||||
buffer.position(0)
|
||||
uploadDataSink.onRewindSucceeded()
|
||||
}
|
||||
}
|
||||
is OutgoingContent.WriteChannelContent -> object : UploadDataProvider() {
|
||||
fun launch() = GlobalScope.writer(callContext) { writeTo(channel) }.channel
|
||||
var chan = launch()
|
||||
override fun getLength() = contentLength!!
|
||||
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
|
||||
GlobalScope.launch(callContext) {
|
||||
chan.readAvailable(byteBuffer)
|
||||
uploadDataSink.onReadSucceeded(false)
|
||||
}
|
||||
}
|
||||
override fun rewind(uploadDataSink: UploadDataSink) {
|
||||
chan = launch()
|
||||
uploadDataSink.onRewindSucceeded()
|
||||
}
|
||||
}
|
||||
is OutgoingContent.ByteArrayContent -> UploadDataProviders.create(bytes())
|
||||
else -> error("UnsupportedContentType $this")
|
||||
}
|
||||
@@ -99,7 +99,6 @@ fun ComicInfo.toSimpleTags() = listOfNotNull(
|
||||
|
||||
fun ComicInfo.write(file: UniFile) {
|
||||
file.openOutputStream().use {
|
||||
it.channel.truncate(0)
|
||||
OutputStreamWriter(it, Charsets.UTF_8).use { writer ->
|
||||
xmlStreaming.newGenericWriter(writer).use { xmlWriter ->
|
||||
xml.encodeToWriter(xmlWriter, ComicInfo.serializer(), this)
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.ehRequest
|
||||
import com.hippo.ehviewer.client.executeSafely
|
||||
import com.hippo.ehviewer.client.getImageKey
|
||||
import com.hippo.ehviewer.coil.read
|
||||
import com.hippo.ehviewer.coil.suspendEdit
|
||||
@@ -157,7 +158,7 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
notifyProgress(total!!, done, (done - prev).toInt())
|
||||
prev = done
|
||||
}
|
||||
}.execute {
|
||||
}.executeSafely {
|
||||
if (it.status.isSuccess()) {
|
||||
saveFromHttpResponse(index, it)
|
||||
} else {
|
||||
@@ -191,8 +192,7 @@ class SpiderDen(val info: GalleryInfo) {
|
||||
val extension = MimeTypeMap.getFileExtensionFromUrl(url).ifEmpty { "jpg" }
|
||||
return saveResponseMeta(index, extension) { outFile ->
|
||||
outFile.openOutputStream().use {
|
||||
val chan = it.channel.apply { truncate(0) }
|
||||
response.bodyAsChannel().copyTo(chan)
|
||||
response.bodyAsChannel().copyTo(it.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,13 +547,20 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
|
||||
private fun doLaunchDownloadJob(index: Int, force: Boolean, orgImg: Boolean = false) {
|
||||
val state = mPageStateArray[index]
|
||||
if (!force && state == STATE_FINISHED) return
|
||||
if (!force) {
|
||||
if (state == STATE_FINISHED) return
|
||||
if (index in mSpiderDen) return updatePageState(index, STATE_FINISHED)
|
||||
}
|
||||
val currentJob = mFetcherJobMap[index]
|
||||
val skipHath = force && !orgImg && currentJob?.isActive == true
|
||||
if (force) currentJob?.cancel(CancellationException(FORCE_RETRY))
|
||||
if (currentJob?.isActive != true) {
|
||||
mFetcherJobMap[index] = launch {
|
||||
runCatching {
|
||||
delayLock.withLock {
|
||||
delay(mDownloadDelay - lastRequestTime.elapsedNow())
|
||||
lastRequestTime = TimeSource.Monotonic.markNow()
|
||||
}
|
||||
mSemaphore.withPermit {
|
||||
doInJob(index, force, orgImg, skipHath)
|
||||
}
|
||||
@@ -591,19 +598,14 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
?: getPTokenFromMultiPageViewer(index)
|
||||
}
|
||||
updatePageState(index, STATE_DOWNLOADING)
|
||||
if (!force && index in mSpiderDen) {
|
||||
return updatePageState(index, STATE_FINISHED)
|
||||
}
|
||||
if (force) {
|
||||
pTokenLock.withLock {
|
||||
val pToken = mSpiderInfo.pTokenMap[index]
|
||||
if (pToken == TOKEN_FAILED) mSpiderInfo.pTokenMap.remove(index)
|
||||
}
|
||||
}
|
||||
val previousPToken: String?
|
||||
val pToken: String
|
||||
|
||||
pTokenLock.withLock {
|
||||
if (force) {
|
||||
if (mSpiderInfo.pTokenMap[index] == TOKEN_FAILED) {
|
||||
mSpiderInfo.pTokenMap.remove(index)
|
||||
}
|
||||
}
|
||||
pToken = getPToken(index)
|
||||
?: return updatePageState(index, STATE_FAILED, PTOKEN_FAILED_MESSAGE).also {
|
||||
mSpiderInfo.pTokenMap[index] = TOKEN_FAILED
|
||||
@@ -611,11 +613,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
previousPToken = getPToken(index - 1)
|
||||
}
|
||||
|
||||
delayLock.withLock {
|
||||
delay(mDownloadDelay - lastRequestTime.elapsedNow())
|
||||
lastRequestTime = TimeSource.Monotonic.markNow()
|
||||
}
|
||||
|
||||
var skipHathKey: String? = null
|
||||
var originImageUrl: String? = null
|
||||
var error: String? = null
|
||||
@@ -738,31 +735,37 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
val currentJob = mDecodeJobMap[index]
|
||||
if (currentJob?.isActive != true) {
|
||||
mDecodeJobMap[index] = launch {
|
||||
doInJob(index)
|
||||
runCatching {
|
||||
mSemaphore.withPermit {
|
||||
mFetcherJobMap[index]?.join()
|
||||
if (mPageStateArray[index] == STATE_FINISHED) {
|
||||
doInJob(index)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
if (mPageStateArray[index] == STATE_FINISHED) {
|
||||
notifyGetImageFailure(index, DECODE_ERROR)
|
||||
}
|
||||
if (it is CancellationException) {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doInJob(index: Int) {
|
||||
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
|
||||
val src = mSpiderDen.getImageSource(index) ?: return
|
||||
val image = Image.decode(src)
|
||||
checkNotNull(image)
|
||||
runCatching {
|
||||
val src = mSpiderDen.getImageSource(index) ?: return
|
||||
val image = mSemaphore.withPermit { Image.decode(src) }
|
||||
checkNotNull(image)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image.recycle()
|
||||
throw it
|
||||
}
|
||||
notifyGetImageSuccess(index, image)
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
notifyGetImageFailure(index, DECODE_ERROR)
|
||||
if (it is CancellationException) {
|
||||
throw it
|
||||
}
|
||||
image.recycle()
|
||||
throw it
|
||||
}
|
||||
notifyGetImageSuccess(index, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +314,11 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
|
||||
val selected = showSelectItemWithIcon(items, EhUtils.getSuitableTitle(info))
|
||||
with(findActivity<MainActivity>()) {
|
||||
when (selected) {
|
||||
0 -> navToReader(info)
|
||||
0 -> {
|
||||
EhDB.putHistoryInfo(info)
|
||||
navToReader(info)
|
||||
}
|
||||
|
||||
1 -> withUIContext {
|
||||
if (downloaded) {
|
||||
confirmRemoveDownload(info)
|
||||
|
||||
@@ -49,27 +49,30 @@ import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Whatshot
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.DrawerState2
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.ModalSideDrawer
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ShapeDefaults
|
||||
import androidx.compose.material3.SideDrawer
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.material3.rememberDrawerState2
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.currentCompositeKeyHash
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -79,8 +82,14 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.BlurEffect
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
@@ -90,6 +99,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
@@ -117,7 +127,7 @@ import com.hippo.ehviewer.ui.destinations.WhatshotScreenDestination
|
||||
import com.hippo.ehviewer.ui.screen.asDst
|
||||
import com.hippo.ehviewer.ui.screen.asDstWith
|
||||
import com.hippo.ehviewer.ui.screen.navWithUrl
|
||||
import com.hippo.ehviewer.ui.screen.navigate
|
||||
import com.hippo.ehviewer.ui.screen.navigateTo
|
||||
import com.hippo.ehviewer.ui.settings.showNewVersion
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
|
||||
@@ -126,6 +136,7 @@ import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider
|
||||
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
||||
import com.hippo.ehviewer.updater.AppUpdater
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.calculateFraction
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
||||
import com.hippo.ehviewer.util.getUrlFromClipboard
|
||||
@@ -134,7 +145,7 @@ import com.hippo.ehviewer.util.isAtLeastS
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.sha1
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.rememberNavHostEngine
|
||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||
import com.ramcosta.composedestinations.utils.currentDestinationAsState
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
@@ -148,7 +159,7 @@ import moe.tarsin.coroutines.runSuspendCatching
|
||||
import splitties.systemservices.clipboardManager
|
||||
import splitties.systemservices.connectivityManager
|
||||
|
||||
private val navItems = arrayOf(
|
||||
private val navItems = arrayOf<Triple<DirectionDestinationSpec, Int, ImageVector>>(
|
||||
Triple(HomePageScreenDestination, R.string.homepage, Icons.Default.Home),
|
||||
Triple(SubscriptionScreenDestination, R.string.subscription, EhIcons.Default.Subscriptions),
|
||||
Triple(WhatshotScreenDestination, R.string.whats_hot, Icons.Default.Whatshot),
|
||||
@@ -203,7 +214,7 @@ class MainActivity : EhActivity() {
|
||||
val configuration = LocalConfiguration.current
|
||||
val dialogState = LocalDialogState.current
|
||||
val navDrawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
val sideSheetState = rememberDrawerState(DrawerValue.Closed)
|
||||
val sideSheetState = rememberDrawerState2(DrawerValue.Closed)
|
||||
val snackbarState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val navController = rememberNavController()
|
||||
@@ -225,7 +236,7 @@ class MainActivity : EhActivity() {
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
navController.navigate(DownloadScreenDestination)
|
||||
navController.navigateTo(DownloadScreenDestination)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,25 +270,24 @@ class MainActivity : EhActivity() {
|
||||
if ("text/plain" == type) {
|
||||
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (keyword != null && !navController.navWithUrl(keyword)) {
|
||||
navController.navigate(ListUrlBuilder(mKeyword = keyword).asDst())
|
||||
navController.navigateTo(ListUrlBuilder(mKeyword = keyword).asDst())
|
||||
}
|
||||
} else if (type != null && type.startsWith("image/")) {
|
||||
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
||||
if (null != uri) {
|
||||
withIOContext { uri.asUniFile().sha1() }?.let {
|
||||
navController.navigate(
|
||||
ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
|
||||
hash = it,
|
||||
).asDst(),
|
||||
)
|
||||
}
|
||||
val hash = withIOContext { uri.asUniFile().sha1() }
|
||||
navController.navigateTo(
|
||||
ListUrlBuilder(
|
||||
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
|
||||
hash = hash,
|
||||
).asDst(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
DownloadService.ACTION_START_DOWNLOADSCENE -> {
|
||||
val args = intent.getBundleExtra(DownloadService.ACTION_START_DOWNLOADSCENE_ARGS)
|
||||
navController.navigate(DownloadsScreenDestination)
|
||||
navController.navigateTo(DownloadsScreenDestination)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,7 +318,7 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
val snackMessage = stringResource(R.string.clipboard_gallery_url_snack_message)
|
||||
val snackAction = stringResource(R.string.clipboard_gallery_url_snack_action)
|
||||
LifecycleResumeEffect {
|
||||
LifecycleResumeEffect(scope) {
|
||||
val job = scope.launch {
|
||||
delay(300)
|
||||
val text = clipboardManager.getUrlFromClipboard(applicationContext)
|
||||
@@ -317,11 +327,11 @@ class MainActivity : EhActivity() {
|
||||
val result1 = GalleryDetailUrlParser.parse(text, false)
|
||||
var launch: (() -> Unit)? = null
|
||||
if (result1 != null) {
|
||||
launch = { navController.navigate(result1.gid asDstWith result1.token) }
|
||||
launch = { navController.navigateTo(result1.gid asDstWith result1.token) }
|
||||
}
|
||||
val result2 = GalleryPageUrlParser.parse(text, false)
|
||||
if (result2 != null) {
|
||||
launch = { navController.navigate(ProgressScreenDestination(result2.gid, result2.pToken, result2.page)) }
|
||||
launch = { navController.navigateTo(ProgressScreenDestination(result2.gid, result2.pToken, result2.page)) }
|
||||
}
|
||||
launch?.let {
|
||||
val ret = snackbarState.showSnackbar(snackMessage, snackAction, true)
|
||||
@@ -358,9 +368,11 @@ class MainActivity : EhActivity() {
|
||||
},
|
||||
) {
|
||||
LocalTouchSlopProvider(Settings.touchSlopFactor.toFloat()) {
|
||||
val minOffset = -with(density) { 360.dp.toPx() }
|
||||
ModalNavigationDrawer(
|
||||
drawerContent = {
|
||||
ModalDrawerSheet(
|
||||
drawerState = navDrawerState,
|
||||
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 56).dp),
|
||||
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Start),
|
||||
) {
|
||||
@@ -398,7 +410,13 @@ class MainActivity : EhActivity() {
|
||||
gesturesEnabled = !drawerLocked || navDrawerState.isOpen,
|
||||
) {
|
||||
val sheet = sideSheet.firstOrNull()
|
||||
SideDrawer(
|
||||
val radius by remember {
|
||||
snapshotFlow {
|
||||
val step = calculateFraction(minOffset, 0f, navDrawerState.currentOffset)
|
||||
with(density) { lerp(0, 10, step).dp.toPx() }
|
||||
}
|
||||
}.collectAsState(0f)
|
||||
ModalSideDrawer(
|
||||
drawerContent = {
|
||||
if (sheet != null) {
|
||||
ModalDrawerSheet(
|
||||
@@ -412,6 +430,13 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.graphicsLayer {
|
||||
if (radius != 0f) {
|
||||
renderEffect = BlurEffect(radius, radius, TileMode.Clamp)
|
||||
shape = RectangleShape
|
||||
clip = true
|
||||
}
|
||||
},
|
||||
drawerState = sideSheetState,
|
||||
gesturesEnabled = sheet != null && !drawerLocked,
|
||||
) {
|
||||
@@ -422,12 +447,8 @@ class MainActivity : EhActivity() {
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
navGraph = NavGraphs.root,
|
||||
startRoute = if (Settings.needSignIn) {
|
||||
SignInScreenDestination
|
||||
} else {
|
||||
StartDestination
|
||||
},
|
||||
engine = rememberNavHostEngine(rootDefaultAnimations = rememberEhNavAnim()),
|
||||
startRoute = if (Settings.needSignIn) SignInScreenDestination else StartDestination,
|
||||
defaultTransitions = rememberEhNavAnim(),
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
@@ -508,7 +529,7 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val LocalNavDrawerState = compositionLocalOf<DrawerState2> { error("CompositionLocal LocalNavDrawerState not present!") }
|
||||
val LocalNavDrawerState = compositionLocalOf<DrawerState> { error("CompositionLocal LocalNavDrawerState not present!") }
|
||||
val LocalSideSheetState = compositionLocalOf<DrawerState2> { error("CompositionLocal LocalSideSheetState not present!") }
|
||||
val LocalDrawerLockHandle = compositionLocalOf<SnapshotStateList<Int>> { error("CompositionLocal LocalDrawerLockHandle not present!") }
|
||||
val LocalSnackBarHostState = compositionLocalOf<SnackbarHostState> { error("CompositionLocal LocalSnackBarHostState not present!") }
|
||||
|
||||
@@ -128,7 +128,7 @@ fun Context.interceptSecurityOrReturn() {
|
||||
val lockedDelayTime = lockedResumeTime - lockedLastLeaveTime
|
||||
if (lockedDelayTime < Settings.securityDelay * 60) {
|
||||
locked = false
|
||||
} else if (Settings.security && isAuthenticationSupported() && locked) {
|
||||
} else if (Settings.security.value && isAuthenticationSupported() && locked) {
|
||||
startActivity(Intent(this, SecurityActivity::class.java))
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,21 @@ package com.hippo.ehviewer.ui
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.hippo.ehviewer.ui.theme.EhTheme
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.ProvideVectorPainterCache
|
||||
import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
import soup.compose.material.motion.animation.materialSharedAxisXIn
|
||||
import soup.compose.material.motion.animation.materialSharedAxisXOut
|
||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||
@@ -46,14 +50,14 @@ inline fun ComponentActivity.setMD3Content(crossinline content: @Composable () -
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberEhNavAnim(): RootNavGraphDefaultAnimations {
|
||||
fun rememberEhNavAnim(): NavHostAnimatedDestinationStyle {
|
||||
val slideDistance = rememberSlideDistance()
|
||||
return remember {
|
||||
RootNavGraphDefaultAnimations(
|
||||
enterTransition = { materialSharedAxisXIn(true, slideDistance) },
|
||||
exitTransition = { materialSharedAxisXOut(true, slideDistance) },
|
||||
popEnterTransition = { materialSharedAxisXIn(false, slideDistance) },
|
||||
popExitTransition = { materialSharedAxisXOut(false, slideDistance) },
|
||||
)
|
||||
return remember(slideDistance) {
|
||||
object : NavHostAnimatedDestinationStyle() {
|
||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = { materialSharedAxisXIn(true, slideDistance) }
|
||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = { materialSharedAxisXOut(true, slideDistance) }
|
||||
override val popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = { materialSharedAxisXIn(false, slideDistance) }
|
||||
override val popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = { materialSharedAxisXOut(false, slideDistance) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,167 +14,44 @@ import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.hippo.ehviewer.R
|
||||
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.ui.LockDrawer
|
||||
import com.hippo.ehviewer.ui.StartDestination
|
||||
import com.hippo.ehviewer.ui.destinations.WebViewSignInScreenDestination
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
@RootNavGraph(start = true)
|
||||
@Destination
|
||||
@Destination<RootGraph>(start = true)
|
||||
@Composable
|
||||
fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
LockDrawer(true)
|
||||
val windowSizeClass = LocalWindowSizeClass.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val focusManager = LocalFocusManager.current
|
||||
var isProgressIndicatorVisible by rememberSaveable { mutableStateOf(false) }
|
||||
var showUsernameError by rememberSaveable { mutableStateOf(false) }
|
||||
var showPasswordError by rememberSaveable { mutableStateOf(false) }
|
||||
var username by rememberSaveable { mutableStateOf("") }
|
||||
var password by rememberSaveable { mutableStateOf("") }
|
||||
var passwordHidden by rememberSaveable { mutableStateOf(true) }
|
||||
val context = LocalContext.current
|
||||
var signInJob by remember { mutableStateOf<Job?>(null) }
|
||||
val dialogState = LocalDialogState.current
|
||||
|
||||
fun signIn() {
|
||||
if (signInJob?.isActive == true) return
|
||||
if (username.isEmpty()) {
|
||||
showUsernameError = true
|
||||
return
|
||||
} else {
|
||||
showUsernameError = false
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
showPasswordError = true
|
||||
return
|
||||
} else {
|
||||
showPasswordError = false
|
||||
}
|
||||
focusManager.clearFocus()
|
||||
isProgressIndicatorVisible = true
|
||||
|
||||
EhUtils.signOut()
|
||||
signInJob = coroutineScope.launchIO {
|
||||
runCatching {
|
||||
EhEngine.signIn(username, password)
|
||||
}.onFailure {
|
||||
withUIContext {
|
||||
focusManager.clearFocus()
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
confirmText = R.string.get_it,
|
||||
title = R.string.sign_in_failed,
|
||||
showCancelButton = false,
|
||||
text = {
|
||||
Text(
|
||||
"""
|
||||
${it.displayString()}
|
||||
${stringResource(R.string.sign_in_failed_tip)}
|
||||
""".trimIndent(),
|
||||
)
|
||||
},
|
||||
)
|
||||
isProgressIndicatorVisible = false
|
||||
}
|
||||
}.onSuccess {
|
||||
withNonCancellableContext { postLogin() }
|
||||
withUIContext { navigator.popNavigate(StartDestination) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UsernameAndPasswordTextField() {
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
modifier = Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
|
||||
label = { Text(stringResource(R.string.username)) },
|
||||
supportingText = { if (showUsernameError) Text(stringResource(R.string.error_username_cannot_empty)) },
|
||||
trailingIcon = { if (showUsernameError) Icon(imageVector = Icons.Filled.Info, contentDescription = null) },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
singleLine = true,
|
||||
isError = showUsernameError,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
visualTransformation = if (passwordHidden) PasswordVisualTransformation() else VisualTransformation.None,
|
||||
supportingText = { if (showPasswordError) Text(stringResource(R.string.error_password_cannot_empty)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
keyboardActions = KeyboardActions(onDone = { signIn() }),
|
||||
trailingIcon = {
|
||||
if (showPasswordError) {
|
||||
Icon(imageVector = Icons.Filled.Info, contentDescription = null)
|
||||
} else {
|
||||
IconButton(onClick = { passwordHidden = !passwordHidden }) {
|
||||
val visibilityIcon = if (passwordHidden) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
|
||||
Icon(imageVector = visibilityIcon, contentDescription = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
isError = showPasswordError,
|
||||
)
|
||||
}
|
||||
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
when (windowSizeClass.widthSizeClass) {
|
||||
@@ -188,7 +65,6 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin)),
|
||||
)
|
||||
UsernameAndPasswordTextField()
|
||||
Text(
|
||||
text = stringResource(id = R.string.app_waring),
|
||||
modifier = Modifier.widthIn(max = dimensionResource(id = R.dimen.single_max_width)).padding(top = 24.dp),
|
||||
@@ -199,7 +75,7 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
modifier = Modifier.widthIn(max = dimensionResource(id = R.dimen.single_max_width)).padding(top = 12.dp),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Row(modifier = Modifier.padding(top = dimensionResource(R.dimen.keyline_margin))) {
|
||||
FilledTonalButton(
|
||||
onClick = { context.openBrowser(EhUrl.URL_REGISTER) },
|
||||
@@ -208,44 +84,28 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
Text(text = stringResource(id = R.string.register))
|
||||
}
|
||||
Button(
|
||||
onClick = ::signIn,
|
||||
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.sign_in))
|
||||
}
|
||||
}
|
||||
Row(modifier = Modifier.padding(horizontal = 4.dp)) {
|
||||
TextButton(
|
||||
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
) {
|
||||
append(stringResource(id = R.string.sign_in_via_webview))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
Settings.needSignIn = false
|
||||
Settings.gallerySite = EhUrl.SITE_E
|
||||
navigator.popNavigate(StartDestination)
|
||||
TextButton(
|
||||
onClick = {
|
||||
Settings.needSignIn = false
|
||||
Settings.gallerySite = EhUrl.SITE_E
|
||||
navigator.popNavigate(StartDestination)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
) {
|
||||
append(stringResource(id = R.string.guest_mode))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
) {
|
||||
append(stringResource(id = R.string.guest_mode))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,11 +139,10 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
UsernameAndPasswordTextField()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
Row(horizontalArrangement = Arrangement.Center) {
|
||||
Button(
|
||||
onClick = ::signIn,
|
||||
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
|
||||
modifier = Modifier.padding(horizontal = 4.dp).width(128.dp),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.sign_in))
|
||||
@@ -296,46 +155,27 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(horizontalArrangement = Arrangement.Center) {
|
||||
TextButton(
|
||||
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
) {
|
||||
append(stringResource(id = R.string.sign_in_via_webview))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
Settings.needSignIn = false
|
||||
Settings.gallerySite = EhUrl.SITE_E
|
||||
navigator.popNavigate(StartDestination)
|
||||
TextButton(
|
||||
onClick = {
|
||||
Settings.needSignIn = false
|
||||
Settings.gallerySite = EhUrl.SITE_E
|
||||
navigator.popNavigate(StartDestination)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
) {
|
||||
append(stringResource(id = R.string.guest_mode))
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
) {
|
||||
append(stringResource(id = R.string.guest_mode))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isProgressIndicatorVisible) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,14 @@ import com.hippo.ehviewer.ui.StartDestination
|
||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||
import com.hippo.ehviewer.util.setDefaultSettings
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import io.ktor.http.Url
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun WebViewSignInScreen(navigator: DestinationsNavigator) {
|
||||
LockDrawer(true)
|
||||
|
||||
@@ -82,7 +82,7 @@ fun SearchFilter(
|
||||
selected = isCategoryChecked(it.first),
|
||||
onClick = { onCategoryChanged(category xor it.first) },
|
||||
label = { Text(text = stringResource(id = it.second)) },
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
modifier = Modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package com.hippo.ehviewer.ui.reader
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Brightness5
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -25,7 +28,7 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.asMutableState
|
||||
|
||||
@Composable
|
||||
fun ColorFilterSetting() = Column {
|
||||
fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
|
||||
val customBrightness = Settings.customBrightness.asMutableState()
|
||||
SwitchChoice(
|
||||
title = stringResource(id = R.string.pref_custom_brightness),
|
||||
|
||||
@@ -43,7 +43,6 @@ fun GalleryPager(
|
||||
val items = pageLoader.pages
|
||||
when (type) {
|
||||
DEFAULT, LEFT_TO_RIGHT, RIGHT_TO_LEFT -> {
|
||||
val zoomableState = rememberZoomableState(zoomSpec = zoomSpec)
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier,
|
||||
@@ -51,6 +50,7 @@ fun GalleryPager(
|
||||
key = { it },
|
||||
) { index ->
|
||||
val page = items[index]
|
||||
val zoomableState = rememberZoomableState(zoomSpec = zoomSpec)
|
||||
val status by page.status.collectAsState()
|
||||
val boxModifier = if (status == Page.State.READY) {
|
||||
Modifier.zoomable(
|
||||
@@ -79,13 +79,13 @@ fun GalleryPager(
|
||||
}
|
||||
}
|
||||
VERTICAL -> {
|
||||
val zoomableState = rememberZoomableState(zoomSpec = zoomSpec)
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier,
|
||||
key = { it },
|
||||
) { index ->
|
||||
val page = items[index]
|
||||
val zoomableState = rememberZoomableState(zoomSpec = zoomSpec)
|
||||
val status by page.status.collectAsState()
|
||||
val boxModifier = if (status == Page.State.READY) {
|
||||
Modifier.zoomable(
|
||||
|
||||
@@ -2,8 +2,12 @@ package com.hippo.ehviewer.ui.reader
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.hippo.ehviewer.R
|
||||
@@ -11,7 +15,7 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.asMutableState
|
||||
|
||||
@Composable
|
||||
fun ReaderGeneralSetting() = Column {
|
||||
fun ReaderGeneralSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
|
||||
SpinnerChoice(
|
||||
title = stringResource(id = R.string.pref_reader_theme),
|
||||
entries = stringArrayResource(id = R.array.reader_themes),
|
||||
|
||||
@@ -4,8 +4,11 @@ import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -21,7 +24,7 @@ import com.hippo.ehviewer.collectAsState
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
|
||||
@Composable
|
||||
fun ReaderModeSetting() = Column {
|
||||
fun ReaderModeSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
|
||||
SpinnerChoice(
|
||||
title = stringResource(id = R.string.pref_category_reading_mode),
|
||||
entries = stringArrayResource(id = R.array.viewers_selector),
|
||||
|
||||
@@ -4,10 +4,10 @@ import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
@@ -42,6 +42,7 @@ import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.ui.composing
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.ui.reader.PageIndicatorText
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderAppBars
|
||||
@@ -53,7 +54,7 @@ import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.kt.unreachable
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
// Workaround for incorrect fling velocity on zoom gesture end
|
||||
@@ -112,7 +113,7 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { dispose() },
|
||||
sheetState = state,
|
||||
windowInsets = WindowInsets(0),
|
||||
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
|
||||
) {
|
||||
ReaderPageSheetMeta(
|
||||
retry = { pageLoader.retryPage(page.index) },
|
||||
@@ -123,7 +124,6 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
||||
saveTo = { launchIO { with(pageLoader) { saveTo(page) } } },
|
||||
dismiss = { launch { state.hide().also { dispose() } } },
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,12 +183,12 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
||||
// Yeah, I know color state should not be read here, but we have to do it...
|
||||
scrimColor = scrim,
|
||||
dragHandle = null,
|
||||
windowInsets = WindowInsets(0),
|
||||
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
|
||||
) {
|
||||
SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
|
||||
isColorFilter = page == 2
|
||||
appbarVisible = !isColorFilter
|
||||
}
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,13 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
||||
import androidx.compose.material3.SwipeToDismissBoxState
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
@@ -61,6 +63,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -111,6 +114,7 @@ import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.util.findActivity
|
||||
import com.hippo.ehviewer.util.mapToLongArray
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
@@ -123,7 +127,7 @@ import moe.tarsin.coroutines.onEachLatest
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
var gridView by Settings.gridView.asMutableState()
|
||||
@@ -142,6 +146,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
val activity = remember { findActivity<MainActivity>() }
|
||||
val density = LocalDensity.current
|
||||
val view = LocalView.current
|
||||
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
|
||||
val allName = stringResource(R.string.download_all)
|
||||
val defaultName = stringResource(R.string.default_download_label_name)
|
||||
val title = stringResource(
|
||||
@@ -261,22 +266,29 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
},
|
||||
)
|
||||
}
|
||||
itemsIndexed(labelList, key = { _, item -> item.label }) { index, (item) ->
|
||||
val dismissState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = {
|
||||
itemsIndexed(labelList, key = { _, item -> item.id!! }) { index, (item, _, id) ->
|
||||
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
||||
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
|
||||
LaunchedEffect(dismissState) {
|
||||
snapshotFlow { dismissState.currentValue }.collect {
|
||||
if (it == SwipeToDismissBoxValue.EndToStart) {
|
||||
launch {
|
||||
runCatching {
|
||||
awaitPermissionOrCancel(confirmText = R.string.delete) {
|
||||
Text(text = stringResource(R.string.delete_label, item))
|
||||
}
|
||||
}.onSuccess {
|
||||
DownloadManager.deleteLabel(item)
|
||||
when (filterState.label) {
|
||||
item -> switchLabel("")
|
||||
null -> invalidateKey = !invalidateKey
|
||||
}
|
||||
}.onFailure {
|
||||
dismissState.reset()
|
||||
}
|
||||
}
|
||||
true
|
||||
},
|
||||
)
|
||||
ReorderableItem(reorderableLabelState, key = item) { isDragging ->
|
||||
}
|
||||
}
|
||||
ReorderableItem(reorderableLabelState, key = id!!) { isDragging ->
|
||||
SwipeToDismissBox2(
|
||||
state = dismissState,
|
||||
backgroundContent = {},
|
||||
@@ -482,7 +494,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
) {
|
||||
items(list, key = { it.gid }) { info ->
|
||||
val checked = info.gid in checkedInfoMap
|
||||
CheckableItem(checked = checked, modifier = Modifier.animateItemPlacement()) { interactionSource ->
|
||||
CheckableItem(checked = checked, modifier = Modifier.animateItem()) { interactionSource ->
|
||||
DownloadCard(
|
||||
onClick = {
|
||||
if (selectMode) {
|
||||
@@ -564,12 +576,15 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
withUIContext { navToReader(list.random().galleryInfo) }
|
||||
}
|
||||
onClick(Icons.AutoMirrored.Default.Sort) {
|
||||
val selected = showSingleChoice(
|
||||
val oldMode = SortMode.from(sortMode)
|
||||
val (selected, checked) = showSelectItemWithCheckBox(
|
||||
sortModes.toList(),
|
||||
SortMode.All.indexOfFirst { it.flag == sortMode },
|
||||
R.string.sort_by,
|
||||
R.string.group_by_download_label,
|
||||
SortMode.All.indexOfFirst { it.field == oldMode.field && it.order == oldMode.order },
|
||||
oldMode.groupByDownloadLabel,
|
||||
)
|
||||
val mode = SortMode.All[selected]
|
||||
val mode = SortMode.All[selected].copy(groupByDownloadLabel = checked)
|
||||
DownloadManager.sortDownloads(mode)
|
||||
sortMode = mode.flag
|
||||
invalidateKey = !invalidateKey
|
||||
|
||||
@@ -80,6 +80,7 @@ import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.mapToLongArray
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
@@ -90,7 +91,7 @@ import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.onEachLatest
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
// Immutables
|
||||
@@ -230,6 +231,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
},
|
||||
onSearchHidden = { fabHidden = false },
|
||||
refreshState = refreshState,
|
||||
tagNamespace = !isLocalFav,
|
||||
searchBarOffsetY = { searchBarOffsetY },
|
||||
trailingIcon = {
|
||||
val sheetState = LocalSideSheetState.current
|
||||
|
||||
@@ -7,13 +7,14 @@ import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.widget.TextView
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -64,6 +65,7 @@ import androidx.compose.ui.platform.LocalTextToolbar
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.parseAsHtml
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.max
|
||||
@@ -90,7 +92,6 @@ import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
|
||||
import com.hippo.ehviewer.ui.tools.normalizeSpan
|
||||
import com.hippo.ehviewer.ui.tools.rememberBBCodeTextToolbar
|
||||
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
||||
import com.hippo.ehviewer.ui.tools.toAnnotatedString
|
||||
import com.hippo.ehviewer.ui.tools.toBBCode
|
||||
import com.hippo.ehviewer.ui.tools.updateSpan
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
@@ -98,6 +99,7 @@ import com.hippo.ehviewer.util.TextUrl
|
||||
import com.hippo.ehviewer.util.addTextToClipboard
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
@@ -143,7 +145,7 @@ private fun Context.generateComment(
|
||||
|
||||
private val MinimumContentPaddingEditText = 88.dp
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
LockDrawer(true)
|
||||
@@ -285,11 +287,7 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
||||
val additionalPadding = if (commenting) {
|
||||
editTextMeasured
|
||||
} else {
|
||||
if (!comments.hasMore) {
|
||||
MinimumContentPaddingEditText
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
0.dp
|
||||
}
|
||||
val voteUpSucceed = stringResource(R.string.vote_up_successfully)
|
||||
val cancelVoteUpSucceed = stringResource(R.string.cancel_vote_up_successfully)
|
||||
@@ -335,7 +333,7 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
||||
}
|
||||
if (comment.editable) {
|
||||
onSelect(editComment) {
|
||||
userComment = TextFieldValue(comment.comment.parseAsHtml().toAnnotatedString())
|
||||
userComment = TextFieldValue(comment.comment.parseAsHtml())
|
||||
commentId = comment.id
|
||||
commenting = true
|
||||
}
|
||||
@@ -358,7 +356,7 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
||||
}
|
||||
|
||||
GalleryCommentCard(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
modifier = Modifier.animateItem(),
|
||||
comment = item,
|
||||
onUserClick = {
|
||||
navigate(
|
||||
@@ -376,28 +374,36 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
||||
}
|
||||
if (comments.hasMore) {
|
||||
item {
|
||||
AnimatedVisibility(refreshing) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center).padding(keylineMargin),
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(!refreshing) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
launchIO {
|
||||
refreshing = true
|
||||
runSuspendCatching { refreshComment(true) }
|
||||
refreshing = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().padding(keylineMargin),
|
||||
Crossfade(targetState = refreshing, modifier = Modifier.padding(keylineMargin), label = "refreshing") {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().height(40.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.click_more_comments))
|
||||
if (it) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = {
|
||||
launchIO {
|
||||
refreshing = true
|
||||
runSuspendCatching { refreshComment(true) }
|
||||
refreshing = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.click_more_comments))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Workaround for comment list lagging when reaching the bottom
|
||||
// https://github.com/FooIbar/EhViewer/issues/994
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(MinimumContentPaddingEditText))
|
||||
}
|
||||
}
|
||||
}
|
||||
PullToRefreshContainer(
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.os.Parcelable
|
||||
import android.text.TextUtils.TruncateAt.END
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.MutatorMutex
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -155,6 +156,7 @@ import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.requestPermission
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
@@ -163,8 +165,6 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import splitties.systemservices.downloadManager
|
||||
@@ -211,7 +211,7 @@ private fun List<GalleryTagGroup>.getArtistTag(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
LockDrawer(true)
|
||||
@@ -484,7 +484,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
navigator.navigate(info.gid asDstWith info.token)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
if (galleryDetail.newerVersions.isNotEmpty()) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
CrystalCard(
|
||||
@@ -494,7 +494,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
}
|
||||
Text(text = stringResource(id = R.string.newer_version_available))
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||
@@ -506,32 +506,28 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
} else {
|
||||
stringResource(id = R.string.not_favorited)
|
||||
}
|
||||
val favoritesLock = remember { Mutex() }
|
||||
val favoritesLock = remember { MutatorMutex() }
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
val removeSucceed = stringResource(R.string.remove_from_favorite_success)
|
||||
val addSucceed = stringResource(R.string.add_to_favorite_success)
|
||||
val removeFailed = stringResource(R.string.remove_from_favorite_failure)
|
||||
// val removeFailed = stringResource(R.string.remove_from_favorite_failure)
|
||||
val addFailed = stringResource(R.string.add_to_favorite_failure)
|
||||
FilledTertiaryIconToggleButton(
|
||||
checked = favSlot != NOT_FAVORITED,
|
||||
onCheckedChange = {
|
||||
launchIO {
|
||||
favoritesLock.withLock {
|
||||
var remove = false
|
||||
favoritesLock.mutate {
|
||||
runSuspendCatching {
|
||||
remove = !modifyFavorites(galleryDetail.galleryInfo)
|
||||
}.onSuccess {
|
||||
if (remove) {
|
||||
showSnackbar(removeSucceed)
|
||||
} else {
|
||||
modifyFavorites(galleryDetail.galleryInfo)
|
||||
}.onSuccess { add ->
|
||||
if (add) {
|
||||
showSnackbar(addSucceed)
|
||||
} else {
|
||||
showSnackbar(removeSucceed)
|
||||
}
|
||||
}.onFailure {
|
||||
if (remove) {
|
||||
showSnackbar(removeFailed)
|
||||
} else {
|
||||
showSnackbar(addFailed)
|
||||
}
|
||||
// TODO: We don't know if it's add or remove
|
||||
showSnackbar(addFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -644,7 +640,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
fun getAllRatingText(rating: Float, ratingCount: Int): String {
|
||||
return getString(
|
||||
R.string.rating_text,
|
||||
@@ -669,7 +665,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
var rating by remember { mutableFloatStateOf(galleryDetail.rating.coerceAtLeast(.5f)) }
|
||||
var text by remember { mutableIntStateOf(getRatingText(rating)) }
|
||||
Text(text = stringResource(id = text), style = MaterialTheme.typography.bodyLarge)
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
GalleryRatingBar(
|
||||
rating = rating,
|
||||
onRatingChange = {
|
||||
@@ -700,11 +696,11 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
GalleryDetailRating(rating = galleryDetail.rating)
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
Text(text = ratingText)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
val tags = galleryDetail.tags
|
||||
if (tags.isEmpty()) {
|
||||
Box(
|
||||
@@ -765,7 +761,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
Spacer(modifier = Modifier.size(keylineMargin))
|
||||
if (Settings.showComments) {
|
||||
GalleryDetailComment(galleryDetail.comments.comments)
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.strip_item_padding_v)))
|
||||
@@ -899,7 +895,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
BelowHeader(galleryDetail)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize().padding(keylineMargin),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
@@ -959,7 +955,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
||||
BelowHeader(galleryDetail)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize().padding(keylineMargin),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
|
||||
@@ -46,11 +46,12 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
||||
import androidx.compose.material3.SwipeToDismissBoxState
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -62,6 +63,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -138,6 +140,7 @@ import com.hippo.ehviewer.util.pickVisualMedia
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.sha1
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.spec.Direction
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
@@ -152,23 +155,23 @@ import moe.tarsin.coroutines.runSuspendCatching
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun HomePageScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(), navigator)
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SubscriptionScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_SUBSCRIPTION), navigator)
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun WhatshotScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_WHATS_HOT), navigator)
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun ToplistScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_TOPLIST, mKeyword = Settings.recentToplist), navigator)
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
val searchFieldState = rememberTextFieldState()
|
||||
@@ -201,6 +204,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
||||
onBack = { showSearchLayout = false },
|
||||
)
|
||||
val density = LocalDensity.current
|
||||
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
|
||||
val listState = rememberLazyGridState()
|
||||
val gridState = rememberLazyStaggeredGridState()
|
||||
val isTopList = remember(urlBuilder) { urlBuilder.mode == MODE_TOPLIST }
|
||||
@@ -385,21 +389,30 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
||||
}
|
||||
itemsIndexed(quickSearchList, key = { _, item -> item.id!! }) { index, item ->
|
||||
ReorderableItem(reorderableLazyListState, key = item.id!!) { isDragging ->
|
||||
val dismissState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = { value ->
|
||||
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
||||
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
|
||||
LaunchedEffect(dismissState) {
|
||||
snapshotFlow { dismissState.currentValue }.collect { value ->
|
||||
if (value == SwipeToDismissBoxValue.EndToStart) {
|
||||
with(quickSearchList) {
|
||||
val removeIndex = indexOf(item)
|
||||
subList(removeIndex + 1, size).forEach {
|
||||
it.position--
|
||||
runCatching {
|
||||
awaitPermissionOrCancel(confirmText = R.string.delete) {
|
||||
Text(text = stringResource(R.string.delete_quick_search, item.name))
|
||||
}
|
||||
removeAt(removeIndex)
|
||||
}.onSuccess {
|
||||
EhDB.deleteQuickSearch(item)
|
||||
with(quickSearchList) {
|
||||
val removeIndex = indexOf(item)
|
||||
subList(removeIndex + 1, size).forEach {
|
||||
it.position--
|
||||
}
|
||||
removeAt(removeIndex)
|
||||
}
|
||||
}.onFailure {
|
||||
dismissState.reset()
|
||||
}
|
||||
launchIO { EhDB.deleteQuickSearch(item) }
|
||||
}
|
||||
true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
SwipeToDismissBox2(
|
||||
state = dismissState,
|
||||
backgroundContent = {},
|
||||
|
||||
@@ -51,11 +51,12 @@ import com.hippo.ehviewer.ui.tools.getOffset
|
||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.util.flattenForEach
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun GalleryPreviewScreen(detail: GalleryDetail, toNextPage: Boolean, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
LockDrawer(true)
|
||||
|
||||
@@ -56,12 +56,13 @@ import com.hippo.ehviewer.ui.tools.SwipeToDismissBox2
|
||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun HistoryScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
val title = stringResource(id = R.string.history)
|
||||
@@ -141,7 +142,7 @@ fun HistoryScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||
SwipeToDismissBox2(
|
||||
state = dismissState,
|
||||
backgroundContent = {},
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
modifier = Modifier.animateItem(),
|
||||
) {
|
||||
GalleryInfoListItem(
|
||||
onClick = { navigate(info.asDst()) },
|
||||
|
||||
@@ -40,12 +40,12 @@ fun DestinationsNavigator.popNavigate(direction: Direction) = navigate(direction
|
||||
@MainThread
|
||||
fun NavController.navWithUrl(url: String): Boolean {
|
||||
val dest = urlToDestination(url) ?: return false
|
||||
navigate(dest)
|
||||
navigateTo(dest)
|
||||
return true
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun NavController.navigate(direction: Direction) = navigate(direction.route)
|
||||
fun NavController.navigateTo(direction: Direction) = navigate(direction.route)
|
||||
|
||||
fun BaseGalleryInfo.asDst() = GalleryDetailScreenDestination(GalleryInfoArgs(this))
|
||||
|
||||
|
||||
@@ -27,11 +27,12 @@ import com.hippo.ehviewer.icons.big.SadAndroid
|
||||
import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun ProgressScreen(gid: Long, token: String, page: Int, navigator: DestinationsNavigator) {
|
||||
LockDrawer(true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.hippo.ehviewer.ui.screen
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
@@ -20,15 +19,14 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.text.input.forEachTextValue
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.text.input.textAsFlow
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
@@ -41,18 +39,20 @@ import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.SearchBarHorizontalPadding
|
||||
import androidx.compose.material3.SearchBarInputField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -61,7 +61,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import arrow.fx.coroutines.mapIndexed
|
||||
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -72,13 +71,14 @@ import com.hippo.ehviewer.dao.SearchDao
|
||||
import com.hippo.ehviewer.ui.LocalNavDrawerState
|
||||
import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.rememberCompositionActiveState
|
||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||
import com.jamal.composeprefs3.ui.ifTrueThen
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -117,7 +117,7 @@ fun SearchBarScreen(
|
||||
) {
|
||||
var mSuggestionList by remember { mutableStateOf(emptyList<Suggestion>()) }
|
||||
val mSearchDatabase = searchDatabase.searchDao()
|
||||
var active by rememberSaveable { mutableStateOf(false) }
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||
val context = LocalContext.current
|
||||
val dialogState = LocalDialogState.current
|
||||
@@ -180,9 +180,9 @@ fun SearchBarScreen(
|
||||
onSearchHidden()
|
||||
}
|
||||
|
||||
if (active) {
|
||||
if (expanded) {
|
||||
LaunchedEffect(Unit) {
|
||||
searchFieldState.forEachTextValue {
|
||||
snapshotFlow { searchFieldState.text }.collectLatest {
|
||||
updateSuggestions()
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ fun SearchBarScreen(
|
||||
|
||||
fun hideSearchView() {
|
||||
onSearchViewHidden()
|
||||
active = false
|
||||
expanded = false
|
||||
}
|
||||
|
||||
fun onApplySearch() {
|
||||
@@ -217,13 +217,13 @@ fun SearchBarScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val scrollAwayModifier = if (!active) {
|
||||
val scrollAwayModifier = if (!expanded) {
|
||||
Modifier.offset { IntOffset(0, searchBarOffsetY()) }
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
// Placeholder, fill immutable SearchBar padding
|
||||
@@ -242,56 +242,66 @@ fun SearchBarScreen(
|
||||
.padding(top = 48.dp) then scrollAwayModifier,
|
||||
)
|
||||
}
|
||||
val onExpandedChange = { v: Boolean ->
|
||||
if (v) {
|
||||
onSearchViewExpanded()
|
||||
} else {
|
||||
onSearchViewHidden()
|
||||
}
|
||||
expanded = v
|
||||
}
|
||||
val activeState = rememberCompositionActiveState()
|
||||
SearchBar(
|
||||
modifier = Modifier.fillMaxWidth().align(Alignment.TopCenter) then scrollAwayModifier
|
||||
modifier = Modifier.align(Alignment.TopCenter) then scrollAwayModifier
|
||||
.windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)),
|
||||
state = searchFieldState,
|
||||
onSearch = {
|
||||
hideSearchView()
|
||||
onApplySearch()
|
||||
},
|
||||
active = active,
|
||||
onActiveChange = {
|
||||
if (it) {
|
||||
onSearchViewExpanded()
|
||||
} else {
|
||||
onSearchViewHidden()
|
||||
}
|
||||
active = it
|
||||
},
|
||||
title = title.ifNotNullThen { Text(title!!, overflow = TextOverflow.Ellipsis) },
|
||||
placeholder = searchFieldHint.ifNotNullThen { Text(searchFieldHint!!) },
|
||||
leadingIcon = {
|
||||
if (active) {
|
||||
IconButton(onClick = { hideSearchView() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
|
||||
}
|
||||
} else {
|
||||
val drawerState = LocalNavDrawerState.current
|
||||
IconButton(onClick = { scope.launchUI { drawerState.open() } }) {
|
||||
Icon(Icons.Default.Menu, contentDescription = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
if (active) {
|
||||
val hasText by searchFieldState.textAsFlow().mapIndexed { index, text ->
|
||||
// Workaround for https://github.com/FooIbar/EhViewer/issues/715
|
||||
if (index == 0) delay(700)
|
||||
text.isNotEmpty()
|
||||
}.collectAsState(false)
|
||||
if (hasText) {
|
||||
IconButton(onClick = { searchFieldState.clearText() }) {
|
||||
Icon(Icons.Default.Close, contentDescription = null)
|
||||
inputField = {
|
||||
SearchBarInputField(
|
||||
state = searchFieldState,
|
||||
onSearch = {
|
||||
hideSearchView()
|
||||
onApplySearch()
|
||||
},
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
modifier = Modifier.widthIn(max = maxWidth - SearchBarHorizontalPadding * 2),
|
||||
label = title.ifNotNullThen {
|
||||
Text(title!!, overflow = TextOverflow.Ellipsis)
|
||||
}.takeUnless {
|
||||
val contentActive by activeState.state
|
||||
expanded || contentActive || searchFieldState.text.isNotEmpty()
|
||||
},
|
||||
placeholder = searchFieldHint.ifNotNullThen { Text(searchFieldHint!!) },
|
||||
leadingIcon = {
|
||||
if (expanded) {
|
||||
IconButton(onClick = { hideSearchView() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
|
||||
}
|
||||
} else {
|
||||
val drawerState = LocalNavDrawerState.current
|
||||
IconButton(onClick = { scope.launchUI { drawerState.open() } }) {
|
||||
Icon(Icons.Default.Menu, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
trailingIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
if (expanded) {
|
||||
if (searchFieldState.text.isNotEmpty()) {
|
||||
IconButton(onClick = { searchFieldState.clearText() }) {
|
||||
Icon(Icons.Default.Close, contentDescription = null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
trailingIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
) {
|
||||
activeState.Anchor()
|
||||
filter?.invoke()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -319,7 +329,7 @@ fun SearchBarScreen(
|
||||
}
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
modifier = Modifier.clickable { it.onClick() }.animateItemPlacement(),
|
||||
modifier = Modifier.clickable { it.onClick() }.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.parseAsHtml
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.parseAsHtml
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -35,13 +35,13 @@ import com.hippo.ehviewer.ui.destinations.LicenseScreenDestination
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.toAnnotatedString
|
||||
import com.hippo.ehviewer.updater.AppUpdater
|
||||
import com.hippo.ehviewer.updater.Release
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.installPackage
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import java.io.File
|
||||
@@ -58,9 +58,9 @@ private fun versionCode() = "${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_S
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
private fun author() = stringResource(R.string.settings_about_author_summary).replace('$', '@').parseAsHtml().toAnnotatedString()
|
||||
private fun author() = stringResource(R.string.settings_about_author_summary).replace('$', '@').parseAsHtml()
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AboutScreen(navigator: DestinationsNavigator) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -6,9 +6,7 @@ import android.net.Uri
|
||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
import android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -17,9 +15,6 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
@@ -30,7 +25,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -48,8 +42,8 @@ import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.Crash
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.isCronetAvailable
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.File
|
||||
@@ -60,7 +54,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AdvancedScreen(navigator: DestinationsNavigator) {
|
||||
val context = LocalContext.current
|
||||
@@ -144,38 +138,6 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
||||
entryValueRes = R.array.app_language_entry_values,
|
||||
value = Settings::language,
|
||||
)
|
||||
var enableCronet by Settings::enableCronet.observed
|
||||
Preference(
|
||||
title = stringResource(id = R.string.settings_advanced_http_engine),
|
||||
summary = if (enableCronet) "Cronet" else "OkHttp",
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
dialogState.awaitPermissionOrCancel(
|
||||
title = R.string.settings_advanced_http_engine,
|
||||
showCancelButton = false,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.align(Alignment.Center)) {
|
||||
SegmentedButton(
|
||||
selected = !enableCronet,
|
||||
onClick = { enableCronet = false },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
|
||||
) {
|
||||
Text("OkHttp")
|
||||
}
|
||||
SegmentedButton(
|
||||
selected = enableCronet,
|
||||
onClick = { enableCronet = true },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
|
||||
enabled = isCronetAvailable,
|
||||
) {
|
||||
Text("Cronet")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.preload_thumb_aggressively),
|
||||
value = Settings::preloadThumbAggressively,
|
||||
|
||||
@@ -50,6 +50,7 @@ import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.displayPath
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
@@ -58,7 +59,7 @@ import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import splitties.init.appCtx
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun DownloadScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
@@ -37,7 +37,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.compose.ui.text.parseAsHtml
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.asMutableState
|
||||
@@ -54,12 +54,12 @@ import com.hippo.ehviewer.ui.screen.popNavigate
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||
import com.hippo.ehviewer.ui.tools.toAnnotatedString
|
||||
import com.hippo.ehviewer.util.copyTextToClipboard
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.isAtLeastT
|
||||
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
@@ -68,7 +68,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalTime
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun EhScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
@@ -116,7 +116,7 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
||||
Column {
|
||||
val warning = stringResource(id = R.string.settings_eh_identity_cookies_signed)
|
||||
val str = cookies.joinToString("\n") { (k, v) -> "$k: $v" }
|
||||
Text(text = warning.parseAsHtml().toAnnotatedString())
|
||||
Text(text = warning.parseAsHtml())
|
||||
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
|
||||
OutlinedTextField(
|
||||
value = str,
|
||||
@@ -264,25 +264,30 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
||||
entryValueRes = R.array.launch_page_entry_values,
|
||||
value = Settings::launchPage.observed,
|
||||
)
|
||||
val listMode = Settings.listMode.asMutableState()
|
||||
SimpleMenuPreferenceInt(
|
||||
title = stringResource(id = R.string.settings_eh_list_mode),
|
||||
entry = R.array.list_mode_entries,
|
||||
entryValueRes = R.array.list_mode_entry_values,
|
||||
value = Settings.listMode.asMutableState(),
|
||||
)
|
||||
IntSliderPreference(
|
||||
maxValue = 60,
|
||||
minValue = 20,
|
||||
step = 7,
|
||||
title = stringResource(id = R.string.list_tile_thumb_size),
|
||||
value = Settings.listThumbSize::value,
|
||||
)
|
||||
SimpleMenuPreferenceInt(
|
||||
title = stringResource(id = R.string.settings_eh_detail_size),
|
||||
entry = R.array.detail_size_entries,
|
||||
entryValueRes = R.array.detail_size_entry_values,
|
||||
value = Settings.detailSize.asMutableState(),
|
||||
value = listMode,
|
||||
)
|
||||
AnimatedVisibility(visible = listMode.value == 0) {
|
||||
Column {
|
||||
IntSliderPreference(
|
||||
maxValue = 60,
|
||||
minValue = 20,
|
||||
step = 7,
|
||||
title = stringResource(id = R.string.list_tile_thumb_size),
|
||||
value = Settings.listThumbSize::value,
|
||||
)
|
||||
SimpleMenuPreferenceInt(
|
||||
title = stringResource(id = R.string.settings_eh_detail_size),
|
||||
entry = R.array.detail_size_entries,
|
||||
entryValueRes = R.array.detail_size_entry_values,
|
||||
value = Settings.detailSize.asMutableState(),
|
||||
)
|
||||
}
|
||||
}
|
||||
IntSliderPreference(
|
||||
maxValue = 10,
|
||||
minValue = 1,
|
||||
|
||||
@@ -58,13 +58,14 @@ import com.hippo.ehviewer.dao.FilterMode
|
||||
import com.hippo.ehviewer.ui.tools.Deferred
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.groupByToObserved
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun FilterScreen(navigator: DestinationsNavigator) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -226,7 +227,7 @@ fun FilterScreen(navigator: DestinationsNavigator) {
|
||||
stickyHeader(key = filterMode) {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp).animateItemPlacement(),
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp).animateItem(),
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
@@ -234,7 +235,7 @@ fun FilterScreen(navigator: DestinationsNavigator) {
|
||||
items(filters, key = { requireNotNull(it.id) }) { filter ->
|
||||
val filterCheckBoxRecomposeScope = currentRecomposeScope
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().animateItemPlacement().clickable { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } },
|
||||
modifier = Modifier.fillMaxWidth().animateItem().clickable { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(
|
||||
|
||||
@@ -21,9 +21,10 @@ import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibraryDefaults
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun LicenseScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
@@ -19,9 +19,10 @@ import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.util.setDefaultSettings
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun MyTagsScreen(navigator: DestinationsNavigator) {
|
||||
val url = EhUrl.myTagsUrl
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import arrow.atomic.Atomic
|
||||
import com.hippo.ehviewer.ui.destinations.DirectionDestination
|
||||
import com.hippo.ehviewer.ui.openBrowser
|
||||
import com.hippo.ehviewer.ui.settings.PreferenceTokens.PreferenceTextPadding
|
||||
import com.hippo.ehviewer.util.ProgressDialog
|
||||
@@ -45,6 +44,7 @@ import com.jamal.composeprefs3.ui.prefs.SpannedTextPref
|
||||
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
||||
import com.jamal.composeprefs3.ui.prefs.TextPref
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.math.roundToInt
|
||||
@@ -64,7 +64,7 @@ object PreferenceTokens {
|
||||
fun PreferenceHeader(
|
||||
icon: Painter,
|
||||
@StringRes title: Int,
|
||||
childRoute: DirectionDestination,
|
||||
childRoute: DirectionDestinationSpec,
|
||||
navigator: DestinationsNavigator,
|
||||
) {
|
||||
Row(
|
||||
@@ -82,7 +82,7 @@ fun PreferenceHeader(
|
||||
fun PreferenceHeader(
|
||||
icon: ImageVector,
|
||||
@StringRes title: Int,
|
||||
childRoute: DirectionDestination,
|
||||
childRoute: DirectionDestinationSpec,
|
||||
navigator: DestinationsNavigator,
|
||||
) = PreferenceHeader(
|
||||
icon = rememberVectorPainter(image = icon),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.hippo.ehviewer.ui.settings
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -22,16 +23,18 @@ import androidx.compose.ui.res.stringResource
|
||||
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.asMutableState
|
||||
import com.hippo.ehviewer.ui.isAuthenticationSupported
|
||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun PrivacyScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
@@ -54,20 +57,27 @@ fun PrivacyScreen(navigator: DestinationsNavigator) {
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) {
|
||||
Column(modifier = Modifier.padding(it).nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||
val security = Settings.security.asMutableState()
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.settings_privacy_require_unlock),
|
||||
value = Settings::security,
|
||||
enabled = LocalContext.current.isAuthenticationSupported(),
|
||||
)
|
||||
val securityDelay = Settings::securityDelay.observed
|
||||
val summary = if (securityDelay.value == 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)
|
||||
IntSliderPreference(
|
||||
maxValue = 30,
|
||||
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
|
||||
summary = summary,
|
||||
value = securityDelay.rememberedAccessor,
|
||||
value = security.rememberedAccessor,
|
||||
enabled = LocalContext.current.isAuthenticationSupported(),
|
||||
)
|
||||
AnimatedVisibility(visible = security.value) {
|
||||
val securityDelay = Settings::securityDelay.observed
|
||||
val summary = if (securityDelay.value == 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)
|
||||
}
|
||||
IntSliderPreference(
|
||||
maxValue = 30,
|
||||
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
|
||||
summary = summary,
|
||||
value = securityDelay.rememberedAccessor,
|
||||
enabled = LocalContext.current.isAuthenticationSupported(),
|
||||
)
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.settings_privacy_secure),
|
||||
summary = stringResource(id = R.string.settings_privacy_secure_summary),
|
||||
|
||||
@@ -27,9 +27,10 @@ import com.hippo.ehviewer.ui.destinations.DownloadScreenDestination
|
||||
import com.hippo.ehviewer.ui.destinations.EhScreenDestination
|
||||
import com.hippo.ehviewer.ui.destinations.PrivacyScreenDestination
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SettingsScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
@@ -28,11 +28,12 @@ import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.ui.LockDrawer
|
||||
import com.hippo.ehviewer.util.setDefaultSettings
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
|
||||
private const val APPLY_JS = "javascript:(function(){var apply = document.getElementById(\"apply\").children[0];apply.click();})();"
|
||||
|
||||
@Destination
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun UConfigScreen(navigator: DestinationsNavigator) {
|
||||
LockDrawer(true)
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package com.hippo.ehviewer.ui.tools
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spanned
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -146,25 +141,6 @@ fun AnnotatedString.toBBCode() = buildString {
|
||||
}
|
||||
}
|
||||
|
||||
fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
|
||||
val spanned = this@toAnnotatedString
|
||||
append(spanned.toString())
|
||||
getSpans(0, spanned.length, Any::class.java).forEach { span ->
|
||||
val start = getSpanStart(span)
|
||||
val end = getSpanEnd(span)
|
||||
fun addStyle(spanStyle: SpanStyle) = addStyle(spanStyle, start, end)
|
||||
when (span) {
|
||||
is StrikethroughSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough))
|
||||
is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline))
|
||||
is StyleSpan -> {
|
||||
val s = span.style
|
||||
if (s and Typeface.BOLD != 0) addStyle(SpanStyle(fontWeight = FontWeight.Bold))
|
||||
if (s and Typeface.ITALIC != 0) addStyle(SpanStyle(fontStyle = FontStyle.Italic))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberBBCodeTextToolbar(textFieldValue: MutableState<TextFieldValue>): TextToolbar {
|
||||
var tfv by textFieldValue
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.hippo.ehviewer.ui.tools
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
@JvmInline
|
||||
value class CompositionActiveState(val state: MutableState<Boolean>) {
|
||||
@Composable
|
||||
fun Anchor() = DisposableEffect(state) {
|
||||
state.value = true
|
||||
onDispose {
|
||||
state.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberCompositionActiveState() = remember {
|
||||
CompositionActiveState(mutableStateOf(false))
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.AlertDialog
|
||||
@@ -99,9 +100,7 @@ class DialogState {
|
||||
content = null
|
||||
}
|
||||
|
||||
// Move to crossinline on next compose compiler update
|
||||
// https://issuetracker.google.com/issues/325502738
|
||||
suspend inline fun <R> dialog(noinline block: @Composable (CancellableContinuation<R>) -> Unit) = suspendCancellableCoroutine { cont ->
|
||||
suspend inline fun <R> dialog(crossinline block: @Composable (CancellableContinuation<R>) -> Unit) = suspendCancellableCoroutine { cont ->
|
||||
cont.invokeOnCancellation { dismiss() }
|
||||
val realContinuation = object : CancellableContinuation<R> by cont {
|
||||
override fun resumeWith(result: Result<R>) {
|
||||
@@ -487,8 +486,12 @@ class DialogState {
|
||||
items: List<String>,
|
||||
@StringRes title: Int,
|
||||
@StringRes checkBoxText: Int,
|
||||
selected: Int = -1,
|
||||
initialChecked: Boolean = false,
|
||||
): Pair<Int, Boolean> = showNoButton {
|
||||
var checked by remember { mutableStateOf(false) }
|
||||
var checked by remember { mutableStateOf(initialChecked) }
|
||||
val textStyle = MaterialTheme.typography.titleMedium
|
||||
val selectedColor = MaterialTheme.colorScheme.tertiary
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
@@ -497,14 +500,20 @@ class DialogState {
|
||||
)
|
||||
LazyColumn {
|
||||
itemsIndexed(items) { index, text ->
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.clickable { dismissWith(index to checked) }.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = text,
|
||||
style = if (index == selected) textStyle.copy(color = selectedColor) else textStyle,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { dismissWith(index to checked) }.fillMaxWidth(),
|
||||
trailingContent = {
|
||||
if (index == selected) {
|
||||
Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = selectedColor)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
LabeledCheckbox(
|
||||
|
||||
@@ -42,7 +42,7 @@ fun ReorderableItemScope.DragHandle(
|
||||
} else if (isAtLeastOMR1) {
|
||||
HapticFeedbackConstants.KEYBOARD_RELEASE
|
||||
} else {
|
||||
HapticFeedbackConstants.VIRTUAL_KEY_RELEASE
|
||||
HapticFeedbackConstants.VIRTUAL_KEY
|
||||
}
|
||||
view.performHapticFeedback(feedbackConstant)
|
||||
onDragStopped()
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.executeAndParseAs
|
||||
import com.hippo.ehviewer.client.executeSafely
|
||||
import com.hippo.ehviewer.util.copyTo
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.prepareGet
|
||||
@@ -36,7 +37,7 @@ object AppUpdater {
|
||||
Settings.lastUpdateTime = now.epochSeconds
|
||||
if (Settings.useCIUpdateChannel) {
|
||||
val curSha = BuildConfig.COMMIT_SHA
|
||||
val branch = ghStatement(API_URL).execute { JSONObject(it.bodyAsText()).getString("default_branch") }
|
||||
val branch = ghStatement(API_URL).executeSafely { JSONObject(it.bodyAsText()).getString("default_branch") }
|
||||
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)
|
||||
@@ -54,7 +55,7 @@ object AppUpdater {
|
||||
return Release(shortSha, changelog, archiveUrl)
|
||||
}
|
||||
} else {
|
||||
val curVersion = BuildConfig.VERSION_NAME.substringBefore('-')
|
||||
val curVersion = BuildConfig.RAW_VERSION_NAME
|
||||
val release = ghStatement(LATEST_RELEASE_URL).executeAndParseAs<GithubRelease>()
|
||||
val latestVersion = release.version
|
||||
val description = release.info
|
||||
@@ -68,7 +69,7 @@ object AppUpdater {
|
||||
}
|
||||
|
||||
suspend fun downloadUpdate(url: String, file: File) =
|
||||
ghStatement(url).execute { response ->
|
||||
ghStatement(url).executeSafely { response ->
|
||||
if (url.endsWith("zip")) {
|
||||
response.bodyAsChannel().toInputStream().use { stream ->
|
||||
ZipInputStream(stream).use { zip ->
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.asUniFile
|
||||
import java.io.File
|
||||
@@ -30,6 +32,12 @@ object AppConfig {
|
||||
private const val PARSE_ERROR = "parse_error"
|
||||
private const val CRASH = "crash"
|
||||
|
||||
private val abi = Build.SUPPORTED_ABIS[0].takeIf {
|
||||
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
|
||||
} ?: "universal"
|
||||
|
||||
fun matchVariant(name: String) = name.contains(BuildConfig.FLAVOR) && name.contains(abi)
|
||||
|
||||
private val externalAppDir: File?
|
||||
get() {
|
||||
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
|
||||
|
||||
@@ -4,18 +4,23 @@ import android.os.ParcelFileDescriptor
|
||||
import android.system.Int64Ref
|
||||
import android.system.Os
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.FileDescriptor
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
|
||||
private fun sendFileTotally(from: FileDescriptor, to: FileDescriptor) {
|
||||
private fun sendFileTotally(from: FileDescriptor, to: FileDescriptor): Long {
|
||||
if (isAtLeastP) {
|
||||
Os.sendfile(to, from, Int64Ref(0), Long.MAX_VALUE)
|
||||
} else {
|
||||
FileInputStream(from).use { src ->
|
||||
FileOutputStream(to).use { dst ->
|
||||
src.channel.transferTo(0, Long.MAX_VALUE, dst.channel)
|
||||
}
|
||||
// sendfile may fail on some devices
|
||||
try {
|
||||
return Os.sendfile(to, from, Int64Ref(0), Long.MAX_VALUE)
|
||||
} catch (e: Exception) {
|
||||
logcat("sendfile", e)
|
||||
}
|
||||
}
|
||||
return FileInputStream(from).use { src ->
|
||||
FileOutputStream(to).use { dst ->
|
||||
src.channel.transferTo(0, Long.MAX_VALUE, dst.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,5 +30,5 @@ infix fun ParcelFileDescriptor.sendTo(fd: ParcelFileDescriptor) {
|
||||
}
|
||||
|
||||
infix fun UniFile.sendTo(file: UniFile) = openFileDescriptor("r").use { src ->
|
||||
file.openFileDescriptor("w").use { dst -> src sendTo dst }
|
||||
file.openFileDescriptor("wt").use { dst -> src sendTo dst }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
fun calculateFraction(a: Float, b: Float, pos: Float) = ((pos - a) / (b - a)).coerceIn(0f, 1f)
|
||||
@@ -17,57 +17,37 @@ package com.hippo.ehviewer.util
|
||||
|
||||
import android.os.Looper
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileReader
|
||||
import java.util.regex.Pattern
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object OSUtils {
|
||||
private const val PROCFS_MEMFILE = "/proc/meminfo"
|
||||
private val PROCFS_MEMFILE_FORMAT = Pattern.compile("^([a-zA-Z]*):[ \t]*([0-9]*)[ \t]kB")
|
||||
private val PROCFS_MEMFILE_FORMAT = Regex("^([a-zA-Z]*):[ \t]*([0-9]*)[ \t]kB")
|
||||
private const val MEMTOTAL_STRING = "MemTotal"
|
||||
private var sTotalMem = Long.MIN_VALUE
|
||||
|
||||
val appAllocatedMemory: Long
|
||||
/**
|
||||
* Get application allocated memory size
|
||||
*/
|
||||
get() = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
|
||||
|
||||
val appMaxMemory: Long
|
||||
/**
|
||||
* Get application max memory size
|
||||
*/
|
||||
get() = Runtime.getRuntime().maxMemory()
|
||||
val totalMemory: Long
|
||||
/**
|
||||
* Get device RAM size
|
||||
*/
|
||||
get() {
|
||||
if (sTotalMem == Long.MIN_VALUE) {
|
||||
runCatching {
|
||||
BufferedReader(FileReader(PROCFS_MEMFILE), 64).use { reader ->
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
val matcher = PROCFS_MEMFILE_FORMAT.matcher(line!!)
|
||||
if (matcher.find() && MEMTOTAL_STRING == matcher.group(1)) {
|
||||
var mem = matcher.group(2)!!.toLongOrDefault(-1L)
|
||||
if (mem != -1L) {
|
||||
mem *= 1024
|
||||
}
|
||||
sTotalMem = mem
|
||||
break
|
||||
}
|
||||
|
||||
val totalMemory by lazy {
|
||||
runCatching {
|
||||
FileReader(PROCFS_MEMFILE).buffered(64).use { reader ->
|
||||
reader.lineSequence().forEach { line ->
|
||||
PROCFS_MEMFILE_FORMAT.find(line)?.destructured?.let { (k, v) ->
|
||||
if (k == MEMTOTAL_STRING) {
|
||||
return@runCatching v.toLong() * 1024
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
logcat(it)
|
||||
}
|
||||
if (sTotalMem == Long.MIN_VALUE) {
|
||||
sTotalMem = -1L
|
||||
}
|
||||
}
|
||||
return sTotalMem
|
||||
}
|
||||
-1L
|
||||
}.onFailure {
|
||||
logcat(it)
|
||||
}.getOrElse { -1L }
|
||||
}
|
||||
}
|
||||
|
||||
val isMainThread: Boolean
|
||||
|
||||
@@ -16,36 +16,24 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.URLSpan
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object TextUrl {
|
||||
private val URL_PATTERN =
|
||||
Pattern.compile("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+)+(:\\d{1,5})?(/[a-zA-Z0-9-_~:#@!&',;=%/*.?+$\\[\\]()]+)?/?")
|
||||
Regex("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+)+(:\\d{1,5})?(/[a-zA-Z0-9-_~:#@!&',;=%/*.?+$\\[\\]()]+)?/?")
|
||||
|
||||
fun handleTextUrl(content: CharSequence): CharSequence {
|
||||
val m = URL_PATTERN.matcher(content)
|
||||
var spannable: Spannable? = null
|
||||
while (m.find()) {
|
||||
// Ensure spannable
|
||||
if (spannable == null) {
|
||||
spannable = if (content is Spannable) {
|
||||
content
|
||||
} else {
|
||||
SpannableString(content)
|
||||
}
|
||||
}
|
||||
val start = m.start()
|
||||
val end = m.end()
|
||||
val links = spannable.getSpans(start, end, URLSpan::class.java)
|
||||
fun handleTextUrl(content: Spannable): Spannable {
|
||||
URL_PATTERN.findAll(content).forEach { result ->
|
||||
val start = result.range.first
|
||||
val end = result.range.last + 1
|
||||
val links = content.getSpans(start, end, URLSpan::class.java)
|
||||
if (links.isNotEmpty()) {
|
||||
// There has been URLSpan already, leave it alone
|
||||
continue
|
||||
return@forEach
|
||||
}
|
||||
val urlSpan = URLSpan(m.group(0))
|
||||
spannable.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
val urlSpan = URLSpan(result.groupValues[0])
|
||||
content.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return spannable ?: content
|
||||
return content
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ fun UniFile.openInputStream(): FileInputStream {
|
||||
* Use Native IO/NIO directly if possible, unless you need process file content on JVM!
|
||||
*/
|
||||
fun UniFile.openOutputStream(): FileOutputStream {
|
||||
return AutoCloseOutputStream(openFileDescriptor("w"))
|
||||
return AutoCloseOutputStream(openFileDescriptor("wt"))
|
||||
}
|
||||
|
||||
fun UniFile.sha1() = openFileDescriptor("r").use { sha1(it.fd) }
|
||||
|
||||
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user