Compare commits

..
25 Commits
Author SHA1 Message Date
FooIbar 6a64ff51ed Prepare for release 1.11.2
Releases / build (push) Has been cancelled
2024-03-17 14:44:16 +08:00
FooIbar ea9d9cfba7 Prepare changelog for 1.11.2 2024-03-17 14:41:43 +08:00
FooIbar 75c6561896 Fix app updater getting wrong download link 2024-03-16 20:13:42 +08:00
FooIbar 8f8db82e9d Fix disowned gallery uploader parsing (#918)
* Fix disowned gallery uploader parsing

* Run rustfmt

* Refine

* Run rustfmt
2024-03-16 17:48:10 +08:00
FooIbar a114a8ac72 Minor tweak to some settings' visibility 2024-03-16 11:10:08 +08:00
FooIbar 2c6216057f Revert "Merge default and marshmallow flavors"
This reverts commit 5676de24da.

Reason for revert: Desugaring causes regression on API 26+
2024-03-15 19:55:59 +08:00
FooIbar c5cbcf7418 Reintroduce confirm dialog for swipe-to-dismiss (#914)
Observe the state changes instead of using `confirmValueChange`, which
will be called twice.
2024-03-15 18:40:51 +08:00
FooIbar 5746e3bb57 Pin compose foundation version to 1.7.0-alpha03 2024-03-14 16:05:18 +08:00
FooIbar fd3c87be22 Make reader bottom sheet scrollable and fix insets 2024-03-14 16:05:08 +08:00
FooIbar 866fff3e45 Fix favorites modification blocked by snackbar 2024-03-14 16:04:55 +08:00
FooIbar 06df7befb6 Fix tag namespace for cloud favorites search 2024-03-13 21:38:16 +08:00
FooIbar 8021f86473 Address java.util.regex.Pattern usage 2024-03-13 21:34:04 +08:00
renovate[bot]andGG Bond c9919a8b39 Cleanup CronetEngine (#901)
* Cleanup CronetEngine with UploadDataProviders

* Remove WriteChannelContent as no user now

* Cleanup

* Cleanup

* Remove unnecessary invokeOnCancellation

* Refine

* Refine

* Format

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2024-03-13 14:40:37 +08:00
renovate[bot] 386015a9ec Update dependency com.squareup.okio:okio-jvm to v3.9.0 2024-03-13 04:00:26 +00:00
FooIbar cbce98b34e Prepare for next iteration 2024-03-12 22:47:24 +08:00
FooIbar f69521fc1a Prepare for release 1.11.1
Releases / build (push) Has been cancelled
2024-03-12 22:45:48 +08:00
FooIbar 01b4054368 Prepare changelog for 1.11.1 2024-03-12 22:34:27 +08:00
FooIbar e07aab4434 Drop basic login as captcha is mandatory now 2024-03-12 21:27:01 +08:00
FooIbar 18caa87512 Implement group by download label 2024-03-12 21:26:09 +08:00
FooIbar 5676de24da Merge default and marshmallow flavors 2024-03-12 12:05:12 +08:00
FooIbar 57bbb1fe38 Embed Cronet and drop OkHttp (#895)
* Embed Cronet and drop OkHttp

* Fix updater
2024-03-11 12:22:39 +00:00
FooIbar dcbfbc2e5f Fix concurrent modification 2024-03-11 15:04:56 +08:00
FooIbar e1f473d301 Convert remaining icons to ImageVector 2024-03-11 12:43:52 +08:00
renovate[bot] a33256d29f Update dependency sh.calvin.reorderable:reorderable to v1.3.3 2024-03-10 11:12:12 +00:00
FooIbar 6a81877b09 Prepare for next iteration 2024-03-09 19:17:18 +08:00
96 changed files with 1284 additions and 1516 deletions

No files matched your search

+42 -199
View File
@@ -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
+32 -67
View File
@@ -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
+38 -1
View File
@@ -1,5 +1,42 @@
# 变更日志
## [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 +44,7 @@
* 由于 E 站 Cookie 策略变更,不再支持 Cookie 登录
* 登录时自动选择 exhentai (如果可用)
* 不再提供默认下载路径
* 手动排序更改为以添加时间/上传时间/标题/下载标签/页数对下载列表排序
* 由于数据库结构变更,此版本导出的数据将无法被旧版本导入
### 新功能
@@ -19,7 +57,6 @@
* 随机打开本地收藏/下载中的画廊 [#695](https://github.com/FooIbar/EhViewer/issues/695)
* 随机在搜索结果中跳转
* 以网格模式查看下载列表
* 以添加时间/上传时间/标题/下载标签/页数对下载列表排序
* 本地收藏/历史/下载中的标签搜索 [#860](https://github.com/FooIbar/EhViewer/issues/860)
### 改进
+4 -10
View File
@@ -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"/>
+25 -25
View File
@@ -60,10 +60,10 @@ android {
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
minSdk = 26
targetSdk = 34
versionCode = 180051
versionName = "1.11.0"
versionCode = 180053
versionName = "1.11.2"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -80,6 +80,7 @@ android {
"nb-rNO",
),
)
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName$versionNameSuffix\"")
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
@@ -151,9 +143,11 @@ android {
}
packaging {
dex {
useLegacyPackaging = false
}
resources {
excludes += "/META-INF/**"
excludes += "/okhttp3/**"
excludes += "/kotlin/**"
excludes += "**.txt"
excludes += "**.bin"
@@ -230,10 +224,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 +247,7 @@ dependencies {
implementation(libs.telephoto.zoomable)
implementation(libs.bundles.ktor)
implementation(libs.ktor.client.core)
implementation(libs.bundles.kotlinx.serialization)
@@ -267,7 +257,17 @@ dependencies {
coreLibraryDesugaring(libs.desugar)
"gmsImplementation"(libs.bundles.cronet)
implementation(libs.cronet.embedded)
debugImplementation(libs.compose.ui.tooling)
implementation(libs.compose.ui.tooling.preview)
}
configurations.all {
resolutionStrategy {
// Workaround for https://issuetracker.google.com/329489167
force("androidx.compose.foundation:foundation-android:1.7.0-alpha03")
}
}
kotlin {
+2
View File
@@ -16,5 +16,7 @@
-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)
}
}
@@ -44,7 +44,7 @@ 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.BasicTextField2
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
@@ -494,7 +494,7 @@ private fun SearchBarInputField(
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
}
BasicTextField(
BasicTextField2(
state = state,
modifier = modifier
.height(InputFieldHeight)
@@ -54,22 +54,16 @@ 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 okio.Path.Companion.toOkioPath
import splitties.arch.room.roomDb
import splitties.init.appCtx
@@ -185,25 +179,9 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
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
}
}
}
@@ -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
@@ -233,22 +232,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) {
@@ -21,10 +21,7 @@ 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
@@ -96,8 +93,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)))
}
@@ -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].matches(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")
}
}
}
}
@@ -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
@@ -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)
}
@@ -26,33 +26,27 @@ 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()
return executeHttpRequest(callContext, data)
}
override suspend fun execute(data: HttpRequestData) = 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() {
@@ -67,23 +61,22 @@ 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 = GlobalScope.writer(callContext) {
pool.useInstance {
chunkChan = Channel()
request.read(it)
chunkChan.consumeEach { buffer ->
buffer.flip()
channel.writeFully(buffer)
buffer.clear()
request.read(buffer)
}
}
}.channel,
),
)
}
@@ -97,24 +90,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 +115,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")
}
@@ -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))
}
}
@@ -14,66 +14,37 @@ 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.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
@@ -81,100 +52,7 @@ import kotlinx.coroutines.Job
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 +66,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 +76,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 +85,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 +140,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 +156,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()
}
}
}
@@ -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),
@@ -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
@@ -112,7 +112,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 +123,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 +182,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
@@ -142,6 +145,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,21 +265,28 @@ 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) ->
// 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 ->
SwipeToDismissBox2(
state = dismissState,
@@ -564,12 +575,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
@@ -230,6 +230,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
},
onSearchHidden = { fabHidden = false },
refreshState = refreshState,
tagNamespace = !isLocalFav,
searchBarOffsetY = { searchBarOffsetY },
trailingIcon = {
val sheetState = LocalSideSheetState.current
@@ -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
@@ -163,8 +164,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
@@ -506,32 +505,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)
}
}
}
@@ -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
@@ -201,6 +203,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 +388,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 = {},
@@ -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,7 +42,6 @@ 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.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.system.logcat
@@ -144,38 +137,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,
@@ -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,
@@ -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,6 +23,7 @@ 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
@@ -54,20 +56,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),
@@ -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
@@ -487,8 +488,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 +502,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(
@@ -54,7 +54,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
@@ -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()) {
@@ -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
}
}
@@ -1,28 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import java.io.IOException
import okhttp3.Interceptor
import okhttp3.Response
/**
* Catches any uncaught exceptions from later in the chain and rethrows as a non-fatal
* IOException to avoid catastrophic failure.
*
* This should be the first interceptor in the client.
*
* See [Interceptor](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-interceptor/index.html)
*/
class UncaughtExceptionInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: Exception) {
if (e is IOException) {
throw e
} else {
throw IOException(e)
}
}
}
}
@@ -25,7 +25,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
@@ -102,13 +101,13 @@ fun BottomReaderBar(onClickSettings: () -> Unit) {
) {
IconButton(onClick = { readingModeExpanded = true }) {
Icon(
painter = painterResource(readingMode.iconRes),
imageVector = readingMode.icon,
contentDescription = stringResource(R.string.viewer),
)
}
IconButton(onClick = { orientationModeExpanded = true }) {
Icon(
painter = painterResource(orientationMode.iconRes),
imageVector = orientationMode.icon,
contentDescription = stringResource(R.string.pref_rotation_type),
)
}
@@ -43,10 +43,9 @@ import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.isSystemInDarkTheme
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.material3.BottomSheetDefaults
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@@ -319,13 +318,12 @@ class ReaderActivity : EhActivity() {
dragHandle = null,
// Yeah, I know color state should not be read here, but we have to do it...
scrimColor = scrim,
windowInsets = WindowInsets(0),
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
) {
SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
isColorFilter = page == 2
setMenuVisibility(!isColorFilter)
}
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -650,10 +648,9 @@ class ReaderActivity : EhActivity() {
dialogState.dialog {
ModalBottomSheet(
onDismissRequest = { it.cancel() },
windowInsets = WindowInsets(0),
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
) {
ReaderPageSheet(page) { it.cancel() }
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@@ -2,12 +2,16 @@ package eu.kanade.tachiyomi.ui.reader
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.material.icons.Icons
import androidx.compose.material.icons.filled.FileCopy
import androidx.compose.material.icons.filled.Refresh
@@ -47,12 +51,14 @@ fun ReaderPageSheetMeta(
Spacer(modifier = Modifier.size(32.dp))
Text(text = stringResource(id = text))
}
Item(icon = Icons.Default.Refresh, text = R.string.refresh, onClick = retry)
Item(icon = Icons.Default.Refresh, text = R.string.refresh_original, onClick = retryOrigin)
Item(icon = Icons.Default.Share, text = R.string.action_share, onClick = share)
Item(icon = Icons.Default.FileCopy, text = R.string.action_copy, onClick = copy)
Item(icon = Icons.Default.Save, text = R.string.action_save, onClick = save)
Item(icon = Icons.Default.Save, text = R.string.action_save_to, onClick = saveTo)
Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
Item(icon = Icons.Default.Refresh, text = R.string.refresh, onClick = retry)
Item(icon = Icons.Default.Refresh, text = R.string.refresh_original, onClick = retryOrigin)
Item(icon = Icons.Default.Share, text = R.string.action_share, onClick = share)
Item(icon = Icons.Default.FileCopy, text = R.string.action_copy, onClick = copy)
Item(icon = Icons.Default.Save, text = R.string.action_save, onClick = save)
Item(icon = Icons.Default.Save, text = R.string.action_save_to, onClick = saveTo)
}
}
@Composable
@@ -1,18 +1,24 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.content.pm.ActivityInfo
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.ui.graphics.vector.ImageVector
import com.hippo.ehviewer.R
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.reader.Landscape
import com.hippo.ehviewer.icons.reader.LandscapeLocked
import com.hippo.ehviewer.icons.reader.Portrait
import com.hippo.ehviewer.icons.reader.PortraitLocked
import com.hippo.ehviewer.icons.reader.ScreenRotation
enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
DEFAULT(0, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.label_default, R.drawable.ic_screen_rotation_24dp, 0x00000000),
FREE(1, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp, 0x00000008),
PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000010),
LANDSCAPE(3, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp, 0x00000018),
LOCKED_PORTRAIT(4, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp, 0x00000020),
LOCKED_LANDSCAPE(5, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp, 0x00000028),
REVERSE_PORTRAIT(6, ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, R.string.rotation_reverse_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000030),
enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val stringRes: Int, val icon: ImageVector, val flagValue: Int) {
DEFAULT(0, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.label_default, EhIcons.Reader.ScreenRotation, 0x00000000),
FREE(1, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, EhIcons.Reader.ScreenRotation, 0x00000008),
PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, EhIcons.Reader.Portrait, 0x00000010),
LANDSCAPE(3, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, EhIcons.Reader.Landscape, 0x00000018),
LOCKED_PORTRAIT(4, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, EhIcons.Reader.PortraitLocked, 0x00000020),
LOCKED_LANDSCAPE(5, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, EhIcons.Reader.LandscapeLocked, 0x00000028),
REVERSE_PORTRAIT(6, ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, R.string.rotation_reverse_portrait, EhIcons.Reader.Portrait, 0x00000030),
;
companion object {
@@ -1,9 +1,16 @@
package eu.kanade.tachiyomi.ui.reader.setting
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.vector.ImageVector
import com.hippo.ehviewer.R
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.reader.ContinuousVertical
import com.hippo.ehviewer.icons.reader.Default
import com.hippo.ehviewer.icons.reader.LeftToRight
import com.hippo.ehviewer.icons.reader.RightToLeft
import com.hippo.ehviewer.icons.reader.Vertical
import com.hippo.ehviewer.icons.reader.Webtoon
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
@@ -12,13 +19,13 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
@Stable
enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
DEFAULT(0, R.string.label_default, R.drawable.ic_reader_default_24dp, 0x00000000),
LEFT_TO_RIGHT(1, R.string.left_to_right_viewer, R.drawable.ic_reader_ltr_24dp, 0x00000001),
RIGHT_TO_LEFT(2, R.string.right_to_left_viewer, R.drawable.ic_reader_rtl_24dp, 0x00000002),
VERTICAL(3, R.string.vertical_viewer, R.drawable.ic_reader_vertical_24dp, 0x00000003),
WEBTOON(4, R.string.webtoon_viewer, R.drawable.ic_reader_webtoon_24dp, 0x00000004),
CONTINUOUS_VERTICAL(5, R.string.vertical_plus_viewer, R.drawable.ic_reader_continuous_vertical_24dp, 0x00000005),
enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, val icon: ImageVector, val flagValue: Int) {
DEFAULT(0, R.string.label_default, EhIcons.Reader.Default, 0x00000000),
LEFT_TO_RIGHT(1, R.string.left_to_right_viewer, EhIcons.Reader.LeftToRight, 0x00000001),
RIGHT_TO_LEFT(2, R.string.right_to_left_viewer, EhIcons.Reader.RightToLeft, 0x00000002),
VERTICAL(3, R.string.vertical_viewer, EhIcons.Reader.Vertical, 0x00000003),
WEBTOON(4, R.string.webtoon_viewer, EhIcons.Reader.Webtoon, 0x00000004),
CONTINUOUS_VERTICAL(5, R.string.vertical_plus_viewer, EhIcons.Reader.ContinuousVertical, 0x00000005),
;
companion object {
@@ -1,17 +1,9 @@
package tachiyomi.data.release
import android.os.Build
import com.hippo.ehviewer.BuildConfig
import com.hippo.ehviewer.util.AppConfig
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
const val FLAVOR = "${BuildConfig.FLAVOR_api}-${BuildConfig.FLAVOR_oss}"
val apkVariant = "$FLAVOR-${
Build.SUPPORTED_ABIS[0].takeIf {
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
} ?: "universal"
}"
/**
* Contains information about the latest release from GitHub.
*/
@@ -23,7 +15,7 @@ data class GithubRelease(
@SerialName("assets") val assets: List<GitHubAssets>,
) {
fun getDownloadLink(): String {
val asset = assets.find { it.downloadLink.contains(apkVariant) } ?: assets[0]
val asset = assets.find { AppConfig.matchVariant(it.downloadLink) } ?: assets[0]
return asset.downloadLink
}
}
@@ -1,5 +1,6 @@
package tachiyomi.data.release
import com.hippo.ehviewer.util.AppConfig
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -20,7 +21,10 @@ data class GithubArtifacts(
@SerialName("artifacts") val artifacts: List<GithubArtifact>,
) {
fun getDownloadLink(): String {
return (artifacts.find { it.name.contains(apkVariant) } ?: artifacts[0]).downloadLink
// The default order is upload order, so we need to sort it
return artifacts.sortedBy { it.name }.run {
find { AppConfig.matchVariant(it.name) } ?: this[0]
}.downloadLink
}
}
@@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M17.3159,1h-10a2.0059,2.0059 0,0 0,-2 2L5.3159,21a2.0059,2.0059 0,0 0,2 2h10a2.0059,2.0059 0,0 0,2 -2L19.3159,3A2.0059,2.0059 0,0 0,17.3159 1ZM17.3159,21h-10L7.3159,3h10Z" />
<path
android:fillColor="@android:color/black"
android:pathData="M11.3083,5h2v5.5h-2z" />
<path
android:fillColor="@android:color/black"
android:pathData="M15.308,16l-2,0l0,-4.5l-2,0l0,4.5l-2,0l3,3l3,-3z" />
</vector>
@@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M6.9906,18L6.9906,6h12L18.9906,3a2.0059,2.0059 0,0 0,-2 -2h-10a2.0059,2.0059 0,0 0,-2 2L4.9906,21a2.0059,2.0059 0,0 0,2 2h10a2.0059,2.0059 0,0 0,2 -2L18.9906,18h-12ZM6.9906,3h10L16.9906,4h-10ZM16.9906,21h-10L6.9906,20h10Z" />
<path
android:fillColor="@android:color/black"
android:pathData="M21.7706,12.44l-0.02,0.03a3.5546,3.5546 0,0 0,0.05 -0.47,3.9031 3.9031,0 0,0 -0.05,-0.48l0.02,0.03 0.85,-0.68a0.5068,0.5068 0,0 0,0.12 -0.64l-0.84,-1.46a0.4986,0.4986 0,0 0,-0.61 -0.22l-1.01,0.39 0.0099,0.04a3.6135,3.6135 0,0 0,-0.8 -0.48l-0.17,-1.07a0.4879,0.4879 0,0 0,-0.49 -0.43h-1.68a0.4876,0.4876 0,0 0,-0.49 0.42l-0.17,1.08a3.6149,3.6149 0,0 0,-0.8 0.48l0.02,-0.03 -1.01,-0.39a0.4878,0.4878 0,0 0,-0.61 0.22l-0.84,1.46a0.5068,0.5068 0,0 0,0.12 0.64l0.85,0.68 0.02,-0.03a3.9148,3.9148 0,0 0,-0.05 0.48,3.915 3.915,0 0,0 0.05,0.48l-0.02,-0.03 -0.85,0.68a0.5068,0.5068 0,0 0,-0.12 0.64l0.84,1.46a0.4985,0.4985 0,0 0,0.61 0.22l1.01,-0.39 -0.02,-0.03a4.4991,4.4991 0,0 0,0.8 0.46l0.17,1.08a0.4876,0.4876 0,0 0,0.49 0.42h1.68a0.4876,0.4876 0,0 0,0.49 -0.42l0.17,-1.08a3.6138,3.6138 0,0 0,0.8 -0.48l-0.02,0.03 1.02,0.39a0.4879,0.4879 0,0 0,0.61 -0.22l0.84,-1.46a0.5068,0.5068 0,0 0,-0.12 -0.64ZM17.9906,14a2,2 0,1 1,2 -2A2.0059,2.0059 0,0 1,17.9906 14Z" />
</vector>
@@ -1,21 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M17.3159,18h-10L7.3159,16h-2v5a2.0059,2.0059 0,0 0,2 2h10a2.0059,2.0059 0,0 0,2 -2L19.3159,16h-2ZM17.3159,21h-10L7.3159,20h10Z" />
<path
android:fillColor="@android:color/black"
android:pathData="M7.3159,6h10L17.3159,8h2L19.3159,3a2.0059,2.0059 0,0 0,-2 -2h-10a2.0059,2.0059 0,0 0,-2 2L5.3159,8h2ZM7.3159,3h10L17.3159,4h-10Z" />
<path
android:fillColor="@android:color/black"
android:pathData="M22.311,12l-3,-3l0,2l-11.99,0l0,2l11.99,0l0,2l3,-3z" />
<path
android:fillColor="@android:color/black"
android:pathData="M2.3206,11h1.5v2h-1.5z" />
<path
android:fillColor="@android:color/black"
android:pathData="M4.8206,11h1.5v2h-1.5z" />
</vector>
@@ -1,21 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M7,6H17V8h2V3a2.0059,2.0059 0,0 0,-2 -2H7A2.0059,2.0059 0,0 0,5 3V8H7ZM7,3H17V4H7Z" />
<path
android:fillColor="@android:color/black"
android:pathData="M17,18L7,18L7,16L5,16v5a2.0059,2.0059 0,0 0,2 2L17,23a2.0059,2.0059 0,0 0,2 -2L19,16L17,16ZM17,21L7,21L7,20L17,20Z" />
<path
android:fillColor="@android:color/black"
android:pathData="M5.005,13l11.99,0l0,-2l-11.99,0l0,-2l-3,3l3,3l0,-2z" />
<path
android:fillColor="@android:color/black"
android:pathData="M20.4953,11h1.5v2h-1.5z" />
<path
android:fillColor="@android:color/black"
android:pathData="M17.9953,11h1.5v2h-1.5z" />
</vector>
@@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M17,1L7,1A2.0059,2.0059 0,0 0,5 3L5,21a2.0059,2.0059 0,0 0,2 2L17,23a2.0059,2.0059 0,0 0,2 -2L19,3A2.0059,2.0059 0,0 0,17 1ZM17,3v1.0462L7,4.0462L7,3ZM17,6.0462v12L7,18.0462v-12ZM7,21v-0.9538L17,20.0462L17,21Z" />
<path
android:fillColor="@android:color/black"
android:pathData="M9,13l3,3l3,-3l-2,0l0,-5l-2,0l0,5l-2,0z" />
</vector>
@@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M11.992,19l3,-3l-2,0l0,-11l-2,0l0,11l-2,0l3,3z" />
<path
android:fillColor="@android:color/black"
android:pathData="M17,1L7,1A2.0059,2.0059 0,0 0,5 3L5,21a2.0059,2.0059 0,0 0,2 2L17,23a2.0059,2.0059 0,0 0,2 -2L19,3A2.0059,2.0059 0,0 0,17 1ZM17,21L7,21L7,3L17,3Z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M21,5L3,5c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,7c0,-1.1 -0.9,-2 -2,-2zM19,17L5,17L5,7h14v10zM10,16h4c0.55,0 1,-0.45 1,-1v-3c0,-0.55 -0.45,-1 -1,-1v-1c0,-1.11 -0.9,-2 -2,-2 -1.11,0 -2,0.9 -2,2v1c-0.55,0 -1,0.45 -1,1v3c0,0.55 0.45,1 1,1zM10.8,10c0,-0.66 0.54,-1.2 1.2,-1.2 0.66,0 1.2,0.54 1.2,1.2v1h-2.4v-1z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M10,16h4c0.55,0 1,-0.45 1,-1v-3c0,-0.55 -0.45,-1 -1,-1v-1c0,-1.11 -0.9,-2 -2,-2 -1.11,0 -2,0.9 -2,2v1c-0.55,0 -1,0.45 -1,1v3c0,0.55 0.45,1 1,1zM10.8,10c0,-0.66 0.54,-1.2 1.2,-1.2 0.66,0 1.2,0.54 1.2,1.2v1h-2.4v-1zM17,1L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,3c0,-1.1 -0.9,-2 -2,-2zM17,19L7,19L7,5h10v14z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M16.48,2.52c3.27,1.55 5.61,4.72 5.97,8.48h1.5C23.44,4.84 18.29,0 12,0l-0.66,0.03 3.81,3.81 1.33,-1.32zM10.23,1.75c-0.59,-0.59 -1.54,-0.59 -2.12,0L1.75,8.11c-0.59,0.59 -0.59,1.54 0,2.12l12.02,12.02c0.59,0.59 1.54,0.59 2.12,0l6.36,-6.36c0.59,-0.59 0.59,-1.54 0,-2.12L10.23,1.75zM14.83,21.19L2.81,9.17l6.36,-6.36 12.02,12.02 -6.36,6.36zM7.52,21.48C4.25,19.94 1.91,16.76 1.55,13L0.05,13C0.56,19.16 5.71,24 12,24l0.66,-0.03 -3.81,-3.81 -1.33,1.32z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M1.01,7L1,17c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V7c0,-1.1 -0.9,-2 -2,-2H3c-1.1,0 -1.99,0.9 -1.99,2zM19,7v10H5V7h14z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M17,1.01L7,1c-1.1,0 -1.99,0.9 -1.99,2v18c0,1.1 0.89,2 1.99,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,19H7V5h10v14z" />
</vector>
-7
View File
@@ -23,11 +23,8 @@
<string name="history">Verlauf</string>
<string name="downloads">Downloads</string>
<string name="settings">Einstellungen</string>
<string name="username">Benutzername</string>
<string name="password">Kennwort</string>
<string name="sign_in">Anmelden</string>
<string name="register">Konto erstellen</string>
<string name="sign_in_via_webview">Mit WebView anmelden</string>
<string name="sign_in_first">Melde dich zunächst an</string>
<string name="text_is_empty">Der Text wird nicht eingegeben</string>
<string name="waring">Achtung</string>
@@ -49,7 +46,6 @@
<string name="error_get_ptoken_error">Fehler beim Erhalten pTokens</string>
<string name="error_cant_save_image">Das Bild konnte nicht gespeichert werden</string>
<string name="error_invalid_number">Nummer ungültig</string>
<string name="get_it">Verstanden</string>
<string name="gallery_list_search_bar_hint_exhentai">ExHentai durchsuchen</string>
<string name="gallery_list_search_bar_hint_e_hentai">E-Hentai durchsuchen</string>
<string name="gallery_list_empty_hit">Die Welt ist so groß und der Panda sitzt ganz allein</string>
@@ -294,10 +290,7 @@
<string name="settings_privacy_secure">Screenshot verhindern</string>
<string name="settings_privacy_secure_summary">Screenshots werden verhindert. Inhalt der App wird in Aktuelle Apps-Liste nicht angezeigt.</string>
<string name="app_waring">Inhalt dieser App kommt aus dem Internet, von dem einige dir körperlichen oder emotionalen Schaden zufügen kann. Du hast es wirklich erfahrt und möchtest immer noch fortsetzen.</string>
<string name="error_username_cannot_empty">Gibt den Nutzername ein</string>
<string name="error_password_cannot_empty">Gibt das Passwort ein</string>
<string name="guest_mode">Anmeldung überspringen</string>
<string name="sign_in_failed">Anmeldung fehlgeschlagen</string>
<string name="clipboard_gallery_url_snack_message">Ein Link von einen Galerie wurde in Zwischenablage erkennt.</string>
<string name="clipboard_gallery_url_snack_action">Gehen</string>
<string name="settings_advanced_read_cache_size">Größe des Lesezwischenspeichers</string>
-8
View File
@@ -26,12 +26,8 @@
<string name="downloads">Descargas</string>
<string name="settings">Configuración</string>
<string name="username">Usuario</string>
<string name="password">Contraseña</string>
<string name="sign_in">Iniciar sesión</string>
<string name="register">Registrarse</string>
<string name="sign_in_via_webview">Iniciar sesión vía web</string>
<string name="sign_in_first">Por favor, inicie sesión primero</string>
<string name="text_is_empty">El texto está vacío</string>
@@ -58,10 +54,7 @@
<string name="error_invalid_number">Número invalido</string>
<string name="app_waring">El contenido de esta aplicación es de Internet. Algunas de ellas pueden causarle daño psicológico. Ahora sabes los riesgos anteriormente mencionado y te gustaría asumirlos</string>
<string name="error_username_cannot_empty">El usuario no puede estar vacío</string>
<string name="guest_mode">Saltar inicio de sesión</string>
<string name="sign_in_failed">Inicio de sesión fallido</string>
<string name="get_it">Entendido</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">Buscar ExHentai</string>
@@ -385,7 +378,6 @@
<string name="delete">Eliminar</string>
<string name="download_state_failed">Descarga fallida</string>
<string name="download_state_none">Descarga en pausa</string>
<string name="error_password_cannot_empty">La contraseña no puede estar vacía</string>
<string name="error_something_wrong_happened">Ocurrió algo malo</string>
<string name="settings_privacy">Privacidad</string>
<string name="filter">Bloqueo de galerías</string>
-8
View File
@@ -26,12 +26,8 @@
<string name="downloads">Téléchargement</string>
<string name="settings">Paramétres</string>
<string name="username">Nom d\'utilisateur</string>
<string name="password">Mot de passe</string>
<string name="sign_in">Se connecter</string>
<string name="register">Créer un compte</string>
<string name="sign_in_via_webview">Se connecter via WebView</string>
<string name="sign_in_first">Connectez-vous pour continuer</string>
<string name="text_is_empty">Entrez le texte.</string>
@@ -58,11 +54,7 @@
<string name="error_invalid_number">Nombre invalide</string>
<string name="app_waring">Le contenu de l\'application provient de l\'Internet, dont quelque chose peut vous causer le dommage physique ou mentale. Vous l\'avez bien compris et voulez continuer quand même.</string>
<string name="error_username_cannot_empty">Nom d\'ultilisateur ne doit pas être une chaîne vide</string>
<string name="error_password_cannot_empty">Mot de passe ne doit pas être une chaîne vide</string>
<string name="guest_mode">Continuer sans se connecter</string>
<string name="sign_in_failed">Impossible de se connecter</string>
<string name="get_it">Compris</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">Chercher sur ExHentai</string>
-8
View File
@@ -24,12 +24,8 @@
<string name="downloads">ダウンロード</string>
<string name="settings">設定</string>
<string name="username">ユーザー名</string>
<string name="password">パスワード</string>
<string name="sign_in">ログイン</string>
<string name="register">登録</string>
<string name="sign_in_via_webview">Webでログイン</string>
<string name="sign_in_first">ログインしてください</string>
<string name="text_is_empty">テキストを入力してください</string>
@@ -56,11 +52,7 @@
<string name="error_invalid_number">無効な数字です</string>
<string name="app_waring">このアプリの内容はインターネットから取得したものです。その内容の一部は使用者のお体や心理に傷つける恐れがあります。それでも続行しますか?</string>
<string name="error_username_cannot_empty">ユーザー名を入力してください。</string>
<string name="error_password_cannot_empty">パスワードを入力してください。</string>
<string name="guest_mode">ログインをスキップ</string>
<string name="sign_in_failed">ログインに失敗しました。</string>
<string name="get_it">了解</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">ExHentaiを検索</string>
-8
View File
@@ -25,12 +25,8 @@
<string name="downloads">다운로드</string>
<string name="settings">설정</string>
<string name="username">사용자 이름</string>
<string name="password">비밀번호</string>
<string name="sign_in">로그인</string>
<string name="register">회원가입</string>
<string name="sign_in_via_webview">WebView를 통하여 로그인</string>
<string name="sign_in_first">먼저 로그인하세요.</string>
<string name="text_is_empty">텍스트가 비어 있습니다.</string>
@@ -60,11 +56,7 @@
<string name="error_invalid_number">잘못된 번호</string>
<string name="app_waring">이 앱의 내용은 인터넷에서 받아옵니다. 이 앱을 사용하기 위해서는 받아온 내용 중 일부가 사용자에게 가할 수 있는 신체적 또는 정신적 피해를 감수해야 합니다.</string>
<string name="error_username_cannot_empty">사용자 이름은 비워둘 수 없습니다.</string>
<string name="error_password_cannot_empty">비밀번호는 비워둘 수 없습니다.</string>
<string name="guest_mode">로그인 없이 사용</string>
<string name="sign_in_failed">로그인 실패</string>
<string name="get_it">알겠습니다</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">ExHentai 검색</string>
@@ -8,8 +8,6 @@
<string name="error_unknown_host">Ukjent vert</string>
<string name="error_redirection">For mange videresendinger</string>
<string name="error_socket">Nettverksfeil</string>
<string name="password">Passord</string>
<string name="sign_in_via_webview">Logg inn via WebView</string>
<string name="downloads">Nedlastninger</string>
<string name="register">Registrering</string>
<string name="error_invalid_number">Ugyldig nummer</string>
@@ -18,7 +16,6 @@
<string name="history">Historikk</string>
<string name="subscription">Abonnement</string>
<string name="settings">Innstillinger</string>
<string name="username">Brukernavn</string>
<string name="sign_in">Logg inn</string>
<string name="sign_in_first">Logg inn først</string>
<string name="download">Last ned</string>
@@ -53,7 +50,6 @@
<string name="torrents">Torrenter</string>
<string name="more_previews">Flere forhåndsvisninger</string>
<string name="refresh">Gjenoppfrisk</string>
<string name="get_it">Skjønner</string>
<string name="keyword_search">Nøkkelordssøk</string>
<string name="search_sft">Etiketter</string>
<string name="any">Ubegrenset</string>
-8
View File
@@ -25,12 +25,8 @@
<string name="downloads">ดาวน์โหลด</string>
<string name="settings">การตั้งค่า</string>
<string name="username">ชื่อผู้ใช้</string>
<string name="password">รหัสผ่าน</string>
<string name="sign_in">ลงชื่อเข้าใช้</string>
<string name="register">ลงทะเบียน</string>
<string name="sign_in_via_webview">ลงชื่อเข้าใช้ผ่านมุมมองเว็บไซต์</string>
<string name="sign_in_first">โปรดลงชื่อเข้าใช้ก่อนเป็นอันดับแรก</string>
<string name="text_is_empty">ข้อความห้ามเว้นว่างไว้</string>
@@ -60,11 +56,7 @@
<string name="error_invalid_number">จำนวนไม่ถูกต้อง</string>
<string name="app_waring">เนื้อหาภายในแอปพลิเคชั่นนี้มาจากอินเทอร์เน็ตทั้งหมด อาจมีเนื้อหาบางอย่างที่มีผลต่อร่างกายหรือจิตใจของคุณ คุณจะต้องยอมรับความเสี่ยงที่ดังกล่าว และดำเนินการต่อไป</string>
<string name="error_username_cannot_empty">ชื่อผู้ใช้ห้ามเว้นว่างไว้</string>
<string name="error_password_cannot_empty">รหัสผ่านห้ามเว้นว่างไว้</string>
<string name="guest_mode">ดำเนินการต่อโดยไม่ต้องลงชื่อเข้าระบบ</string>
<string name="sign_in_failed">การลงชื่อเข้าใช้ล้มเหลว</string>
<string name="get_it">เข้าใจแล้ว ไม่ต้องแสดงอีก</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">ค้นหาใน ExHentai</string>
-8
View File
@@ -8,11 +8,8 @@
<string name="history">Geçmiş</string>
<string name="downloads">İndirilenler</string>
<string name="settings">Ayarlar</string>
<string name="username">Kullanıcı adı</string>
<string name="password">Şifre</string>
<string name="sign_in">Giriş yap</string>
<string name="register">Kaydol</string>
<string name="sign_in_via_webview">WebView aracılığıyla giriş yap</string>
<string name="sign_in_first">Lütfen önce giriş yap</string>
<string name="waring">Uyarı</string>
<string name="invalid_download_location">İndirme konumu kullanılamıyor. Lütfen Ayarlardan ayarlayın.</string>
@@ -33,11 +30,7 @@
<string name="error_get_ptoken_error">pToken alınamadı</string>
<string name="error_cant_save_image">Görsel kaydedilemiyor</string>
<string name="error_invalid_number">Geçersiz numara</string>
<string name="error_username_cannot_empty">Kullanıcı adı boş bir metin olamaz</string>
<string name="error_password_cannot_empty">Şifre boş bir metin olamaz</string>
<string name="guest_mode">Giriş yapmaksızın devam et</string>
<string name="sign_in_failed">Giriş başarısız</string>
<string name="get_it">Anladım</string>
<string name="gallery_list_empty_hit">Dünya büyük olmasına rağmen pandacık yalnız oturuyor</string>
<string name="keyword_search">Anahtar Kelime Arama</string>
<string name="image_search">Görsel Arama</string>
@@ -68,7 +61,6 @@
<string name="error_unknown_host">Bilinmeyen host</string>
<string name="app_waring">Bu uygulamadaki içerikler internetten alınmaktadır. Bazı içerikler fiziksel veya zihinsel olarak size zararlı gelebilir.Yukarıda, riskleri öğrendiniz ve üstleneceksiniz.
\nKullanmaya devam ederek, yukarıdaki koşulları kabul edersiniz.</string>
<string name="sign_in_failed_tip">Bu sorun devam ederse, \"WebView aracılığıyla giriş yap\" seçeneğini veya \"Çerez aracılığıyla giriş yap\" seçeneğini deneyin.</string>
<string name="gallery_list_search_bar_open_gallery">Galeriyi aç</string>
<string name="gallery_list_search_bar_hint_e_hentai">E-Hentai\'de Arayın</string>
<string name="gallery_list_empty_hit_subscription">Etiketlere Ayarlar-&gt;EH-&gt;Etiketlerim yerinden abone olun</string>
+3 -11
View File
@@ -22,11 +22,8 @@
<string name="history">历史</string>
<string name="downloads">下载</string>
<string name="settings">设置</string>
<string name="username">用户名</string>
<string name="password">密码</string>
<string name="sign_in">登录</string>
<string name="register">注册</string>
<string name="sign_in_via_webview">通过网页登录</string>
<string name="sign_in_first">请先登录</string>
<string name="text_is_empty">文本为空</string>
<string name="waring">警告</string>
@@ -52,12 +49,7 @@
<string name="error_invalid_number">非法数字</string>
<string name="app_waring">本应用中内容来自互联网,部分内容可能对您的生理及心理造成难以恢复的伤害。本应用作者不会对由本应用造成的任何后果负责。未成年人应在监护人指导下使用本应用。请遵守所在地的法律法规。</string>
<string name="app_waring_2">继续使用即代表您同意上述条款。</string>
<string name="error_username_cannot_empty">用户名不可为空</string>
<string name="error_password_cannot_empty">密码不可为空</string>
<string name="guest_mode">游客模式</string>
<string name="sign_in_failed">登录失败</string>
<string name="sign_in_failed_tip">若持续出错,请尝试“通过网页登录”或“通过 Cookie 登录”。</string>
<string name="get_it">知道了</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">搜索 ExHentai</string>
<string name="gallery_list_search_bar_hint_e_hentai">搜索 E-Hentai</string>
@@ -93,6 +85,7 @@
<string name="duplicate_quick_search">已存在相同的快速搜索,名称为“%s”。</string>
<string name="duplicate_name">已存在相同名称</string>
<string name="save_progress">保存进度</string>
<string name="delete_quick_search">删除快速搜索“%s”?</string>
<string name="go_to_hint">第 %1$d 页,共 %2$d 页</string>
<!-- search_min_rating -->
<string name="any">不限</string>
@@ -253,6 +246,7 @@
<string name="label_text_is_invalid">“默认”是无效标签</string>
<string name="label_text_exist">标签已存在</string>
<string name="rename_label_title">重命名标签</string>
<string name="delete_label">删除标签“%s”?</string>
<!-- History -->
<string name="no_history">这里是阅读历史</string>
<string name="clear_all">全部清除</string>
@@ -356,7 +350,6 @@
<string name="settings_advanced_dump_logcat_to">已保存日志至 %s</string>
<string name="settings_advanced_read_cache_size">阅读缓存大小</string>
<string name="settings_advanced_app_language_title">App 界面语言</string>
<string name="settings_advanced_http_engine">Http 引擎(重启生效)</string>
<string name="settings_advanced_touch_slop">[实验性] 触摸溢出系数</string>
<string name="settings_advanced_touch_slop_summary">增大此项将降低滑动手势敏感度</string>
<string name="settings_advanced_export_data">导出数据</string>
@@ -463,10 +456,9 @@
<string name="uploaded_time_asc">上传时间(升序)</string>
<string name="title_asc">标题(升序)</string>
<string name="title_desc">标题(降序)</string>
<string name="download_label_asc">下载标签(升序)</string>
<string name="download_label_desc">下载标签(降序)</string>
<string name="page_count_asc">页数(升序)</string>
<string name="page_count_desc">页数(降序)</string>
<string name="group_by_download_label">按下载标签分组</string>
<string name="download_filter">过滤</string>
<string name="download_all">全部</string>
<string name="download_start_all_reversed">全部开始(倒序)</string>
@@ -26,12 +26,8 @@
<string name="downloads">下載</string>
<string name="settings">設置</string>
<string name="username">用户名</string>
<string name="password">密碼</string>
<string name="sign_in">登錄</string>
<string name="register">註冊</string>
<string name="sign_in_via_webview">通過網頁登錄</string>
<string name="sign_in_first">請先登錄</string>
<string name="text_is_empty">文本為空</string>
@@ -61,12 +57,7 @@
<string name="error_invalid_number">非法數字</string>
<string name="app_waring">本應用中內容來自互聯網,部分內容可能對您的生理及心理造成難以恢復的傷害。本應用作者不會對由本應用造成的任何後果負責。\n\n未成年人應在監護人指導下使用本應用。</string>
<string name="error_username_cannot_empty">用户名不可為空</string>
<string name="error_password_cannot_empty">密碼不可為空</string>
<string name="guest_mode">跳過登錄</string>
<string name="sign_in_failed">登錄失敗</string>
<string name="sign_in_failed_tip">若持續出錯,請嘗試“通過網頁登錄”或“通過 Cookie 登錄”。</string>
<string name="get_it">知道了</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">搜索 ExHentai</string>
+3 -11
View File
@@ -22,11 +22,8 @@
<string name="history">歷史</string>
<string name="downloads">下載</string>
<string name="settings">設定</string>
<string name="username">使用者名稱</string>
<string name="password">密碼</string>
<string name="sign_in">登入</string>
<string name="register">註冊</string>
<string name="sign_in_via_webview">透過網頁登入</string>
<string name="sign_in_first">請先登入</string>
<string name="text_is_empty">文字是空的</string>
<string name="waring">警告</string>
@@ -52,12 +49,7 @@
<string name="error_invalid_number">無效的數字</string>
<string name="app_waring">本應用程式中內容來自網際網路,部分內容可能對您的生理及心理造成難以恢復的傷害。本應用程式的作者不會對由其所造成的任何影響負責。\n\n未成年人應在監護人指導下使用本應用程式。\n\n繼續使用即代表您同意上述條款。</string>
<string name="app_waring_2">繼續使用即表示您同意上述條款。</string>
<string name="error_username_cannot_empty">使用者名稱欄位不能為空</string>
<string name="error_password_cannot_empty">密碼欄位不能為空</string>
<string name="guest_mode">略過登入</string>
<string name="sign_in_failed">登入失敗</string>
<string name="sign_in_failed_tip">若持續出錯,請嘗試“通過網頁登入”或“通過 Cookie 登入”。</string>
<string name="get_it">知道了</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">在 ExHentai 搜尋</string>
<string name="gallery_list_search_bar_hint_e_hentai">在 E-Hentai 搜尋</string>
@@ -93,6 +85,7 @@
<string name="duplicate_quick_search">已存在相同的快速搜尋,名稱為“%s”。</string>
<string name="duplicate_name">名稱重複</string>
<string name="save_progress">儲存進度</string>
<string name="delete_quick_search">刪除快速搜尋“%s”?</string>
<string name="go_to_hint">第 %1$d 頁,共 %2$d 頁</string>
<!-- search_min_rating -->
<string name="any">不限</string>
@@ -252,6 +245,7 @@
<string name="label_text_is_invalid">“預設”是無效的標籤</string>
<string name="label_text_exist">標籤已存在</string>
<string name="rename_label_title">重新命名標籤</string>
<string name="delete_label">刪除標籤“%s”?</string>
<!-- History -->
<string name="no_history">歷史紀錄將會顯示在這裡</string>
<string name="clear_all">全部清除</string>
@@ -355,7 +349,6 @@
<string name="settings_advanced_dump_logcat_to">已將紀錄檔儲存至 %s</string>
<string name="settings_advanced_read_cache_size">閱讀快取大小</string>
<string name="settings_advanced_app_language_title">應用程式介面語言</string>
<string name="settings_advanced_http_engine">Http 引擎 (需重新啟動)</string>
<string name="settings_advanced_touch_slop">[測試] 觸控溢出係數</string>
<string name="settings_advanced_touch_slop_summary">提高係數將降低滑動手勢敏感度</string>
<string name="settings_advanced_export_data">匯出檔案</string>
@@ -462,10 +455,9 @@
<string name="uploaded_time_asc">上傳時間 (遞增)</string>
<string name="title_asc">標題 (遞減)</string>
<string name="title_desc">標題 (遞增)</string>
<string name="download_label_asc">下載標籤 (遞減)</string>
<string name="download_label_desc">下載標籤 (遞增)</string>
<string name="page_count_asc">頁數 (遞減)</string>
<string name="page_count_desc">頁數 (遞增)</string>
<string name="group_by_download_label">按下載標籤分組</string>
<string name="download_filter">過濾</string>
<string name="download_all">全部</string>
<string name="download_start_all_reversed">全部開始(倒序)</string>
-2
View File
@@ -235,8 +235,6 @@
<item>@string/uploaded_time_asc</item>
<item>@string/title_asc</item>
<item>@string/title_desc</item>
<item>@string/download_label_asc</item>
<item>@string/download_label_desc</item>
<item>@string/page_count_asc</item>
<item>@string/page_count_desc</item>
</string-array>
+3 -11
View File
@@ -36,11 +36,8 @@
<string name="history">History</string>
<string name="downloads">Downloads</string>
<string name="settings">Settings</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="sign_in">Sign in</string>
<string name="register">Register</string>
<string name="sign_in_via_webview">Sign in via WebView</string>
<string name="sign_in_first">Please sign in first</string>
<string name="text_is_empty">Text is empty</string>
<string name="waring">Warning</string>
@@ -90,12 +87,7 @@
<string name="error_invalid_number">Invalid number</string>
<string name="app_waring">The content of this application is from the Internet. Some of it may do physical or mental harm to you. You have learnt the risks above and would like to undertake them. By continuing to use it, you agree to the above terms.</string>
<string name="app_waring_2">By continuing to use it, you agree to the above terms.</string>
<string name="error_username_cannot_empty">Username cannot be an empty string</string>
<string name="error_password_cannot_empty">Password cannot be an empty string</string>
<string name="guest_mode">Continue without signing in</string>
<string name="sign_in_failed">Sign in failed</string>
<string name="sign_in_failed_tip">If this issue continues, try \"Sign in via WebView\" or \"Sign in via cookies\".</string>
<string name="get_it">Got it</string>
<!-- Gallery List -->
<string name="gallery_list_search_bar_hint_exhentai">Search ExHentai</string>
<string name="gallery_list_search_bar_hint_e_hentai">Search E-Hentai</string>
@@ -133,6 +125,7 @@
<string name="duplicate_quick_search">A duplicate quick search exists. The name is \"%s\".</string>
<string name="duplicate_name">This name is already in use.</string>
<string name="save_progress">Save progress</string>
<string name="delete_quick_search">Delete quick search \"%s\"?</string>
<string name="go_to_hint">Page %1$d, total %2$d pages</string>
<!-- search_min_rating -->
<string name="any">Any</string>
@@ -295,6 +288,7 @@
<string name="label_text_is_invalid">\"Default\" is an invalid label</string>
<string name="label_text_exist">Label exists</string>
<string name="rename_label_title">Rename label</string>
<string name="delete_label">Delete label \"%s\"?</string>
<!-- History -->
<string name="no_history">Viewed galleries will be shown here</string>
<string name="clear_all">Clear all</string>
@@ -400,7 +394,6 @@
<string name="settings_advanced_dump_logcat_to">Logcat dumped to %s</string>
<string name="settings_advanced_read_cache_size">Read cache size</string>
<string name="settings_advanced_app_language_title">App language</string>
<string name="settings_advanced_http_engine">Http Engine (Restart required)</string>
<string name="settings_advanced_touch_slop">[Experimental] Touch slop factor</string>
<string name="settings_advanced_touch_slop_summary">Increasing this will reduce swipe gesture sensitivity</string>
<string name="settings_advanced_export_data">Export data</string>
@@ -528,10 +521,9 @@
<string name="uploaded_time_asc">Uploaded time (ascending)</string>
<string name="title_asc">Title (ascending)</string>
<string name="title_desc">Title (descending)</string>
<string name="download_label_asc">Download label (ascending)</string>
<string name="download_label_desc">Download label (descending)</string>
<string name="page_count_asc">Page count (ascending)</string>
<string name="page_count_desc">Page count (descending)</string>
<string name="group_by_download_label">Group by download label</string>
<string name="download_filter">Filter</string>
<string name="download_all">All</string>
<string name="download_start_all_reversed">Start all (reversed)</string>
+10 -3
View File
@@ -96,9 +96,16 @@ fn parse_token_and_gid(str: &str) -> Option<(i64, String)> {
}
fn parse_uploader_and_pages(str: &str) -> (Option<String>, bool, i32) {
let uploader = regex!(r#"<a href="https://e[x-]hentai.org/uploader/.*?">(.*?)</a>"#)
.captures(str)
.map(|grp| grp[1].to_string());
let uploader =
regex!(r#"<a href="https://e[x-]hentai.org/uploader/.*?">(.*?)</a>|(\(Disowned\))"#)
.captures(str)
.map(|grp| {
grp.get(1)
.or_else(|| grp.get(2))
.unwrap()
.as_str()
.to_string()
});
let pages = match regex!(r"<div>(\d+) pages</div>").captures(str) {
None => 0,
Some(grp) => grp[1].parse().unwrap_or(0),
@@ -1,22 +0,0 @@
@file:Suppress("NewApi")
package com.hippo.ehviewer.cronet
import android.net.http.HttpEngine
import com.hippo.ehviewer.Settings
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
val cronetHttpClient: HttpEngine = HttpEngine.Builder(appCtx).apply {
setEnableBrotli(true)
setUserAgent(Settings.userAgent)
// Cache Quic hint only since the real cache mechanism should on Ktor layer
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 4096)
addQuicHint("e-hentai.org", 443, 443)
addQuicHint("forums.e-hentai.org", 443, 443)
addQuicHint("exhentai.org", 443, 443)
}.build()
@@ -1,182 +0,0 @@
@file:Suppress("NewApi")
package com.hippo.ehviewer.ktor
import android.net.http.HttpException
import android.net.http.UploadDataProvider
import android.net.http.UploadDataSink
import android.net.http.UrlRequest
import android.net.http.UrlResponseInfo
import com.hippo.ehviewer.cronet.cronetDispatcher
import com.hippo.ehviewer.cronet.cronetHttpClient
import com.hippo.ehviewer.cronet.cronetHttpClientExecutor
import com.hippo.ehviewer.cronet.pool
import io.ktor.client.engine.HttpClientEngineBase
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.engine.callContext
import io.ktor.client.request.HttpRequestData
import io.ktor.client.request.HttpResponseData
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpProtocolVersion
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.OutgoingContent
import io.ktor.http.headers
import io.ktor.util.date.GMTDate
import io.ktor.util.flattenForEach
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.InternalAPI
import io.ktor.utils.io.pool.useInstance
import io.ktor.utils.io.writer
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.suspendCancellableCoroutine
object CronetEngine : HttpClientEngineBase("Cronet") {
override val config = HttpClientEngineConfig()
override val dispatcher = cronetDispatcher
@InternalAPI
override suspend fun execute(data: HttpRequestData): HttpResponseData {
val callContext = callContext()
return executeHttpRequest(callContext, data)
}
@OptIn(DelicateCoroutinesApi::class)
private suspend fun executeHttpRequest(
callContext: CoroutineContext,
data: HttpRequestData,
): HttpResponseData = suspendCancellableCoroutine { continuation ->
val requestTime = GMTDate()
val callback = object : UrlRequest.Callback {
lateinit var chunkChan: Channel<ByteBuffer>
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
continuation.resume(
info.toHttpResponseData(
requestTime = requestTime,
callContext = callContext,
),
)
}
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,
),
)
}
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
chunkChan.trySend(byteBuffer).getOrThrow()
}
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
chunkChan.close()
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) {
if (::chunkChan.isInitialized) {
chunkChan.close(error)
} else {
continuation.resumeWithException(error)
}
}
override fun onCanceled(p0: UrlRequest, p1: UrlResponseInfo?) {
// No-op
}
}
val request = cronetHttpClient.newUrlRequestBuilder(data.url.toString(), cronetHttpClientExecutor, callback).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() }
}
}
private fun UrlResponseInfo.toHttpResponseData(
requestTime: GMTDate,
callContext: CoroutineContext,
responseBody: ByteReadChannel = ByteReadChannel.Empty,
): HttpResponseData {
return HttpResponseData(
statusCode = HttpStatusCode.fromValue(httpStatusCode),
requestTime = requestTime,
headers = headers {
headers.asMap.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) {
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()
}
}
else -> error("UnsupportedContentType $this")
}
@@ -1,4 +0,0 @@
package com.hippo.ehviewer.util
val isCronetAvailable: Boolean
get() = isAtLeastSExtension7
+4 -10
View File
@@ -65,16 +65,10 @@
# 下载
| 变种 | 功能 |
|-------------|------------------------|
| Default | Android 9+, 完全支持 |
| Marshmallow | Android 6.0-8.1, 有限支持* |
| OSS | FOSS 依赖项** |
| GMS | 专有 GMS 组件 |
\* 可能不支持某些压缩包
** 在不支持 [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine) 的设备上回退到 OkHttp
| 变种 | 功能 |
|-------------|-----------------------|
| Default | Android 8.0+, 完全支持 |
| Marshmallow | Android 6.0-7.1, 有限支持 |
<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"/>
+4 -10
View File
@@ -65,16 +65,10 @@
# 下載
| 變種 | 功能 |
|-------------|------------------------|
| Default | Android 9+, 完全支援 |
| Marshmallow | Android 6.0-8.1, 有限支援* |
| OSS | FOSS 依賴項** |
| GMS | 專有 GMS 元件 |
\* 可能不支援某些壓縮包
** 在不支援 [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine) 的裝置上回退到 OkHttp
| 變種 | 功能 |
|-------------|-----------------------|
| Default | Android 8.0+, 完全支援 |
| Marshmallow | Android 6.0-7.1, 有限支援 |
<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"/>
+6 -11
View File
@@ -26,8 +26,7 @@ kotlinx-datetime = "0.6.0-RC.2"
kotlinx-serialization = "1.6.3"
ksp = "2.0.0-Beta4-1.0.19"
ktor = "3.0.0-beta-1"
okhttp3 = "5.0.0-alpha.12"
okio = "3.8.0"
okio = "3.9.0"
splitties = "3.0.0"
telephoto = "0.8.0"
xmlutil = "0.86.3"
@@ -73,6 +72,8 @@ compose-foundation = { module = "androidx.compose.foundation:foundation-android"
compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
compose-material3 = { module = "androidx.compose.material3:material3", version = "1.3.0-SNAPSHOT" }
compose-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
compose-ui-util = { module = "androidx.compose.ui:ui-util" }
compose-destinations-core = { module = "io.github.raamcosta.compose-destinations:core", version.ref = "compose-destinations" }
@@ -80,8 +81,7 @@ compose-destinations-compiler = { module = "io.github.raamcosta.compose-destinat
material-motion-core = "io.github.fornewid:material-motion-compose-core:1.2.0"
cronet-api = "org.chromium.net:cronet-api:119.6045.31"
cronet-gms = "com.google.android.gms:play-services-cronet:18.0.1"
cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31"
desugar = "com.android.tools:desugar_jdk_libs_nio:2.0.4"
@@ -93,17 +93,15 @@ jsoup = "org.jsoup:jsoup:1.17.2"
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
logcat = "com.squareup.logcat:logcat:0.1"
okhttp-android = { module = "com.squareup.okhttp3:okhttp-android" }
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp3" }
okio-jvm = { module = "com.squareup.okio:okio-jvm", version.ref = "okio" }
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
reorderable = "sh.calvin.reorderable:reorderable:1.3.2"
reorderable = "sh.calvin.reorderable:reorderable:1.3.3"
serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
@@ -122,10 +120,7 @@ telephoto-zoomable = { module = "me.saket.telephoto:zoomable", version.ref = "te
[bundles]
coil = ["coil-compose", "coil-gif", "coil-network"]
compose = ["compose-foundation", "compose-material-icons-extended", "compose-material3", "compose-material3-windowsizeclass", "compose-ui-util"]
cronet = ["cronet-api", "cronet-gms"]
kotlinx-serialization = ["serialization-cbor", "serialization-json", "serialization-json-okio", "serialization-xml-core", "serialization-xml"]
ktor = ["ktor-client-okhttp"]
okhttp = ["okhttp-android"]
splitties = ["splitties-appctx", "splitties-arch-room", "splitties-preferences", "splitties-systemservices"]
[plugins]