Compare commits
34
Commits
1.9.0
..
1.8.9.7-gms
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
019b961461 | ||
|
|
97f988991e | ||
|
|
0dbc3b7ea7 | ||
|
|
d974cb56df | ||
|
|
13f255529b | ||
|
|
16e912c9ae | ||
|
|
d58e08e24c | ||
|
|
e111ec74be | ||
|
|
01742ac6b9 | ||
|
|
e94424a83e | ||
|
|
56d30aa413 | ||
|
|
2c01acb4a6 | ||
|
|
c0f6d0ab04 | ||
|
|
a6d215a0bf | ||
|
|
cf9441adb5 | ||
|
|
9746f20b60 | ||
|
|
501148179b | ||
|
|
36b16d7be8 | ||
|
|
fab5532407 | ||
|
|
8ad4e8640f | ||
|
|
d08491bf7e | ||
|
|
12f6f08d81 | ||
|
|
5b5a2a9a33 | ||
|
|
7719312355 | ||
|
|
e2345cc756 | ||
|
|
8d0b8534b2 | ||
|
|
48f1b0a958 | ||
|
|
9abeba6b15 | ||
|
|
63f9c8de79 | ||
|
|
f792f2c968 | ||
|
|
75bec4314e | ||
|
|
b767c041b9 | ||
|
|
4e916731b3 | ||
|
|
6c24407fb7 |
No files matched your search
@@ -1,4 +0,0 @@
|
||||
[*.{kt,kts}]
|
||||
ij_kotlin_imports_layout = *
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
@@ -64,7 +64,7 @@ body:
|
||||
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
|
||||
placeholder: 06-15 17:44:53.704 23382 23382 E ...
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: more
|
||||
@@ -79,15 +79,6 @@ body:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: site
|
||||
attributes:
|
||||
label: 浏览站点 / Browsing site
|
||||
description: E-Hentai / ExHentai
|
||||
placeholder: E-Hentai
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
|
||||
+21
-143
@@ -14,12 +14,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup Java 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@@ -54,161 +54,39 @@ jobs:
|
||||
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') }}
|
||||
key: ${{ runner.os }}-cmake-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cmake-
|
||||
|
||||
- name: Build OSS
|
||||
run: ./gradlew assembleDefaultOssRelease
|
||||
- name: Gradle Build
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Upload OSS ARM64
|
||||
- name: Update ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: default-oss-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk
|
||||
name: arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload OSS x86_64
|
||||
- name: Update x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: default-oss-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk
|
||||
name: x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
|
||||
- name: Upload OSS ARM32
|
||||
- name: Update ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: default-oss-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk
|
||||
name: armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload OSS x86
|
||||
- name: Update x86
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: default-oss-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk
|
||||
name: x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86-release.apk
|
||||
|
||||
- name: Upload OSS mapping
|
||||
- name: Update mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: default-oss-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultOssRelease/mapping.txt
|
||||
|
||||
- name: Upload OSS native debug symbols
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: default-oss-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip
|
||||
|
||||
- name: Build GMS
|
||||
run: ./gradlew assembleDefaultGmsRelease
|
||||
|
||||
- name: Upload GMS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
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@v3
|
||||
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@v3
|
||||
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@v3
|
||||
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@v3
|
||||
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@v3
|
||||
with:
|
||||
name: default-gms-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip
|
||||
|
||||
- name: Build Marshmallow OSS
|
||||
run: ./gradlew assembleMarshmallowOssRelease
|
||||
|
||||
- name: Upload Marshmallow OSS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS x86
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowOssRelease/mapping.txt
|
||||
|
||||
- name: Upload Marshmallow OSS native debug symbols
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip
|
||||
|
||||
- name: Build Marshmallow GMS
|
||||
run: ./gradlew assembleMarshmallowGmsRelease
|
||||
|
||||
- name: Upload Marshmallow GMS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
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@v3
|
||||
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@v3
|
||||
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@v3
|
||||
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@v3
|
||||
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@v3
|
||||
with:
|
||||
name: marshmallow-gms-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/release/mapping.txt
|
||||
retention-days: 5
|
||||
@@ -1,22 +0,0 @@
|
||||
name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: lock-threads
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
issue-inactive-days: '1'
|
||||
include-any-issue-labels: 'duplicate, wontfix'
|
||||
process-only: 'issues'
|
||||
@@ -11,12 +11,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Setup Java 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
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') }}
|
||||
key: ${{ runner.os }}-cmake-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cmake-
|
||||
|
||||
- name: Gradle Build
|
||||
@@ -45,71 +45,20 @@ 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/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
|
||||
cp app/build/outputs/apk/release/app-universal-release.apk app/build/outputs/apk/release/EhViewer-universal.apk
|
||||
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
|
||||
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64.apk
|
||||
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86.apk
|
||||
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
|
||||
|
||||
- name: Releases
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: Bump Version
|
||||
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-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
|
||||
app/build/outputs/apk/release/EhViewer-universal.apk
|
||||
app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
|
||||
app/build/outputs/apk/release/EhViewer-x86_64.apk
|
||||
app/build/outputs/apk/release/EhViewer-x86.apk
|
||||
app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
|
||||
app/build/outputs/mapping/release/mapping.txt
|
||||
@@ -6,4 +6,5 @@
|
||||
*.iml
|
||||
.idea
|
||||
/captures
|
||||
release
|
||||
google-services.json
|
||||
@@ -1,40 +0,0 @@
|
||||
# 变更日志
|
||||
|
||||
## 1.9.0
|
||||
|
||||
### 重大变更
|
||||
|
||||
* 除阅读器外全部迁移至 Compose
|
||||
|
||||
### 新功能
|
||||
|
||||
* 设置-高级 中增加实验性滑动手势敏感度设置
|
||||
* 支持通过滑动手势打开右侧抽屉
|
||||
* 支持预见式返回动画,相关设置位于 设置-高级
|
||||
|
||||
### 改进
|
||||
|
||||
* \[阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
|
||||
* 优化原图下载重试策略
|
||||
* 优化小屏设备上的搜索类别布局
|
||||
* 优化过渡动画
|
||||
* 优化历史记录删除动画
|
||||
* 设置-EH 中的缩略图大小设置改为缩略图列数
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 修复点击通知栏的下载通知闪退 [#175](https://github.com/FooIbar/EhViewer/issues/175)
|
||||
* 修复收藏状态显示错误 [#189](https://github.com/FooIbar/EhViewer/issues/189)
|
||||
* 修复双击导航按钮后无法打开导航抽屉
|
||||
* 修复某些情况下闪退 [#142](https://github.com/FooIbar/EhViewer/issues/142) [#208](https://github.com/FooIbar/EhViewer/issues/208)
|
||||
* 修复收藏界面点击重试崩溃 [#193](https://github.com/FooIbar/EhViewer/issues/193)
|
||||
* 修复本地收藏滚动位置丢失 [#192](https://github.com/FooIbar/EhViewer/issues/192)
|
||||
* 修复手速过快页面切换过快使加载圈圈不自动消失 [#209](https://github.com/FooIbar/EhViewer/issues/209)
|
||||
* 修复打开并关闭右侧抽屉后,状态栏一片白色 [#101](https://github.com/FooIbar/EhViewer/issues/101)
|
||||
* 修复使用深色主题时,切换界面会闪过亮色界面 [#121](https://github.com/FooIbar/EhViewer/issues/121)
|
||||
* \[Marshmallow] 修复小内存设备上图片解码失败 [#229](https://github.com/FooIbar/EhViewer/issues/229) [#372](https://github.com/FooIbar/EhViewer/issues/372)
|
||||
* 修复已舍弃画廊上传者解析
|
||||
|
||||
### 已知问题
|
||||
|
||||
* 画廊信息对话框无法通过返回关闭 [#413](https://github.com/FooIbar/EhViewer/issues/413)
|
||||
@@ -1,11 +1,15 @@
|
||||
# Maintenance Mode
|
||||
|
||||
The project is in maintenance mode, will only receive bugfixes.
|
||||
|
||||
<p align="right">
|
||||
<strong>English</strong>
|
||||
<span> | </span>
|
||||
<a href="/docs/README/zh-cn.md">
|
||||
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-cn.md">
|
||||
简体中文
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="/docs/README/zh-tw.md">
|
||||
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-tw.md">
|
||||
正體中文
|
||||
</a>
|
||||
</p>
|
||||
@@ -19,12 +23,15 @@
|
||||
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
|
||||
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
|
||||
</a>
|
||||
<a href="/LICENSE">
|
||||
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
|
||||
</a>
|
||||
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
|
||||
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
|
||||
</a>
|
||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
||||
<img src="https://img.shields.io/github/downloads/FooIbar/EhViewer/total.svg" alt="Downloads">
|
||||
</a>
|
||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
||||
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
|
||||
</a>
|
||||
@@ -35,23 +42,23 @@
|
||||
|
||||
<div align="center">
|
||||
<h3>
|
||||
<a href="#description">
|
||||
<a href="https://github.com/FooIbar/EhViewer#description">
|
||||
Description
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="#download">
|
||||
<a href="https://github.com/FooIbar/EhViewer#download">
|
||||
Download
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="#screenshot">
|
||||
<a href="https://github.com/FooIbar/EhViewer#screenshot">
|
||||
Screenshot
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="#thanks">
|
||||
<a href="https://github.com/FooIbar/EhViewer#thanks">
|
||||
Thanks
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="#license">
|
||||
<a href="https://github.com/FooIbar/EhViewer#license">
|
||||
License
|
||||
</a>
|
||||
</h3>
|
||||
@@ -66,17 +73,6 @@ 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 |
|
||||
|
||||
\* Archives and GIFs are not supported
|
||||
|
||||
** Fall back to OkHttp on devices that don't support [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine)
|
||||
|
||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
||||
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
|
||||
</a>
|
||||
@@ -93,13 +89,14 @@ Here is the libraries
|
||||
- [Arrow](https://arrow-kt.io/)
|
||||
- [AOSP & AndroidX](http://source.android.com/)
|
||||
- [Kotlin & KotlinX](https://kotlinlang.org/)
|
||||
- [MDC-Android](https://github.com/material-components/material-components-android)
|
||||
- [Material Icons](https://github.com/google/material-design-icons)
|
||||
- [OkHttp](https://square.github.io/okhttp/)
|
||||
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
|
||||
- [MCA](https://github.com/material-components/material-components-android)
|
||||
- [material-design-icons](https://github.com/google/material-design-icons)
|
||||
- [Okhttp](https://square.github.io/okhttp/)
|
||||
- [RikkaX](https://github.com/RikkaApps/RikkaX)
|
||||
- [Libarchive](http://www.libarchive.org/)
|
||||
- [Coil](https://coil-kt.github.io/coil/)
|
||||
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
|
||||
- [libarchive](http://www.libarchive.org/)
|
||||
- [jsoup](https://jsoup.org/)
|
||||
- [Jsoup](https://jsoup.org/)
|
||||
|
||||
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
|
||||
|
||||
|
||||
+120
-122
@@ -8,21 +8,21 @@ val isRelease: Boolean
|
||||
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.spotless)
|
||||
alias(libs.plugins.aboutlibrariesPlugin)
|
||||
alias(libs.plugins.rustAndroidPlugin)
|
||||
alias(libs.plugins.composeCompilerReportGenerator)
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.google.devtools.ksp")
|
||||
id("com.diffplug.spotless")
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
id("org.mozilla.rust-android-gradle.rust-android")
|
||||
id("dev.shreyaspatil.compose-compiler-report-generator")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
buildToolsVersion = "34.0.0"
|
||||
ndkVersion = "26.1.10909125"
|
||||
ndkVersion = "26.0.10792818"
|
||||
|
||||
splits {
|
||||
abi {
|
||||
@@ -32,7 +32,7 @@ android {
|
||||
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
|
||||
isUniversalApk = true
|
||||
} else {
|
||||
include("arm64-v8a", "x86_64")
|
||||
include("arm64-v8a")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,10 @@ android {
|
||||
enableV4Signing = true
|
||||
}
|
||||
|
||||
val branch = providers.exec {
|
||||
commandLine = "git branch --show-current".split(' ')
|
||||
}.standardOutput.asText.get().trim()
|
||||
|
||||
val commitSha = providers.exec {
|
||||
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
||||
}.standardOutput.asText.get().trim()
|
||||
@@ -64,8 +68,9 @@ android {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 180046
|
||||
versionName = "1.9.0"
|
||||
versionCode = 180043
|
||||
versionName = "1.8.9.7"
|
||||
versionNameSuffix = "-gms"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
@@ -82,40 +87,10 @@ android {
|
||||
"nb-rNO",
|
||||
),
|
||||
)
|
||||
buildConfigField("String", "BRANCH", "\"$branch\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||
ndk {
|
||||
debugSymbolLevel = "FULL"
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += listOf("api", "oss")
|
||||
|
||||
productFlavors {
|
||||
create("default") {
|
||||
dimension = "api"
|
||||
}
|
||||
create("marshmallow") {
|
||||
dimension = "api"
|
||||
minSdk = 23
|
||||
applicationIdSuffix = ".m"
|
||||
versionNameSuffix = "-M"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
targets += "noop"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
}
|
||||
create("oss") {
|
||||
dimension = "oss"
|
||||
}
|
||||
create("gms") {
|
||||
dimension = "oss"
|
||||
versionNameSuffix = "-gms"
|
||||
}
|
||||
buildConfigField("String", "VERSION_NAME_SUFFIX", "\"$versionNameSuffix\"")
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -124,12 +99,19 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
freeCompilerArgs = listOf(
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive",
|
||||
"-Xjvm-default=all",
|
||||
"-Xlambdas=indy",
|
||||
"-XXLanguage:+BreakContinueInInlineLambdas",
|
||||
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
@@ -145,15 +127,13 @@ android {
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-opt-in=splitties.experimental.ExperimentalSplittiesApi",
|
||||
"-opt-in=splitties.preferences.DataStorePreferencesPreview",
|
||||
|
||||
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
|
||||
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:intrinsicRemember=true",
|
||||
)
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += setOf("MissingTranslation", "MissingQuantity")
|
||||
fatal += setOf("NewApi", "InlineApi")
|
||||
abortOnError = true
|
||||
checkReleaseBuilds = false
|
||||
disable.add("MissingTranslation")
|
||||
}
|
||||
|
||||
packaging {
|
||||
@@ -188,7 +168,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
||||
kotlinCompilerExtensionVersion = "1.5.3"
|
||||
}
|
||||
|
||||
namespace = "com.hippo.ehviewer"
|
||||
@@ -196,97 +176,105 @@ android {
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.biometric)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation("androidx.activity:activity-compose:1.8.0-rc01")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
|
||||
implementation("androidx.browser:browser:1.6.0")
|
||||
implementation("androidx.collection:collection-ktx:1.3.0-rc01")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
||||
api(platform(libs.compose.bom))
|
||||
implementation(libs.bundles.compose)
|
||||
// api(platform("androidx.compose:compose-bom:2023.05.00"))
|
||||
api(platform("dev.chrisbanes.compose:compose-bom:2023.07.00-alpha02"))
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.compose.material3:material3-window-size-class")
|
||||
implementation("androidx.compose.ui:ui-util")
|
||||
|
||||
implementation(libs.compose.destinations.core)
|
||||
ksp(libs.compose.destinations.compiler)
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
|
||||
implementation(libs.androidx.constraintlayout.compose)
|
||||
implementation(libs.androidx.coordinatorlayout)
|
||||
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha12")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha05")
|
||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.6.2")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||
implementation(libs.bundles.androidx.navigation)
|
||||
val navigation = "2.7.3"
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:$navigation")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:$navigation")
|
||||
implementation("androidx.navigation:navigation-compose:$navigation")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||
implementation(libs.bundles.androidx.paging)
|
||||
|
||||
implementation(libs.androidx.recyclerview)
|
||||
val paging = "3.2.1"
|
||||
implementation("androidx.paging:paging-compose:$paging")
|
||||
implementation("androidx.paging:paging-runtime-ktx:$paging")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
||||
implementation("androidx.recyclerview:recyclerview-selection:1.2.0-alpha01")
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/room
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.room.paging)
|
||||
val room = "2.6.0-rc01"
|
||||
ksp("androidx.room:room-compiler:$room")
|
||||
implementation("androidx.room:room-paging:$room")
|
||||
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.photoview) // Dead Dependency
|
||||
implementation(libs.directionalviewpager) // Dead Dependency
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
implementation("androidx.work:work-runtime-ktx:2.8.1")
|
||||
implementation("com.drakeet.drawer:drawer:1.0.3")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
|
||||
// https://github.com/google/accompanist/releases
|
||||
implementation(libs.bundles.accompanist)
|
||||
implementation(libs.material)
|
||||
val accompanist = "0.32.0"
|
||||
implementation("com.google.accompanist:accompanist-themeadapter-material3:$accompanist")
|
||||
implementation("com.google.accompanist:accompanist-webview:$accompanist")
|
||||
implementation("com.google.android.material:material:1.11.0-alpha03")
|
||||
|
||||
implementation(libs.bundles.splitties)
|
||||
val splitties = "3.0.0"
|
||||
implementation("com.louiscad.splitties:splitties-appctx:$splitties")
|
||||
implementation("com.louiscad.splitties:splitties-systemservices:$splitties")
|
||||
implementation("com.louiscad.splitties:splitties-preferences:$splitties")
|
||||
implementation("com.louiscad.splitties:splitties-arch-room:$splitties")
|
||||
|
||||
// https://square.github.io/okhttp/changelogs/changelog/
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.bundles.okhttp)
|
||||
implementation(platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11"))
|
||||
implementation("com.squareup.okhttp3:okhttp-android")
|
||||
implementation("com.squareup.okhttp3:okhttp-coroutines")
|
||||
|
||||
implementation(libs.okio.jvm)
|
||||
implementation("com.squareup.okio:okio-jvm:3.5.0")
|
||||
|
||||
implementation(libs.diff)
|
||||
implementation("com.mikepenz:aboutlibraries-core:10.9.1")
|
||||
|
||||
implementation(libs.aboutlibraries.core)
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.6.1") // Dead Dependency
|
||||
implementation("dev.rikka.rikkax.core:core-ktx:1.4.1")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||
|
||||
implementation(libs.insetter) // Dead Dependency
|
||||
|
||||
implementation(libs.reorderable)
|
||||
|
||||
implementation(platform(libs.arrow.stack))
|
||||
implementation(libs.arrow.fx.coroutines)
|
||||
implementation(platform("io.arrow-kt:arrow-stack:1.2.1"))
|
||||
implementation("io.arrow-kt:arrow-fx-coroutines")
|
||||
|
||||
// https://coil-kt.github.io/coil/changelog/
|
||||
implementation(platform(libs.coil.bom))
|
||||
implementation(libs.bundles.coil)
|
||||
implementation(platform("io.coil-kt:coil-bom:2.4.0"))
|
||||
implementation("io.coil-kt:coil-compose")
|
||||
implementation("io.coil-kt:coil-gif")
|
||||
|
||||
implementation(libs.bundles.ktor)
|
||||
val ktor = "2.3.4"
|
||||
implementation("io.ktor:ktor-io-jvm:$ktor")
|
||||
implementation("io.ktor:ktor-utils-jvm:$ktor")
|
||||
|
||||
implementation(libs.bundles.kotlinx.serialization)
|
||||
implementation("org.chromium.net:cronet-api:113.5672.61")
|
||||
implementation("com.google.android.gms:play-services-cronet:18.0.1")
|
||||
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.jsoup)
|
||||
val serialization = "1.6.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:$serialization")
|
||||
|
||||
debugImplementation(libs.chucker)
|
||||
releaseImplementation(libs.chucker.nop)
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
implementation("org.jsoup:jsoup:1.16.1")
|
||||
|
||||
coreLibraryDesugaring(libs.desugar)
|
||||
val chunker = "4.0.0"
|
||||
debugImplementation("com.github.chuckerteam.chucker:library:$chunker")
|
||||
releaseImplementation("com.github.chuckerteam.chucker:library-no-op:$chunker")
|
||||
|
||||
"gmsImplementation"(libs.bundles.cronet)
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
// Workaround IME won't show again once hidden.
|
||||
// Google issue-tracker link?
|
||||
force("androidx.compose.ui:ui-text-android:1.6.0-alpha08")
|
||||
exclude("androidx.navigation", "navigation-compose")
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
|
||||
}
|
||||
|
||||
ksp {
|
||||
@@ -297,30 +285,40 @@ ksp {
|
||||
aboutLibraries {
|
||||
duplicationMode = MERGE
|
||||
duplicationRule = GROUP
|
||||
exclusionPatterns = listOf(
|
||||
"androidx\\..*".toPattern(),
|
||||
".*annotations".toPattern(),
|
||||
"com\\.google\\.code\\..*".toPattern(),
|
||||
"com\\.google\\.guava:listenablefuture".toPattern(),
|
||||
"com\\.google\\.protobuf:protobuf-javalite".toPattern(),
|
||||
"org\\.checkerframework:checker-qual".toPattern(),
|
||||
"org\\.chromium\\.net:cronet-embedded".toPattern(),
|
||||
"org\\.slf4j:slf4j-api".toPattern(),
|
||||
)
|
||||
}
|
||||
|
||||
cargo {
|
||||
module = "src/main/rust"
|
||||
libname = "ehviewer_rust"
|
||||
targets = if (isRelease) listOf("arm", "x86", "arm64", "x86_64") else listOf("arm64", "x86_64")
|
||||
targets = if (isRelease) listOf("arm", "x86", "arm64", "x86_64") else listOf("arm64")
|
||||
if (isRelease) profile = "release"
|
||||
}
|
||||
|
||||
val ktlintVersion = libs.versions.ktlint.get()
|
||||
|
||||
spotless {
|
||||
kotlin {
|
||||
// https://github.com/diffplug/spotless/issues/111
|
||||
target("src/**/*.kt")
|
||||
ktlint(ktlintVersion)
|
||||
ktlint()
|
||||
}
|
||||
kotlinGradle {
|
||||
ktlint(ktlintVersion)
|
||||
ktlint().editorConfigOverride(
|
||||
mapOf(
|
||||
"ktlint_standard_multiline-expression-wrapping" to "disabled",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.configureEach {
|
||||
if (name.startsWith("merge") && name.endsWith("JniLibFolders")) {
|
||||
if ((name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders")) {
|
||||
dependsOn("cargoBuild")
|
||||
// fix mergeDebugJniLibFolders UP-TO-DATE
|
||||
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
|
||||
|
||||
Vendored
+23
-13
@@ -1,20 +1,30 @@
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
# https://issuetracker.google.com/issues/307323842
|
||||
-keepclassmembers class androidx.compose.ui.platform.WindowInfoImpl {
|
||||
setWindowFocused(boolean);
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-allowaccessmodification
|
||||
# https://issuetracker.google.com/issues/190382641
|
||||
-keepclassmembers class kotlin.SafePublicationLazyImpl {
|
||||
java.lang.Object _value;
|
||||
}
|
||||
|
||||
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
|
||||
|
||||
-keep class com.hippo.ehviewer.client.parser.Torrent { *; }
|
||||
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
|
||||
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
|
||||
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
|
||||
-keep class com.hippo.ehviewer.ui.scene.SearchBarScene { getBinding(); }
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
-keepattributes LineNumberTable,SourceFile
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
||||
-repackageclasses
|
||||
-allowaccessmodification
|
||||
-overloadaggressively
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import com.google.android.gms.net.CronetProviderInstaller
|
||||
import com.hippo.ehviewer.Settings
|
||||
import org.chromium.net.CronetProvider
|
||||
import splitties.init.appCtx
|
||||
|
||||
val isCronetSupported: Boolean
|
||||
get() = Settings.enableCronet && CronetProvider.getAllProviders(appCtx).any { it.isEnabled }.also {
|
||||
if (!it) {
|
||||
CronetProviderInstaller.installProvider(appCtx)
|
||||
}
|
||||
}
|
||||
@@ -23,20 +23,16 @@
|
||||
android:allowBackup="true"
|
||||
android:appCategory="image"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_scheme"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="@bool/large_heap"
|
||||
android:localeConfig="@xml/locale_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:targetApi="tiramisu">
|
||||
<profileable android:shell="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.ConfigureActivity"
|
||||
android:exported="false"
|
||||
@@ -48,7 +44,6 @@
|
||||
android:configChanges="screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme.Splash"
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -139,7 +134,8 @@
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
<activity android:name=".ui.WebViewActivity" />
|
||||
<activity
|
||||
android:name=".ui.WebViewActivity" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
||||
@@ -24,7 +24,7 @@ set(BUILD_TESTING 0)
|
||||
|
||||
FetchContent_Declare(liblzma
|
||||
GIT_REPOSITORY https://github.com/FooIbar/xz.git
|
||||
GIT_TAG v5.4.5
|
||||
GIT_TAG v5.4.4
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(liblzma)
|
||||
@@ -78,5 +78,3 @@ include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED archive.c gifutils.c natsort/strnatcmp.c)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log)
|
||||
|
||||
add_custom_target("noop")
|
||||
@@ -279,7 +279,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
|
||||
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
|
||||
jlong size) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
@@ -359,7 +359,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
#pragma ide diagnostic ignored "UnreachableCode"
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
|
||||
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
|
||||
jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
@@ -389,7 +389,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass th
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
||||
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_closeArchive(JNIEnv *env, jclass thiz) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++)
|
||||
@@ -408,14 +408,14 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {
|
||||
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_needPassword(JNIEnv *env, jclass thiz) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
return need_encrypt;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
jstring str) {
|
||||
EH_UNUSED(thiz);
|
||||
struct archive_entry *entry;
|
||||
@@ -446,7 +446,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz,
|
||||
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_getFilename(JNIEnv *env, jclass thiz,
|
||||
jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
@@ -462,7 +462,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz,
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
||||
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToFd(JNIEnv *env, jclass thiz,
|
||||
jint index, jint fd) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
@@ -477,7 +477,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
|
||||
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
|
||||
jobject buffer) {
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <jni.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <android/log.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/mman.h>
|
||||
@@ -12,7 +11,6 @@
|
||||
|
||||
#define GIF_HEADER_87A "GIF87a"
|
||||
#define GIF_HEADER_89A "GIF89a"
|
||||
#define GIF_HEADER_LENGTH 6
|
||||
|
||||
static int FRAME_DELAY_START_MARKER = 0x0021F904;
|
||||
|
||||
@@ -23,8 +21,7 @@ typedef signed char byte;
|
||||
#define DEFAULT_FRAME_DELAY 10
|
||||
|
||||
static inline bool isGif(void *addr) {
|
||||
return !memcmp(addr, GIF_HEADER_87A, GIF_HEADER_LENGTH) ||
|
||||
!memcmp(addr, GIF_HEADER_89A, GIF_HEADER_LENGTH);
|
||||
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
|
||||
}
|
||||
|
||||
static void doRewrite(byte *addr, size_t size) {
|
||||
@@ -45,33 +42,20 @@ static void doRewrite(byte *addr, size_t size) {
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_GifUtilsKt_isGif(JNIEnv *env, jclass clazz, jint fd) {
|
||||
byte buffer[GIF_HEADER_LENGTH];
|
||||
return read(fd, buffer, GIF_HEADER_LENGTH) == GIF_HEADER_LENGTH &&
|
||||
isGif(buffer);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_GifUtilsKt_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
Java_com_hippo_ehviewer_image_ImageKt_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
doRewrite(addr, size);
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_ehviewer_jni_GifUtilsKt_mmap(JNIEnv *env, jclass clazz, jint fd) {
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_image_ImageKt_rewriteGifSource2(JNIEnv *env, jclass clazz, jint fd) {
|
||||
struct stat64 st;
|
||||
fstat64(fd, &st);
|
||||
size_t size = st.st_size;
|
||||
byte *addr = mmap64(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
|
||||
if (addr == MAP_FAILED) return NULL;
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_GifUtilsKt_munmap(JNIEnv *env, jclass clazz, jobject buffer) {
|
||||
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
byte *addr = mmap64(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if (addr == MAP_FAILED) return;
|
||||
doRewrite(addr, size);
|
||||
munmap(addr, size);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package androidx.navigation.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MenuItem
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavOptions
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
|
||||
val builder = NavOptions.Builder().setLaunchSingleTop(true)
|
||||
if (
|
||||
navController.currentDestination!!.parent!!.findNode(item.itemId)
|
||||
is ActivityNavigator.Destination
|
||||
) {
|
||||
builder.setEnterAnim(R.anim.nav_default_enter_anim)
|
||||
.setExitAnim(R.anim.nav_default_exit_anim)
|
||||
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
|
||||
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
|
||||
} else {
|
||||
builder.setEnterAnim(androidx.fragment.R.animator.fragment_open_enter)
|
||||
.setExitAnim(androidx.fragment.R.animator.fragment_open_exit)
|
||||
.setPopEnterAnim(androidx.fragment.R.animator.fragment_close_enter)
|
||||
.setPopExitAnim(androidx.fragment.R.animator.fragment_close_exit)
|
||||
}
|
||||
val options = builder.build()
|
||||
return try {
|
||||
// TODO provide proper API instead of using Exceptions as Control-Flow.
|
||||
navController.navigate(item.itemId, null, options)
|
||||
// Return true only if the destination we've navigated to matches the MenuItem
|
||||
navController.currentDestination?.matchDestination(item.itemId) == true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
|
||||
hierarchy.any { it.id == destId }
|
||||
@@ -0,0 +1,25 @@
|
||||
package androidx.swiperefreshlayout.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import rikka.core.res.resolveColor
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
class ThemedSwipeRefreshLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : SwipeRefreshLayout(context, attrs) {
|
||||
|
||||
init {
|
||||
val surfaceColor =
|
||||
context.theme.resolveColor(com.google.android.material.R.attr.colorSurface)
|
||||
val overlayColor =
|
||||
context.getColor(com.google.android.material.R.color.m3_popupmenu_overlay_color)
|
||||
val backgroundColor = ColorUtils.compositeColors(overlayColor, surfaceColor)
|
||||
(mCircleView.background as ShapeDrawable).paint.color = backgroundColor
|
||||
setColorSchemeColors(context.theme.resolveColor(com.google.android.material.R.attr.colorAccent))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.davemorrissey.labs.subscaleview
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
|
||||
/**
|
||||
* Helper class used to set the source and additional attributes from a variety of sources. Supports
|
||||
* use of a bitmap, asset, resource, external file or any other URI.
|
||||
*
|
||||
*
|
||||
* When you are using a preview image, you must set the dimensions of the full size image on the
|
||||
* ImageSource object for the full size image using the [.dimensions] method.
|
||||
*/
|
||||
class ImageSource {
|
||||
val bitmap: Bitmap?
|
||||
val provider: InputProvider?
|
||||
|
||||
private var sWidth = 0
|
||||
private var sHeight = 0
|
||||
|
||||
var sRegion = Rect(0, 0, 0, 0)
|
||||
private set
|
||||
var isCached = false
|
||||
private set
|
||||
|
||||
private constructor(bitmap: Bitmap, cached: Boolean) {
|
||||
this.bitmap = bitmap
|
||||
provider = null
|
||||
sWidth = bitmap.width
|
||||
sHeight = bitmap.height
|
||||
isCached = cached
|
||||
}
|
||||
|
||||
private constructor(provider: InputProvider) {
|
||||
bitmap = null
|
||||
this.provider = provider
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a region of the source image. Region must be set independently for the full size image and the preview if
|
||||
* you are using one.
|
||||
*
|
||||
* @param sRegion the region of the source image to be displayed.
|
||||
* @return this instance for chaining.
|
||||
*/
|
||||
fun region(sRegion: Rect): ImageSource {
|
||||
this.sRegion = sRegion
|
||||
setInvariants()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI
|
||||
* and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare
|
||||
* the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset.
|
||||
*
|
||||
* @param sWidth width of the source image.
|
||||
* @param sHeight height of the source image.
|
||||
* @return this instance for chaining.
|
||||
*/
|
||||
fun dimensions(sWidth: Int, sHeight: Int): ImageSource {
|
||||
if (bitmap == null) {
|
||||
this.sWidth = sWidth
|
||||
this.sHeight = sHeight
|
||||
}
|
||||
setInvariants()
|
||||
return this
|
||||
}
|
||||
|
||||
private fun setInvariants() {
|
||||
if (!sRegion.isEmpty) {
|
||||
sWidth = sRegion.width()
|
||||
sHeight = sRegion.height()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create an instance from an input provider.
|
||||
*
|
||||
* @param provider input stream provider.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun provider(provider: InputProvider): ImageSource {
|
||||
return ImageSource(provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a loaded bitmap for display.
|
||||
*
|
||||
* @param bitmap bitmap to be displayed.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun bitmap(bitmap: Bitmap): ImageSource {
|
||||
return ImageSource(bitmap, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no
|
||||
* longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso
|
||||
* or Volley.
|
||||
*
|
||||
* @param bitmap bitmap to be displayed.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun cachedBitmap(bitmap: Bitmap): ImageSource {
|
||||
return ImageSource(bitmap, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.davemorrissey.labs.subscaleview
|
||||
|
||||
import android.graphics.PointF
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.
|
||||
*/
|
||||
class ImageViewState(val scale: Float, center: PointF) : Serializable {
|
||||
private val centerX: Float = center.x
|
||||
private val centerY: Float = center.y
|
||||
|
||||
val center: PointF
|
||||
get() = PointF(centerX, centerY)
|
||||
}
|
||||
+1081
-32
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,50 @@
|
||||
package com.davemorrissey.labs.subscaleview.decoder
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
|
||||
/**
|
||||
* Interface for image decoding classes.
|
||||
*/
|
||||
interface Decoder {
|
||||
/**
|
||||
* Initialise the decoder. When possible, perform initial setup work once in this method. The
|
||||
* dimensions of the image must be returned.
|
||||
*
|
||||
* @param context Application context. A reference may be held, but must be cleared on recycle.
|
||||
* @param provider Provider of the image.
|
||||
* @return Dimensions of the image.
|
||||
* @throws Exception if initialisation fails.
|
||||
*/
|
||||
fun init(context: Context, provider: InputProvider): Point
|
||||
|
||||
/**
|
||||
* Decode a region of the image with the given sample size. This method is called off the UI
|
||||
* thread so it can safely load the image on the current thread. It is called from
|
||||
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
|
||||
* implementations must be thread safe. Adding `synchronized` to the method signature
|
||||
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
|
||||
* called concurrently.
|
||||
*
|
||||
* @param sRect Source image rectangle to decode.
|
||||
* @param sampleSize Sample size.
|
||||
* @return The decoded region. It is safe to return null if decoding fails.
|
||||
*/
|
||||
fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap
|
||||
|
||||
/**
|
||||
* Status check. Should return false before initialisation and after recycle.
|
||||
*
|
||||
* @return true if the decoder is ready to be used.
|
||||
*/
|
||||
val isReady: Boolean
|
||||
|
||||
/**
|
||||
* This method will be called when the decoder is no longer required. It should clean up any
|
||||
* resources still in use.
|
||||
*/
|
||||
fun recycle()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.davemorrissey.labs.subscaleview.decoder
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
|
||||
class ImageDecoder(private val cropBorders: Boolean, bitmapConfig: Bitmap.Config? = null) : Decoder {
|
||||
init {
|
||||
val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the decoder. When possible, perform initial setup work once in this method. The
|
||||
* dimensions of the image must be returned.
|
||||
*
|
||||
* @param context Application context. A reference may be held, but must be cleared on recycle.
|
||||
* @param provider Provider of the image.
|
||||
* @return Dimensions of the image.
|
||||
* @throws Exception if initialisation fails.
|
||||
*/
|
||||
override fun init(context: Context, provider: InputProvider): Point {
|
||||
TODO()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a region of the image with the given sample size. This method is called off the UI
|
||||
* thread so it can safely load the image on the current thread. It is called from
|
||||
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
|
||||
* implementations must be thread safe. Adding `synchronized` to the method signature
|
||||
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
|
||||
* called concurrently.
|
||||
*
|
||||
* @param sRect Source image rectangle to decode.
|
||||
* @param sampleSize Sample size.
|
||||
* @return The decoded region. It is safe to return null if decoding fails.
|
||||
*/
|
||||
override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap {
|
||||
TODO()
|
||||
}
|
||||
|
||||
/**
|
||||
* Status check. Should return false before initialisation and after recycle.
|
||||
*
|
||||
* @return true if the decoder is ready to be used.
|
||||
*/
|
||||
override val isReady: Boolean = TODO()
|
||||
|
||||
/**
|
||||
* This method will be called when the decoder is no longer required. It should clean up any
|
||||
* resources still in use.
|
||||
*/
|
||||
override fun recycle() {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
interface InputProvider
|
||||
+11
-27
@@ -20,12 +20,12 @@ import android.app.Application
|
||||
import android.content.ComponentCallbacks2
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.snapshots.Snapshot
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.util.DebugLogger
|
||||
import com.google.android.gms.net.CronetProviderInstaller
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
@@ -48,15 +48,11 @@ import com.hippo.ehviewer.util.Crash
|
||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.isAtLeastP
|
||||
import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import com.hippo.ehviewer.util.isAtLeastS
|
||||
import com.hippo.ehviewer.util.isCronetSupported
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
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 kotlinx.coroutines.launch
|
||||
import moe.tarsin.kt.unreachable
|
||||
import okhttp3.AsyncDns
|
||||
@@ -70,15 +66,6 @@ private val lifecycleScope = lifecycle.coroutineScope
|
||||
|
||||
class EhApplication : Application(), ImageLoaderFactory {
|
||||
override fun onCreate() {
|
||||
// Initialize Settings on first access
|
||||
lifecycleScope.launchIO {
|
||||
val mode = Settings.theme
|
||||
if (!isAtLeastS) {
|
||||
withUIContext {
|
||||
AppCompatDelegate.setDefaultNightMode(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycle.addObserver(lockObserver)
|
||||
val handler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { t, e ->
|
||||
@@ -91,17 +78,22 @@ class EhApplication : Application(), ImageLoaderFactory {
|
||||
handler?.uncaughtException(t, e)
|
||||
}
|
||||
super.onCreate()
|
||||
if (isAtLeastP) {
|
||||
System.loadLibrary("ehviewer")
|
||||
}
|
||||
CronetProviderInstaller.installProvider(appCtx)
|
||||
System.loadLibrary("ehviewer")
|
||||
System.loadLibrary("ehviewer_rust")
|
||||
ReadableTime.initialize(this)
|
||||
lifecycleScope.launchIO {
|
||||
val theme = Settings.theme
|
||||
withUIContext {
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
}
|
||||
}
|
||||
lifecycleScope.launchIO {
|
||||
launchIO {
|
||||
migrateCookies()
|
||||
}
|
||||
launchIO {
|
||||
EhTagDatabase
|
||||
EhTagDatabase.update()
|
||||
}
|
||||
launchIO {
|
||||
EhDB
|
||||
@@ -119,11 +111,6 @@ class EhApplication : Application(), ImageLoaderFactory {
|
||||
}
|
||||
}
|
||||
cleanObsoleteCache(this)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Snapshot.registerApplyObserver { anies, _ ->
|
||||
logcat { anies.toString() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun cleanupDownload() {
|
||||
@@ -165,9 +152,7 @@ class EhApplication : Application(), ImageLoaderFactory {
|
||||
} else {
|
||||
callFactory(baseOkHttpClient)
|
||||
}
|
||||
if (isAtLeastP) {
|
||||
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
|
||||
}
|
||||
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
|
||||
add(MergeInterceptor)
|
||||
}
|
||||
diskCache(imageCache)
|
||||
@@ -185,7 +170,6 @@ class EhApplication : Application(), ImageLoaderFactory {
|
||||
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
|
||||
}
|
||||
chunker { alwaysReadResponseBody(false) }
|
||||
addInterceptor(UncaughtExceptionInterceptor())
|
||||
addInterceptor(CloudflareInterceptor(appCtx))
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -130,7 +130,7 @@ object EhDB {
|
||||
deleteGalleryInfo(galleryInfo)
|
||||
}
|
||||
|
||||
suspend fun removeLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
|
||||
suspend fun removeLocalFavorites(galleryInfoList: List<BaseGalleryInfo>) {
|
||||
galleryInfoList.forEach {
|
||||
removeLocalFavorites(it)
|
||||
}
|
||||
@@ -146,7 +146,7 @@ object EhDB {
|
||||
db.localFavoritesDao().upsert(LocalFavoriteInfo(galleryInfo.gid))
|
||||
}
|
||||
|
||||
suspend fun putLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
|
||||
suspend fun putLocalFavorites(galleryInfoList: List<BaseGalleryInfo>) {
|
||||
galleryInfoList.forEach {
|
||||
putLocalFavorites(it)
|
||||
}
|
||||
@@ -195,7 +195,7 @@ object EhDB {
|
||||
db.historyDao().upsert(HistoryInfo(galleryInfo.gid))
|
||||
}
|
||||
|
||||
suspend fun updateFavoriteSlot(gid: Long, slot: Int) {
|
||||
suspend fun modifyHistoryInfoFavslotNonRefresh(gid: Long, slot: Int) {
|
||||
val dao = db.galleryDao()
|
||||
dao.load(gid)?.let {
|
||||
it.favoriteSlot = slot
|
||||
@@ -292,7 +292,7 @@ object EhDB {
|
||||
}
|
||||
runCatching {
|
||||
val downloadInfoList = oldDB.downloadsDao().joinList().asReversed()
|
||||
DownloadManager.addDownload(downloadInfoList)
|
||||
DownloadManager.addDownload(downloadInfoList, false)
|
||||
}
|
||||
runCatching {
|
||||
val historyInfoList = oldDB.historyDao().list()
|
||||
Regular → Executable
+10
-103
@@ -2,95 +2,18 @@
|
||||
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisallowComposableCalls
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import java.util.Locale
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.map
|
||||
import splitties.preferences.BoolPref
|
||||
import splitties.preferences.DataStorePreferences
|
||||
import splitties.preferences.FloatPref
|
||||
import splitties.preferences.IntPref
|
||||
import splitties.preferences.LongPref
|
||||
import splitties.preferences.PrefDelegate
|
||||
import splitties.preferences.StringOrNullPref
|
||||
import splitties.preferences.StringPref
|
||||
import splitties.preferences.StringSetOrNullPref
|
||||
import splitties.preferences.StringSetPref
|
||||
import splitties.preferences.edit
|
||||
|
||||
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
|
||||
fun <T> PrefDelegate<T>.getValue(): T = when (this) {
|
||||
is BoolPref -> value
|
||||
is IntPref -> value
|
||||
is FloatPref -> value
|
||||
is LongPref -> value
|
||||
is StringPref -> value
|
||||
is StringOrNullPref -> value
|
||||
is StringSetPref -> value
|
||||
is StringSetOrNullPref -> value
|
||||
} as T
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> PrefDelegate<T>.setValue(newValue: T) = when (this) {
|
||||
is BoolPref -> value = newValue as Boolean
|
||||
is IntPref -> value = newValue as Int
|
||||
is FloatPref -> value = newValue as Float
|
||||
is LongPref -> value = newValue as Long
|
||||
is StringPref -> value = newValue as String
|
||||
is StringOrNullPref -> value = newValue as? String
|
||||
is StringSetPref -> value = newValue as Set<String?>
|
||||
is StringSetOrNullPref -> value = newValue as? Set<String?>
|
||||
}
|
||||
|
||||
@Stable
|
||||
@Composable
|
||||
inline fun <R, T> PrefDelegate<T>.collectAsState(crossinline transform: @DisallowComposableCalls (T) -> R): State<R> {
|
||||
val flow = remember { valueFlow().map { transform(it) } }
|
||||
val init = getValue()
|
||||
return flow.collectAsState(transform(init))
|
||||
}
|
||||
|
||||
@Stable
|
||||
@Composable
|
||||
fun <T> PrefDelegate<T>.collectAsState(): State<T> {
|
||||
val flow = remember { valueFlow() }
|
||||
val init = getValue()
|
||||
return flow.collectAsState(init)
|
||||
}
|
||||
|
||||
@Stable
|
||||
@Composable
|
||||
fun <T> PrefDelegate<T>.asMutableState(): MutableState<T> {
|
||||
val flow = remember { valueFlow() }
|
||||
val init = getValue()
|
||||
val readOnly = flow.collectAsState(init)
|
||||
return remember {
|
||||
object : MutableState<T> {
|
||||
override var value: T
|
||||
get() = readOnly.value
|
||||
set(value) {
|
||||
setValue(value)
|
||||
}
|
||||
override fun component1() = TODO()
|
||||
override fun component2() = TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
import java.util.Locale
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
object Settings : DataStorePreferences(null) {
|
||||
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
|
||||
@@ -101,37 +24,30 @@ object Settings : DataStorePreferences(null) {
|
||||
var favCount by intArrayPref("fav_count", 10).emitTo(_favFlow)
|
||||
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
|
||||
|
||||
val listMode = intPref("list_mode_2", 0)
|
||||
val detailSize = intPref("detail_size_2", 0)
|
||||
val thumbColumns = intPref("thumb_columns", 3)
|
||||
val listThumbSize = intPref("list_tile_size", 40)
|
||||
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
|
||||
val predictiveNavAnim = boolPref("predictive_nav_anim", true)
|
||||
|
||||
val needSignInFlow: Flow<Boolean>
|
||||
var downloadScheme by stringOrNullPref("image_scheme", null)
|
||||
var downloadAuthority by stringOrNullPref("image_authority", null)
|
||||
var downloadPath by stringOrNullPref("image_path", null)
|
||||
var downloadQuery by stringOrNullPref("image_query", null)
|
||||
var downloadFragment by stringOrNullPref("image_fragment", null)
|
||||
var archivePasswds by stringSetOrNullPref("archive_passwds")
|
||||
var downloadDelay by intPref("download_delay_3", 1000)
|
||||
var downloadDelay by intPref("download_delay_2", 0)
|
||||
var gallerySite by intPref("gallery_site_2", 0).observed { updateWhenGallerySiteChanges() }
|
||||
var multiThreadDownload by intPref("download_thread_2", 3)
|
||||
var preloadImage by intPref("preload_image_2", 5)
|
||||
var downloadTimeout by intPref("download_timeout", 60)
|
||||
var theme by intPref("theme_2", -1).observed { updateWhenThemeChanges() }
|
||||
var listMode by intPref("list_mode_2", 0)
|
||||
var detailSize by intPref("detail_size_2", 0)
|
||||
var thumbResolution by intPref("thumb_resolution_2", 0)
|
||||
var readCacheSize by intPref("read_cache_size_2", 640)
|
||||
var launchPage by intPref("launch_page_2", 0)
|
||||
var commentThreshold by intPref("comment_threshold", -101)
|
||||
var forceEhThumb by boolPref("force_eh_thumb", false)
|
||||
var showComments by boolPref("show_gallery_comments", true)
|
||||
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
|
||||
var hideHvEvents by boolPref("hide_hv_events", false)
|
||||
var showJpnTitle by boolPref("show_jpn_title", false)
|
||||
var showGalleryPages by boolPref("show_gallery_pages", true)
|
||||
var showGalleryPages by boolPref("show_gallery_pages", false)
|
||||
var showTagTranslations by boolPref(KEY_SHOW_TAG_TRANSLATIONS, false).observed { updateWhenTagTranslationChanges() }
|
||||
var meteredNetworkWarning by boolPref("cellular_network_warning", false)
|
||||
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
|
||||
var enabledSecurity by boolPref("enable_secure", false)
|
||||
var useCIUpdateChannel by boolPref("ci_update_channel", false)
|
||||
@@ -142,22 +58,21 @@ object Settings : DataStorePreferences(null) {
|
||||
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).also { needSignInFlow = it.valueFlow() }
|
||||
var needSignIn by boolPref("need_sign_in", true)
|
||||
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
|
||||
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
||||
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
||||
var downloadOriginImage by boolPref("download_origin_image", false)
|
||||
var enableCronet by boolPref("enable_cronet", true)
|
||||
var thumbSizeDp by intPref("thumb_size_", 120)
|
||||
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)
|
||||
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
|
||||
var listThumbSize by intPref("list_tile_size", 40)
|
||||
var searchCategory by intPref("search_pref", EhUtils.ALL_CATEGORY)
|
||||
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
|
||||
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
|
||||
var updateIntervalDays by intPref("update_interval_days", 0)
|
||||
var recentToplist by stringPref("recent_toplist", "11")
|
||||
var userAgent by stringPref("user_agent", CHROME_USER_AGENT)
|
||||
var recentDownloadLabel by stringOrNullPref("recent_download_label", null)
|
||||
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
|
||||
var displayName by stringOrNullPref("display_name", null)
|
||||
@@ -165,10 +80,6 @@ object Settings : DataStorePreferences(null) {
|
||||
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
|
||||
var lastDawnDay by longPref("last_dawn_day", 0)
|
||||
var lastUpdateDay by longPref("last_update_day", 0)
|
||||
var favDialogTheta by floatPref("fav_select_dialog_delta", 0F)
|
||||
|
||||
// TODO: Remove this after swipe gestures are correctly handled in compose
|
||||
var touchSlopFactor by intPref("touch_slop", 3)
|
||||
|
||||
init {
|
||||
if ("CN" == Locale.getDefault().country) {
|
||||
@@ -186,8 +97,6 @@ object Settings : DataStorePreferences(null) {
|
||||
|
||||
private fun intArrayPref(key: String, count: Int) = object : Delegate<IntArray> {
|
||||
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
|
||||
|
||||
@Suppress("ktlint:standard:property-naming")
|
||||
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
|
||||
override fun getValue(thisRef: Any?, prop: KProperty<*>?): IntArray = _value.map { it.value }.toIntArray()
|
||||
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: IntArray) {
|
||||
@@ -198,8 +107,6 @@ object Settings : DataStorePreferences(null) {
|
||||
|
||||
private fun stringArrayPref(key: String, count: Int, defMetaValue: String) = object : Delegate<Array<String>> {
|
||||
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
|
||||
|
||||
@Suppress("ktlint:standard:property-naming")
|
||||
private var _value = (0 until count).map { stringPref("${key}_$it", "$defMetaValue $it") }.toTypedArray()
|
||||
override fun getValue(thisRef: Any?, prop: KProperty<*>?): Array<String> = _value.map { it.value }.toTypedArray()
|
||||
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Array<String>) {
|
||||
+1
-12
@@ -1,6 +1,5 @@
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import android.app.UiModeManager
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
||||
import androidx.core.os.LocaleListCompat
|
||||
@@ -9,7 +8,6 @@ import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
|
||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||
import com.hippo.ehviewer.util.isAtLeastS
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -19,7 +17,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import splitties.init.appCtx
|
||||
import splitties.preferences.PrefDelegate
|
||||
import splitties.systemservices.uiModeManager
|
||||
|
||||
private val collectScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
fun <T, R : PrefDelegate<T>> R.observed(func: (Unit) -> Unit) = apply { collectScope.launch { changesFlow().collect(func) } }
|
||||
@@ -47,14 +44,6 @@ fun updateWhenKeepMediaStatusChanges() {
|
||||
fun updateWhenThemeChanges() {
|
||||
collectScope.launch {
|
||||
delay(100) // Avoid recompose being cancelled
|
||||
if (isAtLeastS) {
|
||||
val mode = when (Settings.theme) {
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> UiModeManager.MODE_NIGHT_NO
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> UiModeManager.MODE_NIGHT_YES
|
||||
else -> UiModeManager.MODE_NIGHT_AUTO
|
||||
}
|
||||
uiModeManager.setApplicationNightMode(mode)
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(Settings.theme)
|
||||
}
|
||||
}
|
||||
@@ -62,7 +51,7 @@ fun updateWhenThemeChanges() {
|
||||
fun updateWhenAmoledModeChanges() {
|
||||
collectScope.launch {
|
||||
delay(100) // Avoid recompose being cancelled
|
||||
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY) // Trigger onNightModeChanged
|
||||
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY)
|
||||
AppCompatDelegate.setDefaultNightMode(Settings.theme)
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+21
-25
@@ -16,11 +16,6 @@
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastJoinToString
|
||||
import androidx.compose.ui.util.fastMapNotNull
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
@@ -29,15 +24,13 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
object EhCookieStore : CookieJar {
|
||||
private val manager = CookieManager.getInstance()
|
||||
fun signOut() = manager.removeAllCookies(null)
|
||||
fun contains(url: HttpUrl, name: String) = get(url).fastAny { it.name == name }
|
||||
fun contains(url: HttpUrl, name: String) = get(url).any { it.name == name }
|
||||
|
||||
fun get(url: HttpUrl): List<Cookie> {
|
||||
val cookies = manager.getCookie(url.toString())
|
||||
|
||||
return if (!cookies.isNullOrEmpty()) {
|
||||
cookies.split(";").fastMapNotNull { setCookie ->
|
||||
Cookie.parse(url, setCookie)?.takeUnless { it.name == KEY_UTMP_NAME }
|
||||
}
|
||||
return if (cookies != null && cookies.isNotEmpty()) {
|
||||
cookies.split(";").mapNotNull { Cookie.parse(url, it) }.filterNot { it.name == KEY_UTMP_NAME }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
@@ -64,12 +57,9 @@ object EhCookieStore : CookieJar {
|
||||
}.build()
|
||||
|
||||
fun copyNecessaryCookies() {
|
||||
val cookies = get(EhUrl.HOST_E.toHttpUrl())
|
||||
cookies.fastForEach {
|
||||
if (it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH) {
|
||||
manager.setCookie(EhUrl.HOST_EX, it.toString())
|
||||
}
|
||||
}
|
||||
val cookie = get(EhUrl.HOST_E.toHttpUrl()).filter { it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH || it.name == KEY_IGNEOUS }
|
||||
cookie.forEach { manager.setCookie(EhUrl.HOST_EX, it.toString()) }
|
||||
flush()
|
||||
}
|
||||
|
||||
fun deleteCookie(url: HttpUrl, name: String) {
|
||||
@@ -77,29 +67,35 @@ object EhCookieStore : CookieJar {
|
||||
}
|
||||
|
||||
fun addCookie(cookie: Cookie) {
|
||||
manager.setCookie(if (EhUrl.DOMAIN_E == cookie.domain) EhUrl.HOST_E else EhUrl.HOST_EX, cookie.toString())
|
||||
if (EhUrl.DOMAIN_E in cookie.domain) manager.setCookie(EhUrl.HOST_E, cookie.toString()) else manager.setCookie(EhUrl.DOMAIN_EX, cookie.toString())
|
||||
}
|
||||
|
||||
fun flush() = manager.flush()
|
||||
|
||||
fun getCookieHeader(url: HttpUrl): String {
|
||||
return loadForRequest(url).fastJoinToString("; ") {
|
||||
"${it.name}=${it.value}"
|
||||
val cookies = loadForRequest(url)
|
||||
val cookieHeader = StringBuilder()
|
||||
var i = 0
|
||||
val size = cookies.size
|
||||
while (i < size) {
|
||||
if (i > 0) {
|
||||
cookieHeader.append("; ")
|
||||
}
|
||||
val cookie = cookies[i]
|
||||
cookieHeader.append(cookie.name).append('=').append(cookie.value)
|
||||
i++
|
||||
}
|
||||
return cookieHeader.toString()
|
||||
}
|
||||
|
||||
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.fastForEach {
|
||||
if (it.name != KEY_UTMP_NAME) {
|
||||
manager.setCookie(url.toString(), it.toString())
|
||||
}
|
||||
}
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.filterNot { it.name == KEY_UTMP_NAME }.forEach { manager.setCookie(url.toString(), it.toString()) }.also { flush() }
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
val checkTips = EhUrl.DOMAIN_E in url.host
|
||||
return get(url).run {
|
||||
if (checkTips) {
|
||||
fastFilter { it.name != KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
|
||||
filterNot { it.name == KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
+99
-110
@@ -21,7 +21,6 @@ import arrow.fx.coroutines.parZip
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
||||
@@ -35,6 +34,7 @@ import com.hippo.ehviewer.client.parser.GalleryApiParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryListParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageApiParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
@@ -48,50 +48,57 @@ import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||
import com.hippo.ehviewer.cronet.awaitBodyFully
|
||||
import com.hippo.ehviewer.cronet.cronetRequest
|
||||
import com.hippo.ehviewer.cronet.execute
|
||||
import com.hippo.ehviewer.cronet.withRequestBody
|
||||
import com.hippo.ehviewer.dailycheck.showEventNotification
|
||||
import com.hippo.ehviewer.dailycheck.today
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.StatusCodeException
|
||||
import com.hippo.ehviewer.util.isCronetSupported
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import io.ktor.utils.io.pool.useInstance
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.ceil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.addJsonArray
|
||||
import kotlinx.serialization.json.put
|
||||
import moe.tarsin.coroutines.removeAllSuspend
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import org.jsoup.Jsoup
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.ceil
|
||||
|
||||
private const val TAG = "EhEngine"
|
||||
private const val MAX_REQUEST_SIZE = 25
|
||||
private const val SAD_PANDA_DISPOSITION = "inline; filename=\"sadpanda.jpg\""
|
||||
private const val SAD_PANDA_TYPE = "image/gif"
|
||||
private const val SAD_PANDA_LENGTH = "9615"
|
||||
private const val KOKOMADE_URL = "https://exhentai.org/img/kokomade.jpg"
|
||||
private const val U_CONFIG_TEXT = "Selected Profile"
|
||||
private val MEDIA_TYPE_JPEG: MediaType = "image/jpeg".toMediaType()
|
||||
|
||||
private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
|
||||
private fun rethrowExactly(code: Int, headers: Headers, body: String, e: Throwable): Nothing {
|
||||
// Don't translate coroutine cancellation
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
// Check sad panda
|
||||
if (SAD_PANDA_DISPOSITION == headers["Content-Disposition"] && SAD_PANDA_TYPE == headers["Content-Type"] && SAD_PANDA_LENGTH == headers["Content-Length"]) {
|
||||
throw EhException("Sad Panda")
|
||||
}
|
||||
|
||||
// Check sad panda(without panda)
|
||||
if (body.isEmpty()) {
|
||||
if (EhUtils.isExHentai) {
|
||||
throw EhException("Sad Panda\n(without panda)")
|
||||
} else {
|
||||
throw EhException("IP banned")
|
||||
}
|
||||
if ("text/html; charset=UTF-8" == headers["Content-Type"] && "0" == headers["Content-Length"] && EhUtils.isExHentai) {
|
||||
throw EhException("Sad Panda\n(without panda)")
|
||||
}
|
||||
|
||||
// Check kokomade
|
||||
if (body.contains(KOKOMADE_URL)) {
|
||||
throw EhException("今回はここまで ${appCtx.getString(R.string.kokomade_tip)}".trimIndent())
|
||||
}
|
||||
|
||||
// Check Gallery Not Available
|
||||
@@ -107,8 +114,10 @@ private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
|
||||
throw StatusCodeException(code)
|
||||
}
|
||||
|
||||
if (e is ParseException || e is SerializationException) {
|
||||
if ("<" !in body) {
|
||||
if (e is ParseException) {
|
||||
if (body.isEmpty()) {
|
||||
throw EhException(appCtx.getString(R.string.error_empty_html))
|
||||
} else if ("<" !in body) {
|
||||
throw EhException(body)
|
||||
} else {
|
||||
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e, body)
|
||||
@@ -120,53 +129,33 @@ private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
|
||||
throw e
|
||||
}
|
||||
|
||||
val httpContentPool = DirectByteBufferPool(8, 0x80000)
|
||||
val httpContentPool = DirectByteBufferPool(16, 16384)
|
||||
|
||||
suspend fun <T> fetchCompat(
|
||||
url: String,
|
||||
referer: String? = null,
|
||||
origin: String? = null,
|
||||
reqBody: RequestBody? = null,
|
||||
parser: suspend (ByteBuffer) -> T,
|
||||
): T {
|
||||
Log.d(TAG, url)
|
||||
fun commonChecks(body: ByteBuffer) {
|
||||
// Check sad panda(without panda)
|
||||
if (!body.hasRemaining()) {
|
||||
if (EhUtils.isExHentai) {
|
||||
throw EhException("Sad Panda\n(without panda)")
|
||||
} else {
|
||||
throw EhException("IP banned")
|
||||
}
|
||||
}
|
||||
}
|
||||
suspend inline fun <T> fetchCompat(url: String, referer: String? = null, crossinline parser: (ByteBuffer) -> T): T {
|
||||
return if (isCronetSupported) {
|
||||
cronetRequest(url, referer, origin) {
|
||||
reqBody?.let { withRequestBody(it) }
|
||||
}.execute {
|
||||
@Suppress("NewApi")
|
||||
val code = it.httpStatusCode
|
||||
if (code >= 400) {
|
||||
throw StatusCodeException(code)
|
||||
}
|
||||
httpContentPool.useInstance { buffer ->
|
||||
awaitBodyFully(buffer)
|
||||
cronetRequest(url, referer).execute {
|
||||
val buffer = httpContentPool.borrow()
|
||||
try {
|
||||
awaitBodyFully {
|
||||
buffer.put(it)
|
||||
}
|
||||
buffer.flip()
|
||||
commonChecks(buffer)
|
||||
parser(buffer)
|
||||
} finally {
|
||||
httpContentPool.recycle(buffer)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ehRequest(url, referer, origin) {
|
||||
reqBody?.let { post(it) }
|
||||
}.execute {
|
||||
httpContentPool.useInstance { buffer ->
|
||||
ehRequest(url, referer).execute {
|
||||
val buffer = httpContentPool.borrow()
|
||||
try {
|
||||
while (true) {
|
||||
if (body.source().read(buffer) == -1) break
|
||||
}
|
||||
buffer.flip()
|
||||
commonChecks(buffer)
|
||||
parser(buffer)
|
||||
} finally {
|
||||
httpContentPool.recycle(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +169,7 @@ private suspend inline fun <T> Request.executeAndParsingWith(block: suspend Stri
|
||||
try {
|
||||
block(body)
|
||||
} catch (e: Exception) {
|
||||
rethrowExactly(code, body, e)
|
||||
rethrowExactly(code, headers, body, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,7 +188,7 @@ object EhEngine {
|
||||
|
||||
suspend fun getTorrentList(url: String, gid: Long, token: String?): TorrentResult {
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
return fetchCompat(url, referer, parser = TorrentParser::parse)
|
||||
return fetchCompat(url, referer, TorrentParser::parse)
|
||||
}
|
||||
|
||||
suspend fun getArchiveList(url: String, gid: Long, token: String?) = ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
|
||||
@@ -243,9 +232,9 @@ object EhEngine {
|
||||
return ehRequest(url, referer).executeAndParsingWith(GalleryPageParser::parse)
|
||||
}
|
||||
|
||||
suspend fun getGalleryList(url: String) = fetchCompat(url, EhUrl.referer, parser = GalleryListParser::parse)
|
||||
suspend fun getGalleryList(url: String) = ehRequest(url, EhUrl.referer)
|
||||
.executeAndParsingWith(GalleryListParser::parse)
|
||||
.apply { fillGalleryList(galleryInfoList, url, true) }
|
||||
.takeUnless { it.galleryInfoList.isEmpty() } ?: GalleryListParser.emptyResult
|
||||
|
||||
suspend fun getGalleryDetail(url: String) = ehRequest(url, EhUrl.referer).executeAndParsingWith {
|
||||
EventPaneParser.parse(this)?.let {
|
||||
@@ -259,8 +248,9 @@ object EhEngine {
|
||||
GalleryDetailParser.parsePreviewList(this) to GalleryDetailParser.parsePreviewPages(this)
|
||||
}
|
||||
|
||||
suspend fun getFavorites(url: String) = fetchCompat(url, EhUrl.referer, parser = FavoritesParser::parse)
|
||||
.apply { fillGalleryList(galleryInfoList, url) }
|
||||
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer)
|
||||
.executeAndParsingWith(FavoritesParser::parse)
|
||||
.apply { fillGalleryList(galleryInfoList, url, false) }
|
||||
|
||||
suspend fun signIn(username: String, password: String): String {
|
||||
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
|
||||
@@ -278,13 +268,13 @@ object EhEngine {
|
||||
}.executeAndParsingWith(SignInParser::parse)
|
||||
}
|
||||
|
||||
suspend fun commentGallery(url: String, comment: String, id: Long = -1) = ehRequest(url, url, EhUrl.origin) {
|
||||
suspend fun commentGallery(url: String, comment: String, id: String?) = ehRequest(url, url, EhUrl.origin) {
|
||||
formBody {
|
||||
if (id == -1L) {
|
||||
if (id == null) {
|
||||
add("commenttext_new", comment)
|
||||
} else {
|
||||
add("commenttext_edit", comment)
|
||||
add("edit_comment", id.toString())
|
||||
add("edit_comment", id)
|
||||
}
|
||||
}
|
||||
}.executeAndParsingWith {
|
||||
@@ -296,21 +286,31 @@ object EhEngine {
|
||||
GalleryDetailParser.parseComments(document)
|
||||
}
|
||||
|
||||
suspend fun modifyFavorites(gid: Long, token: String?, dstCat: Int = -1, note: String = "") {
|
||||
/**
|
||||
* @param dstCat -1 for delete, 0 - 9 for cloud favorite, others throw Exception
|
||||
* @param note max 250 characters
|
||||
*/
|
||||
suspend fun modifyFavorites(
|
||||
gid: Long,
|
||||
token: String?,
|
||||
dstCat: Int = -1,
|
||||
note: String = "",
|
||||
) {
|
||||
val catStr: String = when (dstCat) {
|
||||
-1 -> "favdel"
|
||||
in 0..9 -> dstCat.toString()
|
||||
else -> throw EhException("Invalid dstCat: $dstCat")
|
||||
}
|
||||
val url = EhUrl.getAddFavorites(gid, token)
|
||||
val body = formBody {
|
||||
add("favcat", catStr)
|
||||
add("favnote", note)
|
||||
// apply=Add+to+Favorites is not necessary, just use apply=Apply+Changes all the time
|
||||
add("apply", "Apply Changes")
|
||||
add("update", "1")
|
||||
}
|
||||
fetchCompat(url, url, EhUrl.origin, body) { }
|
||||
return ehRequest(url, url, EhUrl.origin) {
|
||||
formBody {
|
||||
add("favcat", catStr)
|
||||
add("favnote", note)
|
||||
// apply=Add+to+Favorites is not necessary, just use apply=Apply+Changes all the time
|
||||
add("apply", "Apply Changes")
|
||||
add("update", "1")
|
||||
}
|
||||
}.executeAndParsingWith { }
|
||||
}
|
||||
|
||||
suspend fun downloadArchive(
|
||||
@@ -363,24 +363,18 @@ object EhEngine {
|
||||
}
|
||||
}.executeAndParsingWith(HomeParser::parseResetLimits)
|
||||
|
||||
suspend fun modifyFavorites(gidArray: LongArray, srcCat: Int, dstCat: Int): FavoritesParser.Result {
|
||||
val url = ehUrl {
|
||||
addPathSegments(EhUrl.FAV_PATH)
|
||||
if (FavListUrlBuilder.isValidFavCat(srcCat)) {
|
||||
addQueryParameter("favcat", srcCat.toString())
|
||||
}
|
||||
}.toString()
|
||||
suspend fun modifyFavorites(url: String, gidArray: LongArray, dstCat: Int): FavoritesParser.Result {
|
||||
val catStr: String = when (dstCat) {
|
||||
-1 -> "delete"
|
||||
in 0..9 -> "fav$dstCat"
|
||||
else -> throw EhException("Invalid dstCat: $dstCat")
|
||||
}
|
||||
val body = formBody {
|
||||
add("ddact", catStr)
|
||||
gidArray.forEach { add("modifygids[]", it.toString()) }
|
||||
}
|
||||
return fetchCompat(url, url, EhUrl.origin, body, FavoritesParser::parse)
|
||||
.apply { fillGalleryList(galleryInfoList, url) }
|
||||
return ehRequest(url, url, EhUrl.origin) {
|
||||
formBody {
|
||||
add("ddact", catStr)
|
||||
gidArray.forEach { add("modifygids[]", it.toString()) }
|
||||
}
|
||||
}.executeAndParsingWith(FavoritesParser::parse).apply { fillGalleryList(galleryInfoList, url, false) }
|
||||
}
|
||||
|
||||
suspend fun getGalleryPageApi(
|
||||
@@ -389,7 +383,7 @@ object EhEngine {
|
||||
pToken: String,
|
||||
showKey: String?,
|
||||
previousPToken: String?,
|
||||
): GalleryPageParser.Result {
|
||||
): GalleryPageApiParser.Result {
|
||||
val referer = if (index > 0 && previousPToken != null) EhUrl.getPageUrl(gid, index - 1, previousPToken) else null
|
||||
return ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
@@ -399,7 +393,7 @@ object EhEngine {
|
||||
put("imgkey", pToken)
|
||||
put("showkey", showKey)
|
||||
}
|
||||
}.executeAndParsingWith { GalleryPageParser.parse(filterNot { it == '\\' }) }
|
||||
}.executeAndParsingWith(GalleryPageApiParser::parse)
|
||||
}
|
||||
|
||||
suspend fun rateGallery(
|
||||
@@ -419,23 +413,22 @@ object EhEngine {
|
||||
}
|
||||
}.executeAndParsingWith(String::parseAs)
|
||||
|
||||
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String) =
|
||||
galleryInfoList.chunked(MAX_REQUEST_SIZE).parMap(concurrency = Settings.multiThreadDownload) {
|
||||
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "gdata")
|
||||
array("gidlist") {
|
||||
it.forEach {
|
||||
addJsonArray {
|
||||
add(it.gid)
|
||||
add(it.token)
|
||||
}
|
||||
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String) = galleryInfoList.chunked(MAX_REQUEST_SIZE).parMap {
|
||||
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "gdata")
|
||||
array("gidlist") {
|
||||
it.forEach {
|
||||
addJsonArray {
|
||||
add(it.gid)
|
||||
add(it.token)
|
||||
}
|
||||
}
|
||||
put("namespace", 1)
|
||||
}
|
||||
}.executeAndParsingWith { GalleryApiParser.parse(this, it) }
|
||||
}
|
||||
put("namespace", 1)
|
||||
}
|
||||
}.executeAndParsingWith { GalleryApiParser.parse(this, it) }
|
||||
}
|
||||
|
||||
suspend fun voteComment(
|
||||
apiUid: Long,
|
||||
@@ -491,21 +484,17 @@ object EhEngine {
|
||||
/**
|
||||
* @param image Must be jpeg
|
||||
*/
|
||||
suspend fun imageSearch(image: File, uss: Boolean, osc: Boolean) = fetchCompat(
|
||||
EhUrl.imageSearchUrl,
|
||||
EhUrl.referer,
|
||||
EhUrl.origin,
|
||||
suspend fun imageSearch(image: File, uss: Boolean, osc: Boolean) = ehRequest(EhUrl.imageSearchUrl, EhUrl.referer, EhUrl.origin) {
|
||||
multipartBody {
|
||||
setType(MultipartBody.FORM)
|
||||
addFormDataPart("sfile", "a.jpg", image.asRequestBody(MEDIA_TYPE_JPEG))
|
||||
if (uss) addFormDataPart("fs_similar", "on")
|
||||
if (osc) addFormDataPart("fs_covers", "on")
|
||||
addFormDataPart("f_sfile", "File Search")
|
||||
},
|
||||
parser = GalleryListParser::parse,
|
||||
).apply { fillGalleryList(galleryInfoList, EhUrl.imageSearchUrl) }
|
||||
}
|
||||
}.executeAndParsingWith(GalleryListParser::parse).apply { fillGalleryList(galleryInfoList, EhUrl.imageSearchUrl, true) }
|
||||
|
||||
private suspend fun fillGalleryList(list: MutableList<BaseGalleryInfo>, url: String, filter: Boolean = false) {
|
||||
private suspend fun fillGalleryList(list: MutableList<BaseGalleryInfo>, url: String, filter: Boolean) {
|
||||
// Filter title and uploader
|
||||
if (filter) list.removeAllSuspend { EhFilter.filterTitle(it) || EhFilter.filterUploader(it) }
|
||||
|
||||
@@ -513,7 +502,7 @@ object EhEngine {
|
||||
var hasPages = false
|
||||
var hasRated = false
|
||||
for (gi in list) {
|
||||
if (!gi.simpleTags.isNullOrEmpty()) {
|
||||
if (gi.simpleTags != null) {
|
||||
hasTags = true
|
||||
}
|
||||
if (gi.pages != 0) {
|
||||
@@ -530,7 +519,7 @@ object EhEngine {
|
||||
if (filter) list.removeAllSuspend { EhFilter.filterUploader(it) || EhFilter.filterTag(it) || EhFilter.filterTagNamespace(it) }
|
||||
}
|
||||
|
||||
suspend fun addFavorites(galleryList: List<Pair<Long, String>>, dstCat: Int) {
|
||||
galleryList.parMap(concurrency = Settings.multiThreadDownload) { (gid, token) -> modifyFavorites(gid, token, dstCat) }
|
||||
suspend fun modifyFavoritesRange(galleryList: List<Pair<Long, String>>, dstCat: Int) {
|
||||
galleryList.forEach { (gid, token) -> modifyFavorites(gid, token, dstCat) }
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -17,17 +17,17 @@ object EhFilter : CoroutineScope {
|
||||
val filters = async { EhDB.getAllFilter() as MutableList }
|
||||
private suspend inline fun anyActive(mode: FilterMode, predicate: (Filter) -> Boolean) = filters.await().any { it.mode == mode && it.enable && predicate(it) }
|
||||
private fun <R> Filter.launchOps(
|
||||
callback: (suspend (R) -> Unit)? = null,
|
||||
callback: ((R) -> Unit)? = null,
|
||||
ops: suspend Filter.() -> R,
|
||||
) = launch { ops().let { callback?.invoke(it) } }
|
||||
fun Filter.remember(callback: (suspend (Boolean) -> Unit)? = null) = launchOps(callback) {
|
||||
fun Filter.remember(callback: ((Boolean) -> Unit)? = null) = launchOps(callback) {
|
||||
EhDB.addFilter(this).also { if (it) filters.await().add(this) }
|
||||
}
|
||||
fun Filter.trigger(callback: (suspend (Unit) -> Unit)? = null) = launchOps(callback) {
|
||||
fun Filter.trigger(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
|
||||
enable = !enable
|
||||
EhDB.updateFilter(this)
|
||||
}
|
||||
fun Filter.forget(callback: (suspend (Unit) -> Unit)? = null) = launchOps(callback) {
|
||||
fun Filter.forget(callback: ((Unit) -> Unit)? = null) = launchOps(callback) {
|
||||
EhDB.deleteFilter(this)
|
||||
filters.await().remove(this)
|
||||
}
|
||||
+6
-8
@@ -19,11 +19,6 @@ import android.util.Log
|
||||
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
|
||||
import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient
|
||||
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
||||
import com.hippo.ehviewer.Settings
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
@@ -43,19 +38,22 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.executeAsync
|
||||
import okio.BufferedSource
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
inline fun ehRequest(url: String, referer: String? = null, origin: String? = null, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply {
|
||||
addHeader("User-Agent", Settings.userAgent)
|
||||
addHeader("User-Agent", CHROME_USER_AGENT)
|
||||
addHeader("Accept", CHROME_ACCEPT)
|
||||
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
|
||||
referer?.let { addHeader("Referer", it) }
|
||||
origin?.let { addHeader("Origin", it) }
|
||||
}.apply(builder).build()
|
||||
|
||||
inline fun formBody(builder: FormBody.Builder.() -> Unit) = FormBody.Builder().apply(builder).build()
|
||||
inline fun Request.Builder.formBody(builder: FormBody.Builder.() -> Unit) = post(FormBody.Builder().apply(builder).build())
|
||||
|
||||
inline fun multipartBody(builder: MultipartBody.Builder.() -> Unit) = MultipartBody.Builder().apply(builder).build()
|
||||
inline fun Request.Builder.multipartBody(builder: MultipartBody.Builder.() -> Unit) = post(MultipartBody.Builder().apply(builder).build())
|
||||
|
||||
val MEDIA_TYPE_JSON: MediaType = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
+27
-26
@@ -24,8 +24,6 @@ import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.copyToFile
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -44,6 +42,8 @@ import okio.blackholeSink
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
object EhTagDatabase : CoroutineScope {
|
||||
private const val NAMESPACE_PREFIX = "n"
|
||||
@@ -55,6 +55,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
)
|
||||
private lateinit var tagGroups: Map<String, Map<String, String>>
|
||||
private val updateLock = Mutex()
|
||||
private val dbLock = Mutex()
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
|
||||
var initialized by mutableStateOf(false)
|
||||
@@ -201,42 +202,42 @@ object EhTagDatabase : CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
private fun issueUpdateInMemoryData(file: File? = null) {
|
||||
val dataFile = file ?: getMetadata(appCtx)?.let { metadata ->
|
||||
val dataName = metadata[2]
|
||||
val dir = AppConfig.getFilesDir("tag-translations")
|
||||
File(dir, dataName).takeIf { it.exists() }
|
||||
} ?: return
|
||||
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
|
||||
initialized = true
|
||||
}
|
||||
|
||||
init {
|
||||
launch {
|
||||
updateLock.withLock {
|
||||
private suspend fun issueUpdateInMemoryData() {
|
||||
dbLock.withLock {
|
||||
getMetadata(appCtx)?.let { urls ->
|
||||
val dataName = urls[2]
|
||||
val dir = AppConfig.getFilesDir("tag-translations")
|
||||
val dataFile = File(dir, dataName).takeIf { it.exists() } ?: return
|
||||
runSuspendCatching {
|
||||
issueUpdateInMemoryData()
|
||||
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
|
||||
initialized = true
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
updateInternal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
launch {
|
||||
issueUpdateInMemoryData()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateInternal() {
|
||||
getMetadata(appCtx)?.let { metadata ->
|
||||
val sha1Name = metadata[0]
|
||||
val sha1Url = metadata[1]
|
||||
val dataName = metadata[2]
|
||||
val dataUrl = metadata[3]
|
||||
val urls = getMetadata(appCtx)
|
||||
urls?.let {
|
||||
val sha1Name = urls[0]
|
||||
val sha1Url = urls[1]
|
||||
val dataName = urls[2]
|
||||
val dataUrl = urls[3]
|
||||
|
||||
val dir = AppConfig.getFilesDir("tag-translations")
|
||||
checkNotNull(dir)
|
||||
val sha1File = File(dir, sha1Name)
|
||||
val dataFile = File(dir, dataName)
|
||||
|
||||
runSuspendCatching {
|
||||
runCatching {
|
||||
// Check current sha1 and current data
|
||||
val sha1 = getFileContent(sha1File)
|
||||
if (!checkData(sha1, dataFile)) {
|
||||
@@ -253,7 +254,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
if (tempSha1 == sha1) {
|
||||
// The data is the same
|
||||
FileUtils.delete(tempSha1File)
|
||||
return
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
// Save new data
|
||||
@@ -264,7 +265,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
if (!checkData(tempSha1, tempDataFile)) {
|
||||
FileUtils.delete(tempSha1File)
|
||||
FileUtils.delete(tempDataFile)
|
||||
return
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
// Replace current sha1 and current data with new sha1 and new data
|
||||
@@ -274,7 +275,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
tempDataFile.renameTo(dataFile)
|
||||
|
||||
// Read new EhTagDatabase
|
||||
issueUpdateInMemoryData(dataFile)
|
||||
issueUpdateInMemoryData()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
+6
-10
@@ -24,13 +24,13 @@ object EhUrl {
|
||||
const val DOMAIN_EX = "exhentai.org"
|
||||
const val DOMAIN_E = "e-hentai.org"
|
||||
const val HOST_EX = "https://$DOMAIN_EX/"
|
||||
const val API_EX = "https://s.exhentai.org/api.php"
|
||||
const val API_EX = HOST_EX + "api.php"
|
||||
const val FAV_PATH = "favorites.php"
|
||||
const val WATCHED_PATH = "watched"
|
||||
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
|
||||
const val URL_MY_TAGS_EX = HOST_EX + "mytags"
|
||||
const val HOST_E = "https://$DOMAIN_E/"
|
||||
const val API_E = "https://api.e-hentai.org/api.php"
|
||||
const val API_E = HOST_E + "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"
|
||||
@@ -64,14 +64,10 @@ object EhUrl {
|
||||
}
|
||||
|
||||
val apiUrl: String
|
||||
get() = if (Settings.forceEhThumb) {
|
||||
API_E
|
||||
} else {
|
||||
when (Settings.gallerySite) {
|
||||
SITE_E -> API_E
|
||||
SITE_EX -> API_EX
|
||||
else -> API_E
|
||||
}
|
||||
get() = when (Settings.gallerySite) {
|
||||
SITE_E -> API_E
|
||||
SITE_EX -> API_EX
|
||||
else -> API_E
|
||||
}
|
||||
|
||||
val referer: String
|
||||
+19
-17
@@ -16,13 +16,12 @@
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import arrow.core.memoize
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.google.android.material.R
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.color.utilities.Hct
|
||||
import com.google.android.material.color.utilities.MathUtils
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -103,19 +102,17 @@ object EhUtils {
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
val harmonizeWithRole = { primaryContainer: Int, src: Int ->
|
||||
fun harmonizeWithRole(context: Context, src: Int): Int {
|
||||
val primary = MaterialColors.getColor(context, R.attr.colorPrimaryContainer, "EhUtils")
|
||||
val fromHct = Hct.fromInt(src)
|
||||
val toHct = Hct.fromInt(primaryContainer)
|
||||
val toHct = Hct.fromInt(primary)
|
||||
val differenceDegrees = MathUtils.differenceDegrees(fromHct.hue, toHct.hue)
|
||||
val rotationDegrees = minOf(differenceDegrees * 0.5, 15.0)
|
||||
val outputHue = MathUtils.sanitizeDegreesDouble(fromHct.hue + rotationDegrees * MathUtils.rotationDirection(fromHct.hue, toHct.hue))
|
||||
Hct.from(outputHue, toHct.chroma, toHct.tone).toInt()
|
||||
}.memoize()
|
||||
return Hct.from(outputHue, toHct.chroma, toHct.tone).toInt()
|
||||
}
|
||||
|
||||
@Stable
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
fun getCategoryColor(category: Int): Color {
|
||||
private fun getCategoryColor(context: Context, category: Int): Int {
|
||||
val primary = when (category) {
|
||||
DOUJINSHI -> BG_COLOR_DOUJINSHI
|
||||
MANGA -> BG_COLOR_MANGA
|
||||
@@ -129,15 +126,16 @@ object EhUtils {
|
||||
MISC -> BG_COLOR_MISC
|
||||
else -> BG_COLOR_UNKNOWN
|
||||
}.toInt()
|
||||
val color = if (Settings.harmonizeCategoryColor) {
|
||||
val primaryContainer = MaterialTheme.colorScheme.primaryContainer.toArgb()
|
||||
harmonizeWithRole(primaryContainer, primary)
|
||||
return if (Settings.harmonizeCategoryColor) {
|
||||
harmonizeWithRole(context, primary)
|
||||
} else {
|
||||
primary
|
||||
}
|
||||
return Color(color)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getCategoryColor(category: Int) = Color(getCategoryColor(LocalContext.current, category))
|
||||
|
||||
val categoryTextColor = Color(0xffe6e0e9)
|
||||
|
||||
fun signOut() {
|
||||
@@ -148,6 +146,10 @@ object EhUtils {
|
||||
Settings.needSignIn = true
|
||||
}
|
||||
|
||||
fun needSignedIn(): Boolean {
|
||||
return Settings.needSignIn
|
||||
}
|
||||
|
||||
fun getSuitableTitle(gi: GalleryInfo): String {
|
||||
return if (Settings.showJpnTitle) {
|
||||
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
|
||||
File renamed without changes.
+1
-2
@@ -16,7 +16,6 @@
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
@@ -26,7 +25,7 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@Entity(tableName = "GALLERIES")
|
||||
data class BaseGalleryInfo @Keep constructor(
|
||||
open class BaseGalleryInfo(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "GID")
|
||||
override var gid: Long = 0,
|
||||
+9
-9
@@ -22,16 +22,16 @@ import com.hippo.ehviewer.client.ehUrl
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class FavListUrlBuilder(
|
||||
val favCat: Int = FAV_CAT_ALL,
|
||||
val keyword: String? = null,
|
||||
class FavListUrlBuilder(
|
||||
var mPrev: String? = null,
|
||||
var mNext: String? = null,
|
||||
var jumpTo: String? = null,
|
||||
var prev: String? = null,
|
||||
var next: String? = null,
|
||||
var keyword: String? = null,
|
||||
var favCat: Int = FAV_CAT_ALL,
|
||||
) : Parcelable {
|
||||
fun setIndex(index: String?, isNext: Boolean) {
|
||||
next = index.takeIf { isNext }
|
||||
prev = index.takeUnless { isNext }
|
||||
mNext = index.takeIf { isNext }
|
||||
mPrev = index.takeUnless { isNext }
|
||||
}
|
||||
|
||||
fun build() = ehUrl {
|
||||
@@ -42,8 +42,8 @@ data class FavListUrlBuilder(
|
||||
addQueryParameter("favcat", "all")
|
||||
}
|
||||
addQueryParameterIfNotBlank("f_search", keyword)
|
||||
addQueryParameterIfNotBlank("prev", prev)
|
||||
addQueryParameterIfNotBlank("next", next)
|
||||
addQueryParameterIfNotBlank("prev", mPrev)
|
||||
addQueryParameterIfNotBlank("next", mNext)
|
||||
addQueryParameterIfNotBlank("seek", jumpTo)
|
||||
}.toString()
|
||||
|
||||
+16
-14
@@ -19,18 +19,20 @@ import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class GalleryComment(
|
||||
val id: Long,
|
||||
val score: Int,
|
||||
val editable: Boolean,
|
||||
val voteUpAble: Boolean,
|
||||
val voteUpEd: Boolean,
|
||||
val voteDownAble: Boolean,
|
||||
val voteDownEd: Boolean,
|
||||
val uploader: Boolean,
|
||||
val voteState: String?,
|
||||
val time: Long,
|
||||
val user: String?,
|
||||
val comment: String,
|
||||
val lastEdited: Long,
|
||||
class GalleryApiInfo(
|
||||
var gid: Long = 0,
|
||||
var token: String? = null,
|
||||
var archiverKey: String? = null,
|
||||
var title: String? = null,
|
||||
var titleJpn: String? = null,
|
||||
var category: Int = 0,
|
||||
var thumb: String? = null,
|
||||
var uploader: String? = null,
|
||||
var posted: Long = 0,
|
||||
var filecount: Int = 0,
|
||||
var filesize: Long = 0,
|
||||
var expunged: Boolean = false,
|
||||
var rating: Float = 0f,
|
||||
var torrentcount: Int = 0,
|
||||
var tags: Array<String>?,
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class GalleryComment(
|
||||
// 0 for uploader comment. can't vote
|
||||
var id: Long = 0,
|
||||
var score: Int = 0,
|
||||
var editable: Boolean = false,
|
||||
var voteUpAble: Boolean = false,
|
||||
var voteUpEd: Boolean = false,
|
||||
var voteDownAble: Boolean = false,
|
||||
var voteDownEd: Boolean = false,
|
||||
var uploader: Boolean = false,
|
||||
var voteState: String? = null,
|
||||
var time: Long = 0,
|
||||
var user: String? = null,
|
||||
var comment: String? = null,
|
||||
var lastEdited: Long = 0,
|
||||
) : Parcelable
|
||||
+2
-2
@@ -19,7 +19,7 @@ import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class GalleryCommentList(
|
||||
val comments: List<GalleryComment>,
|
||||
class GalleryCommentList(
|
||||
var comments: Array<GalleryComment>,
|
||||
val hasMore: Boolean,
|
||||
) : Parcelable
|
||||
+2
-2
@@ -37,7 +37,7 @@ private val LANGUAGES = arrayOf(
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class GalleryDetail(
|
||||
class GalleryDetail(
|
||||
val galleryInfo: BaseGalleryInfo = BaseGalleryInfo(),
|
||||
var apiUid: Long = -1L,
|
||||
var apiKey: String? = null,
|
||||
@@ -51,7 +51,7 @@ data class GalleryDetail(
|
||||
var size: String? = null,
|
||||
var favoriteCount: Int = 0,
|
||||
var ratingCount: Int = 0,
|
||||
val tags: List<GalleryTagGroup>,
|
||||
val tags: Array<GalleryTagGroup>,
|
||||
var comments: GalleryCommentList,
|
||||
val previewPages: Int,
|
||||
val previewList: List<GalleryPreview>,
|
||||
+56
-51
@@ -16,7 +16,6 @@
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import java.util.regex.Pattern
|
||||
import moe.tarsin.kt.unreachable
|
||||
|
||||
interface GalleryInfo {
|
||||
var gid: Long
|
||||
@@ -44,29 +43,67 @@ interface GalleryInfo {
|
||||
?: title?.let { generateSLangFromTitle(it) }
|
||||
}
|
||||
|
||||
private fun generateSLangFromTags(simpleTags: List<String>): String? {
|
||||
for (tag in simpleTags) {
|
||||
for (i in S_LANGS.indices) {
|
||||
if (S_LANG_TAGS[i] == tag) {
|
||||
return S_LANGS[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun generateSLangFromTitle(title: String): String? {
|
||||
for (i in S_LANGS.indices) {
|
||||
if (S_LANG_PATTERNS[i].matcher(title).find()) {
|
||||
return S_LANGS[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* ISO 639-1
|
||||
*/
|
||||
const val S_LANG_JA = "JA"
|
||||
const val S_LANG_EN = "EN"
|
||||
const val S_LANG_ZH = "ZH"
|
||||
const val S_LANG_NL = "NL"
|
||||
const val S_LANG_FR = "FR"
|
||||
const val S_LANG_DE = "DE"
|
||||
const val S_LANG_HU = "HU"
|
||||
const val S_LANG_IT = "IT"
|
||||
const val S_LANG_KO = "KO"
|
||||
const val S_LANG_PL = "PL"
|
||||
const val S_LANG_PT = "PT"
|
||||
const val S_LANG_RU = "RU"
|
||||
const val S_LANG_ES = "ES"
|
||||
const val S_LANG_TH = "TH"
|
||||
const val S_LANG_VI = "VI"
|
||||
|
||||
val S_LANGS = arrayOf(
|
||||
"EN",
|
||||
"ZH",
|
||||
"ES",
|
||||
"KO",
|
||||
"RU",
|
||||
"FR",
|
||||
"PT",
|
||||
"TH",
|
||||
"DE",
|
||||
"IT",
|
||||
"VI",
|
||||
"PL",
|
||||
"HU",
|
||||
"NL",
|
||||
S_LANG_EN,
|
||||
S_LANG_ZH,
|
||||
S_LANG_ES,
|
||||
S_LANG_KO,
|
||||
S_LANG_RU,
|
||||
S_LANG_FR,
|
||||
S_LANG_PT,
|
||||
S_LANG_TH,
|
||||
S_LANG_DE,
|
||||
S_LANG_IT,
|
||||
S_LANG_VI,
|
||||
S_LANG_PL,
|
||||
S_LANG_HU,
|
||||
S_LANG_NL,
|
||||
)
|
||||
private val S_LANG_PATTERNS = arrayOf(
|
||||
val S_LANG_PATTERNS = arrayOf(
|
||||
Pattern.compile(
|
||||
"[(\\[]eng(?:lish)?[)\\]]|英訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
),
|
||||
// [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
|
||||
), // [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
|
||||
Pattern.compile(
|
||||
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
@@ -93,7 +130,7 @@ interface GalleryInfo {
|
||||
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
|
||||
)
|
||||
private val S_LANG_TAGS = arrayOf(
|
||||
val S_LANG_TAGS = arrayOf(
|
||||
"language:english",
|
||||
"language:chinese",
|
||||
"language:spanish",
|
||||
@@ -110,39 +147,7 @@ interface GalleryInfo {
|
||||
"language:dutch",
|
||||
)
|
||||
|
||||
private fun generateSLangFromTags(simpleTags: List<String>): String? {
|
||||
for (tag in simpleTags) {
|
||||
for (i in S_LANGS.indices) {
|
||||
if (S_LANG_TAGS[i] == tag) {
|
||||
return S_LANGS[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun generateSLangFromTitle(title: String): String? {
|
||||
for (i in S_LANGS.indices) {
|
||||
if (S_LANG_PATTERNS[i].matcher(title).find()) {
|
||||
return S_LANGS[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const val NOT_FAVORITED = -2
|
||||
const val LOCAL_FAVORITED = -1
|
||||
}
|
||||
}
|
||||
|
||||
fun GalleryInfo.findBaseInfo(): BaseGalleryInfo {
|
||||
return when (this) {
|
||||
is BaseGalleryInfo -> this
|
||||
is GalleryDetail -> galleryInfo
|
||||
else -> unreachable()
|
||||
}
|
||||
}
|
||||
|
||||
fun GalleryInfo.asGalleryDetail(): GalleryDetail? {
|
||||
return this as? GalleryDetail
|
||||
}
|
||||
File renamed without changes.
+3
-3
@@ -19,7 +19,7 @@ import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class GalleryTagGroup(
|
||||
class GalleryTagGroup(
|
||||
val groupName: String,
|
||||
private val tagList: MutableList<String> = mutableListOf(),
|
||||
) : Parcelable, MutableList<String> by tagList
|
||||
private val mTagList: MutableList<String> = mutableListOf(),
|
||||
) : Parcelable, MutableList<String> by mTagList
|
||||
+63
-44
@@ -25,9 +25,9 @@ import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.ui.main.AdvanceTable
|
||||
import com.hippo.ehviewer.util.encodeUTF8
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class ListUrlBuilder(
|
||||
@@ -35,7 +35,6 @@ data class ListUrlBuilder(
|
||||
var mode: Int = MODE_NORMAL,
|
||||
private var mPrev: String? = null,
|
||||
var mNext: String? = null,
|
||||
// Reset to null after initial loading
|
||||
var mJumpTo: String? = null,
|
||||
var category: Int = EhUtils.NONE,
|
||||
private var mKeyword: String? = null,
|
||||
@@ -49,6 +48,23 @@ data class ListUrlBuilder(
|
||||
var isOnlySearchCovers: Boolean = false,
|
||||
) : Parcelable {
|
||||
|
||||
fun reset() {
|
||||
mode = MODE_NORMAL
|
||||
mPrev = null
|
||||
mNext = null
|
||||
mJumpTo = null
|
||||
this.category = EhUtils.NONE
|
||||
mKeyword = null
|
||||
advanceSearch = -1
|
||||
minRating = -1
|
||||
pageFrom = -1
|
||||
pageTo = -1
|
||||
imagePath = null
|
||||
isUseSimilarityScan = false
|
||||
isOnlySearchCovers = false
|
||||
hash = null
|
||||
}
|
||||
|
||||
fun setIndex(index: String?, isNext: Boolean = true) {
|
||||
mNext = index?.takeIf { isNext }
|
||||
mPrev = index?.takeUnless { isNext }
|
||||
@@ -64,7 +80,8 @@ data class ListUrlBuilder(
|
||||
mKeyword = keyword
|
||||
}
|
||||
|
||||
constructor(q: QuickSearch) : this() {
|
||||
fun set(q: QuickSearch) {
|
||||
reset()
|
||||
mode = q.mode
|
||||
this.category = q.category
|
||||
mKeyword = q.keyword
|
||||
@@ -117,8 +134,9 @@ data class ListUrlBuilder(
|
||||
/**
|
||||
* @param query xxx=yyy&mmm=nnn
|
||||
*/
|
||||
constructor(query: String?) : this() {
|
||||
// TODO page
|
||||
// TODO page
|
||||
fun setQuery(query: String?) {
|
||||
reset()
|
||||
if (query.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
@@ -302,65 +320,66 @@ data class ListUrlBuilder(
|
||||
// Pages
|
||||
if (pageFrom != -1 || pageTo != -1) {
|
||||
addQueryParameter("f_sp", "on")
|
||||
addQueryParameterIfNotBlank(
|
||||
addQueryParameter(
|
||||
"f_spf",
|
||||
pageFrom.takeUnless { it == -1 }?.toString(),
|
||||
(if (pageFrom != -1) pageFrom.toString() else "").toString(),
|
||||
)
|
||||
addQueryParameterIfNotBlank(
|
||||
addQueryParameter(
|
||||
"f_spt",
|
||||
pageTo.takeUnless { it == -1 }?.toString(),
|
||||
(if (pageTo != -1) pageTo.toString() else "").toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
|
||||
MODE_UPLOADER -> {
|
||||
buildString {
|
||||
append(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
append("uploader/").append(encodeUTF8(it))
|
||||
}
|
||||
mPrev?.let {
|
||||
append("?prev=").append(it)
|
||||
}
|
||||
mNext?.let {
|
||||
append("?next=").append(it)
|
||||
}
|
||||
mJumpTo?.let {
|
||||
append("&seek=").append(it)
|
||||
}
|
||||
val sb = StringBuilder(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
sb.append("uploader/")
|
||||
sb.append(encodeUTF8(it))
|
||||
}
|
||||
mPrev?.let {
|
||||
sb.append("?prev=").append(it)
|
||||
}
|
||||
mNext?.let {
|
||||
sb.append("?next=").append(it)
|
||||
}
|
||||
mJumpTo?.let {
|
||||
sb.append("&seek=").append(it)
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
MODE_TAG -> {
|
||||
buildString {
|
||||
append(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
append("tag/").append(encodeUTF8(it))
|
||||
}
|
||||
mPrev?.let {
|
||||
append("?prev=").append(it)
|
||||
}
|
||||
mNext?.let {
|
||||
append("?next=").append(it)
|
||||
}
|
||||
mJumpTo?.let {
|
||||
append("&seek=").append(it)
|
||||
}
|
||||
val sb = StringBuilder(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
sb.append("tag/")
|
||||
sb.append(encodeUTF8(it))
|
||||
}
|
||||
mPrev?.let {
|
||||
sb.append("?prev=").append(it)
|
||||
}
|
||||
mNext?.let {
|
||||
sb.append("?next=").append(it)
|
||||
}
|
||||
mJumpTo?.let {
|
||||
sb.append("&seek=").append(it)
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
|
||||
MODE_TOPLIST -> {
|
||||
buildString {
|
||||
append(EhUrl.HOST_E)
|
||||
append("toplist.php?tl=")
|
||||
append(mKeyword!!)
|
||||
mJumpTo?.let {
|
||||
append("&p=").append(it)
|
||||
}
|
||||
val sb = StringBuilder(EhUrl.HOST_E)
|
||||
sb.append("toplist.php?tl=")
|
||||
mKeyword.orEmpty().let {
|
||||
sb.append(encodeUTF8(it))
|
||||
}
|
||||
mJumpTo?.let {
|
||||
sb.append("&p=").append(it)
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+10
-11
@@ -1,19 +1,18 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.NotLoggedInException
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import splitties.init.appCtx
|
||||
|
||||
object FavoritesParser {
|
||||
suspend fun parse(body: ByteBuffer): Result {
|
||||
val catArray = arrayOfNulls<String>(10)
|
||||
val countArray = runSuspendCatching {
|
||||
parseFav(body, favCat = catArray).also { check(it.isNotEmpty()) }
|
||||
}.getOrElse {
|
||||
if (it is RuntimeException && it.message == "Not logged in!") throw NotLoggedInException()
|
||||
throw ParseException("Parse favorites error", it)
|
||||
suspend fun parse(body: String): Result {
|
||||
if (body.contains("This page requires you to log on.</p>")) {
|
||||
throw EhException(appCtx.getString(R.string.need_sign_in))
|
||||
}
|
||||
val catArray = arrayOfNulls<String>(10)
|
||||
val countArray = parseFav(body, catArray)
|
||||
if (countArray.isEmpty()) throw ParseException("Parse favorites error")
|
||||
val result = GalleryListParser.parse(body)
|
||||
return Result(catArray.requireNoNulls(), countArray, result)
|
||||
}
|
||||
@@ -29,4 +28,4 @@ object FavoritesParser {
|
||||
}
|
||||
}
|
||||
|
||||
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit(), favCat: Array<String?>): IntArray
|
||||
private external fun parseFav(body: String, favCat: Array<String?>): IntArray
|
||||
File renamed without changes.
+2
-3
@@ -33,7 +33,7 @@ object GalleryApiParser {
|
||||
titleJpn = item.titleJpn.unescapeXml()
|
||||
category = getCategory(item.category)
|
||||
thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!)
|
||||
uploader = item.uploader?.unescapeXml()
|
||||
uploader = item.uploader.unescapeXml()
|
||||
posted = ParserUtils.formatDate(item.posted * 1000)
|
||||
rating = item.rating
|
||||
simpleTags = item.tags
|
||||
@@ -54,8 +54,7 @@ object GalleryApiParser {
|
||||
val titleJpn: String,
|
||||
val category: String,
|
||||
val thumb: String,
|
||||
// Null in some old galleries
|
||||
val uploader: String?,
|
||||
val uploader: String,
|
||||
val posted: Long,
|
||||
@SerialName("filecount")
|
||||
val pages: Int,
|
||||
+47
-40
@@ -17,7 +17,6 @@ package com.hippo.ehviewer.client.parser
|
||||
|
||||
import arrow.core.unzip
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhFilter
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.EhUtils.getCategory
|
||||
@@ -42,10 +41,6 @@ import com.hippo.ehviewer.util.toFloatOrDefault
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import com.hippo.ehviewer.util.trimAnd
|
||||
import com.hippo.ehviewer.util.unescapeXml
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@@ -53,6 +48,10 @@ import org.jsoup.nodes.Node
|
||||
import org.jsoup.select.Elements
|
||||
import org.jsoup.select.NodeTraversor
|
||||
import org.jsoup.select.NodeVisitor
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
object GalleryDetailParser {
|
||||
private val PATTERN_ERROR = Regex("<div class=\"d\">\n<p>([^<]+)</p>")
|
||||
@@ -76,7 +75,8 @@ object GalleryDetailParser {
|
||||
private val PATTERN_NEWER_DATE = Regex(", added (.+?)<br />")
|
||||
private val PATTERN_FAVORITE_SLOT =
|
||||
Regex("/fav.png\\); background-position:0px -(\\d+)px")
|
||||
private val EMPTY_GALLERY_COMMENT_LIST = GalleryCommentList(emptyList(), false)
|
||||
private val EMPTY_GALLERY_TAG_GROUP_ARRAY = arrayOf<GalleryTagGroup>()
|
||||
private val EMPTY_GALLERY_COMMENT_ARRAY = GalleryCommentList(arrayOf(), false)
|
||||
private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
|
||||
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC)
|
||||
private const val OFFENSIVE_STRING =
|
||||
@@ -307,7 +307,7 @@ object GalleryDetailParser {
|
||||
/**
|
||||
* Parse tag groups with html parser
|
||||
*/
|
||||
private fun parseTagGroups(document: Document): List<GalleryTagGroup> {
|
||||
private fun parseTagGroups(document: Document): Array<GalleryTagGroup> {
|
||||
return try {
|
||||
val taglist = document.getElementById("taglist")!!
|
||||
val tagGroups = taglist.child(0).child(0).children()
|
||||
@@ -315,69 +315,73 @@ object GalleryDetailParser {
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
EMPTY_GALLERY_TAG_GROUP_ARRAY
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTagGroups(trs: Elements): List<GalleryTagGroup> {
|
||||
private fun parseTagGroups(trs: Elements): Array<GalleryTagGroup> {
|
||||
return try {
|
||||
trs.mapNotNull { parseTagGroup(it) }
|
||||
trs.mapNotNull { parseTagGroup(it) }.toTypedArray()
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
EMPTY_GALLERY_TAG_GROUP_ARRAY
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parseComment(element: Element): GalleryComment? {
|
||||
return try {
|
||||
val comment = GalleryComment()
|
||||
// Id
|
||||
val a = element.previousElementSibling()
|
||||
val name = a!!.attr("name")
|
||||
val id = name trimAnd { substring(1).toInt().toLong() }
|
||||
comment.id = name trimAnd { substring(1).toInt().toLong() }
|
||||
// Editable, vote up and vote down
|
||||
val c4 = element.getElementsByClass("c4").first()
|
||||
var voteUpAble = false
|
||||
var voteUpEd = false
|
||||
var voteDownAble = false
|
||||
var voteDownEd = false
|
||||
var editable = false
|
||||
var uploader = false
|
||||
if (null != c4) {
|
||||
if ("Uploader Comment" == c4.text()) {
|
||||
uploader = true
|
||||
comment.uploader = true
|
||||
}
|
||||
for (e in c4.children()) {
|
||||
when (e.text()) {
|
||||
"Vote+" -> {
|
||||
voteUpAble = true
|
||||
voteUpEd = e.attr("style").trim().isNotEmpty()
|
||||
comment.voteUpAble = true
|
||||
comment.voteUpEd = e.attr("style").trim().isNotEmpty()
|
||||
}
|
||||
|
||||
"Vote-" -> {
|
||||
voteDownAble = true
|
||||
voteDownEd = e.attr("style").trim().isNotEmpty()
|
||||
comment.voteDownAble = true
|
||||
comment.voteDownEd = e.attr("style").trim().isNotEmpty()
|
||||
}
|
||||
|
||||
"Edit" -> editable = true
|
||||
"Edit" -> comment.editable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Vote state
|
||||
val c7 = element.getElementsByClass("c7").first()
|
||||
val voteState = c7?.text()?.trim()
|
||||
if (null != c7) {
|
||||
comment.voteState = c7.text().trim()
|
||||
}
|
||||
// Score
|
||||
val c5 = element.getElementsByClass("c5").first()
|
||||
val score = c5?.children()?.first()?.text()?.trim()?.toIntOrDefault(0) ?: 0
|
||||
if (null != c5) {
|
||||
val es = c5.children()
|
||||
if (!es.isEmpty()) {
|
||||
comment.score = es[0].text().trim().toIntOrDefault(0)
|
||||
}
|
||||
}
|
||||
// time
|
||||
val c3 = element.getElementsByClass("c3").first()
|
||||
val temp = c3!!.ownText()
|
||||
val (timeStr, user) = if (temp.endsWith(':')) {
|
||||
temp.substring("Posted on ".length, temp.length - " by:".length) to c3.child(0).text()
|
||||
val time = if (temp.endsWith(':')) {
|
||||
// user
|
||||
comment.user = c3.child(0).text()
|
||||
temp.substring("Posted on ".length, temp.length - " by:".length)
|
||||
} else {
|
||||
temp.substring("Posted on ".length) to null
|
||||
temp.substring("Posted on ".length)
|
||||
}
|
||||
val time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(timeStr)).toEpochMilli()
|
||||
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).toEpochMilli()
|
||||
// comment
|
||||
val c6 = element.getElementsByClass("c6").first()
|
||||
// fix underline support
|
||||
@@ -386,21 +390,24 @@ object GalleryDetailParser {
|
||||
e.tagName("u")
|
||||
}
|
||||
}
|
||||
val commentHtml = c6.html()
|
||||
comment.comment = c6.html()
|
||||
// filter comment
|
||||
if (!uploader) {
|
||||
if (!comment.uploader) {
|
||||
val sEhFilter = EhFilter
|
||||
if (score <= Settings.commentThreshold || sEhFilter.filterCommenter(user.orEmpty()) || sEhFilter.filterComment(commentHtml)) {
|
||||
if (sEhFilter.filterCommenter(comment.user!!) || sEhFilter.filterComment(comment.comment!!)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// last edited
|
||||
val c8 = element.getElementsByClass("c8").first()
|
||||
val lastEdited = c8?.children()?.first()?.run { Instant.from(WEB_COMMENT_DATE_FORMAT.parse(text())).toEpochMilli() } ?: 0
|
||||
GalleryComment(
|
||||
id, score, editable, voteUpAble, voteUpEd, voteDownAble,
|
||||
voteDownEd, uploader, voteState, time, user, commentHtml, lastEdited,
|
||||
)
|
||||
if (c8 != null) {
|
||||
val e = c8.children().first()
|
||||
if (e != null) {
|
||||
comment.lastEdited =
|
||||
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(e.text())).toEpochMilli()
|
||||
}
|
||||
}
|
||||
comment
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
@@ -430,11 +437,11 @@ object GalleryDetailParser {
|
||||
},
|
||||
chd!!,
|
||||
)
|
||||
GalleryCommentList(list, hasMore)
|
||||
GalleryCommentList(list.toTypedArray(), hasMore)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
EMPTY_GALLERY_COMMENT_LIST
|
||||
EMPTY_GALLERY_COMMENT_ARRAY
|
||||
}
|
||||
}
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,38 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
|
||||
object GalleryListParser {
|
||||
suspend fun parse(body: String) = runCatching {
|
||||
parseGalleryInfoList(body).apply {
|
||||
galleryInfoList.onEach {
|
||||
if (it.favoriteSlot == NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
|
||||
it.favoriteSlot = LOCAL_FAVORITED
|
||||
}
|
||||
it.generateSLang()
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
if (body.contains("<p>You do not have any watched tags")) {
|
||||
throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
}
|
||||
if (body.contains("No hits found</p>")) {
|
||||
throw EhException(R.string.gallery_list_empty_hit)
|
||||
}
|
||||
throw ParseException("Can't parse gallery list", it)
|
||||
}
|
||||
}
|
||||
|
||||
class GalleryListResult(
|
||||
val prev: String?,
|
||||
val next: String?,
|
||||
val galleryInfoList: ArrayList<BaseGalleryInfo>,
|
||||
)
|
||||
|
||||
private external fun parseGalleryInfoList(e: String): GalleryListResult
|
||||
+7
-2
@@ -42,7 +42,9 @@ object GalleryListUrlParser {
|
||||
}
|
||||
val path = url.path ?: return null
|
||||
return if (PATH_NORMAL == path || path.isEmpty()) {
|
||||
ListUrlBuilder(url.query)
|
||||
val builder = ListUrlBuilder()
|
||||
builder.setQuery(url.query)
|
||||
builder
|
||||
} else if (path.startsWith(PATH_UPLOADER)) {
|
||||
parseUploader(path)
|
||||
} else if (path.startsWith(PATH_TAG)) {
|
||||
@@ -55,7 +57,10 @@ object GalleryListUrlParser {
|
||||
} catch (e: NumberFormatException) {
|
||||
return null
|
||||
}
|
||||
ListUrlBuilder(url.query).also { it.category = category }
|
||||
val builder = ListUrlBuilder()
|
||||
builder.setQuery(url.query)
|
||||
builder.category = category
|
||||
builder
|
||||
} else {
|
||||
null
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.parseAs
|
||||
import com.hippo.ehviewer.util.unescapeXml
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object GalleryPageApiParser {
|
||||
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
|
||||
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
|
||||
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
|
||||
|
||||
fun parse(body: String): Result {
|
||||
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error) }
|
||||
val res = body.parseAs<JsonResult>()
|
||||
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
|
||||
groupValues[1].trim().unescapeXml()
|
||||
}
|
||||
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKey)?.run {
|
||||
groupValues[1].trim().unescapeXml()
|
||||
}
|
||||
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.originImageUrl)?.run {
|
||||
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
|
||||
}
|
||||
if (!imageUrl.isNullOrEmpty()) {
|
||||
return Result(imageUrl, skipHathKey, originImageUrl)
|
||||
} else {
|
||||
throw ParseException("Parse image url and skip hath key error")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class JsonResult(
|
||||
@SerialName("i3")
|
||||
val imageUrl: String,
|
||||
@SerialName("i6")
|
||||
val skipHathKey: String,
|
||||
@SerialName("i7")
|
||||
val originImageUrl: String,
|
||||
)
|
||||
|
||||
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.ParseException
|
||||
import com.hippo.ehviewer.util.unescapeXml
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object GalleryPageParser {
|
||||
private val PATTERN_IMAGE_URL = Pattern.compile("<img[^>]*src=\"([^\"]+)\" style")
|
||||
private val PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^)]+)'\\)")
|
||||
private val PATTERN_ORIGIN_IMAGE_URL =
|
||||
Pattern.compile("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
|
||||
|
||||
// TODO Not sure about the size of show keys
|
||||
private val PATTERN_SHOW_KEY = Pattern.compile("var showkey=\"([0-9a-z]+)\";")
|
||||
|
||||
fun parse(body: String): Result {
|
||||
var m = PATTERN_IMAGE_URL.matcher(body)
|
||||
val imageUrl = if (m.find()) {
|
||||
m.group(1)!!.trim().unescapeXml()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
m = PATTERN_SKIP_HATH_KEY.matcher(body)
|
||||
val skipHathKey = if (m.find()) {
|
||||
m.group(1)!!.trim().unescapeXml()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
m = PATTERN_ORIGIN_IMAGE_URL.matcher(body)
|
||||
val originImageUrl = if (m.find()) {
|
||||
m.group(1)!!.unescapeXml() + "fullimg.php" + m.group(2)!!.unescapeXml()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
m = PATTERN_SHOW_KEY.matcher(body)
|
||||
val showKey = if (m.find()) {
|
||||
m.group(1)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {
|
||||
Result(imageUrl, skipHathKey, originImageUrl, showKey)
|
||||
} else {
|
||||
throw ParseException("Parse image url and show error")
|
||||
}
|
||||
}
|
||||
|
||||
class Result(
|
||||
val imageUrl: String,
|
||||
val skipHathKey: String?,
|
||||
val originImageUrl: String?,
|
||||
val showKey: String,
|
||||
)
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+3
-6
@@ -1,19 +1,16 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object HomeParser {
|
||||
private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
|
||||
private const val INSUFFICIENT_FUNDS = "Insufficient funds."
|
||||
|
||||
fun parse(body: ByteBuffer) = runCatching {
|
||||
parseLimit(body)
|
||||
}.getOrElse { throw ParseException("Parse image limits error", it) }
|
||||
fun parse(body: ByteBuffer) = parseLimit(body)
|
||||
|
||||
fun parseResetLimits(body: String) {
|
||||
if (body.contains(INSUFFICIENT_FUNDS)) {
|
||||
@@ -36,6 +33,6 @@ object HomeParser {
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Limits @Keep constructor(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
||||
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
||||
|
||||
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Limits
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-3
@@ -1,6 +1,5 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@@ -10,7 +9,7 @@ object TorrentParser {
|
||||
}.getOrElse { throw ParseException("Can't parse torrent list", it) }
|
||||
}
|
||||
|
||||
class Torrent @Keep constructor(
|
||||
class Torrent(
|
||||
val posted: String,
|
||||
val size: String,
|
||||
val seeds: Int,
|
||||
@@ -22,6 +21,6 @@ class Torrent @Keep constructor(
|
||||
|
||||
fun Torrent.format() = "[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloads]"
|
||||
|
||||
typealias TorrentResult = List<Torrent>
|
||||
typealias TorrentResult = ArrayList<Torrent>
|
||||
|
||||
private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): ArrayList<Torrent>
|
||||
File renamed without changes.
File renamed without changes.
+1
-2
@@ -15,7 +15,6 @@ import coil.request.Options
|
||||
import com.hippo.ehviewer.cronet.copyToChannel
|
||||
import com.hippo.ehviewer.cronet.cronetRequest
|
||||
import com.hippo.ehviewer.cronet.execute
|
||||
import com.hippo.ehviewer.cronet.noCache
|
||||
import java.io.RandomAccessFile
|
||||
|
||||
/**
|
||||
@@ -28,7 +27,7 @@ class CronetHttpUriFetcher(private val data: String, private val options: Option
|
||||
val diskCache = requireNotNull(imageLoader.diskCache)
|
||||
val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run {
|
||||
val success = cronetRequest(data) {
|
||||
noCache()
|
||||
disableCache()
|
||||
}.execute {
|
||||
diskCache.suspendEdit(diskCacheKey) {
|
||||
RandomAccessFile(data.toFile(), "rw").use {
|
||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -3,8 +3,6 @@ package com.hippo.ehviewer.coil
|
||||
import coil.intercept.Interceptor
|
||||
import coil.request.ImageResult
|
||||
import com.hippo.ehviewer.Settings
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -13,6 +11,8 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
object MergeInterceptor : Interceptor {
|
||||
private val pendingContinuationMap: HashMap<String, MutableList<CancellableContinuation<Unit>>> = hashMapOf()
|
||||
+48
-37
@@ -1,38 +1,44 @@
|
||||
package com.hippo.ehviewer.cronet
|
||||
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
||||
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
||||
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import org.chromium.net.CronetEngine
|
||||
import org.chromium.net.CronetException
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
import splitties.init.appCtx
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.RequestBody
|
||||
import okio.Buffer
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import org.chromium.net.CronetEngine
|
||||
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 splitties.init.appCtx
|
||||
|
||||
private const val TAG = "CronetRequest"
|
||||
val pool = DirectByteBufferPool(32)
|
||||
|
||||
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
|
||||
enableBrotli(true)
|
||||
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
|
||||
setStoragePath(cache.absolutePath)
|
||||
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
|
||||
setUserAgent(Settings.userAgent)
|
||||
setUserAgent(CHROME_USER_AGENT)
|
||||
}.build()
|
||||
|
||||
val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService
|
||||
|
||||
// TODO: Rewrite this to use android.net.http.HttpEngine and make it Android 14 only when released
|
||||
class CronetRequest {
|
||||
lateinit var consumer: (ByteBuffer) -> Unit
|
||||
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
|
||||
@@ -41,6 +47,7 @@ class CronetRequest {
|
||||
lateinit var readerCont: Continuation<Unit>
|
||||
val callback = object : UrlRequest.Callback() {
|
||||
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
|
||||
logcat(tag = TAG) { "Redirected to $url" }
|
||||
req.followRedirect()
|
||||
}
|
||||
|
||||
@@ -49,6 +56,7 @@ class CronetRequest {
|
||||
}
|
||||
|
||||
override fun onReadCompleted(req: UrlRequest, info: UrlResponseInfo, data: ByteBuffer) {
|
||||
data.flip()
|
||||
consumer(data)
|
||||
}
|
||||
|
||||
@@ -62,16 +70,38 @@ class CronetRequest {
|
||||
}
|
||||
}
|
||||
|
||||
inline fun cronetRequest(url: String, referer: String? = null, origin: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
|
||||
inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
|
||||
request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply {
|
||||
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
|
||||
addHeader("Accept", CHROME_ACCEPT)
|
||||
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
|
||||
referer?.let { addHeader("Referer", it) }
|
||||
origin?.let { addHeader("Origin", it) }
|
||||
}.apply(conf).build()
|
||||
}
|
||||
|
||||
suspend inline fun CronetRequest.awaitBodyFully(crossinline callback: (ByteBuffer) -> Unit) {
|
||||
val buffer = pool.borrow()
|
||||
return try {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
consumer = {
|
||||
callback(it)
|
||||
buffer.clear()
|
||||
request.read(buffer)
|
||||
}
|
||||
onError = { readerCont.resumeWithException(it) }
|
||||
readerCont = cont
|
||||
request.read(buffer)
|
||||
}
|
||||
} finally {
|
||||
pool.recycle(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun CronetRequest.copyToChannel(chan: FileChannel, crossinline listener: ((Int) -> Unit) = {}) = awaitBodyFully {
|
||||
val bytes = chan.write(it)
|
||||
listener(bytes)
|
||||
}
|
||||
|
||||
suspend inline fun <R> CronetRequest.execute(crossinline callback: suspend CronetRequest.(UrlResponseInfo) -> R): R {
|
||||
contract {
|
||||
callsInPlace(callback, InvocationKind.EXACTLY_ONCE)
|
||||
@@ -85,22 +115,3 @@ suspend inline fun <R> CronetRequest.execute(crossinline callback: suspend Crone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun UrlRequest.Builder.withRequestBody(body: RequestBody) {
|
||||
addHeader("Content-Type", body.contentType().toString())
|
||||
val buffer = Buffer().apply { body.writeTo(this) }
|
||||
val provider = object : UploadDataProvider() {
|
||||
override fun getLength() = body.contentLength()
|
||||
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
|
||||
buffer.read(byteBuffer)
|
||||
uploadDataSink.onReadSucceeded(false)
|
||||
}
|
||||
override fun rewind(uploadDataSink: UploadDataSink) {
|
||||
error("OneShot!")
|
||||
}
|
||||
}
|
||||
setUploadDataProvider(provider, cronetHttpClientExecutor)
|
||||
}
|
||||
|
||||
fun UrlRequest.Builder.noCache(): UrlRequest.Builder = disableCache()
|
||||
fun UrlResponseInfo.getHeadersMap(): Map<String, List<String>> = allHeaders
|
||||
+5
-6
@@ -24,12 +24,11 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import splitties.init.appCtx
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.TimeUnit
|
||||
import splitties.init.appCtx
|
||||
|
||||
private val signedIn
|
||||
get() = EhCookieStore.hasSignedIn()
|
||||
@@ -67,21 +66,21 @@ private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
|
||||
.build()
|
||||
return PeriodicWorkRequestBuilder<DailyCheckWork>(Duration.ofDays(1))
|
||||
.setConstraints(constraints)
|
||||
.setInitialDelay(initialDelay.seconds, TimeUnit.SECONDS)
|
||||
.setInitialDelay(initialDelay)
|
||||
.build()
|
||||
}
|
||||
|
||||
private const val WORK_NAME = "DailyCheckWork"
|
||||
private const val workName = "DailyCheckWork"
|
||||
|
||||
fun updateDailyCheckWork(context: Context) {
|
||||
if (Settings.requestNews) {
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
WORK_NAME,
|
||||
workName,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
getDailyCheckWorkRequest(),
|
||||
)
|
||||
} else {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
|
||||
WorkManager.getInstance(context).cancelUniqueWork(workName)
|
||||
}
|
||||
}
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 527 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user