Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3cb6d2782 | ||
|
|
45be957df5 | ||
|
|
8e45014e3f | ||
|
|
d84e948352 | ||
|
|
68bd835038 | ||
|
|
c297bebea1 | ||
|
|
6c0a823216 | ||
|
|
067a701bdc | ||
|
|
c3fb008c92 | ||
|
|
bc4321da2e | ||
|
|
8dec040803 | ||
|
|
29e4e77c44 | ||
|
|
11429ef7fe | ||
|
|
f5fcd96a1e | ||
|
|
19120038c5 | ||
|
|
9021f28ead | ||
|
|
62934f72e3 | ||
|
|
6c534e1049 | ||
|
|
f80cd90792 | ||
|
|
3999c0c114 | ||
|
|
59399453c4 | ||
|
|
694bb19e2a | ||
|
|
9928ecd4d9 | ||
|
|
2c5293af6e | ||
|
|
9569ca774d | ||
|
|
3a04460926 | ||
|
|
acb4fb6166 | ||
|
|
7d137bb016 | ||
|
|
8c2ed37b72 | ||
|
|
749b230250 | ||
|
|
23531cb6e0 | ||
|
|
0ac11dbe0b | ||
|
|
9b0e017542 | ||
|
|
f2f5fcacf9 | ||
|
|
52ded265f3 | ||
|
|
e77f905c4a | ||
|
|
2d14d34d53 | ||
|
|
78facda18a | ||
|
|
1fe1b43015 | ||
|
|
63164dc70a | ||
|
|
0d39092a7f | ||
|
|
c50db8a327 | ||
|
|
7e107cc5e2 | ||
|
|
260949e3a7 | ||
|
|
d9ec108721 | ||
|
|
1e97407936 | ||
|
|
73afe05a18 | ||
|
|
23475de932 | ||
|
|
6fb2172c2c | ||
|
|
9226b71119 | ||
|
|
58e38b58bf | ||
|
|
db0d110627 | ||
|
|
08812bf7ff | ||
|
|
daf7f8ffca | ||
|
|
74564fa07f | ||
|
|
fc51d3fc81 | ||
|
|
c5d5b8abd2 | ||
|
|
193badccb8 | ||
|
|
2653d0568a | ||
|
|
adf5a65371 | ||
|
|
5b59dabc66 | ||
|
|
f7259c5b40 | ||
|
|
6f9fceaef8 | ||
|
|
6a64ff51ed | ||
|
|
ea9d9cfba7 | ||
|
|
75c6561896 | ||
|
|
8f8db82e9d | ||
|
|
a114a8ac72 | ||
|
|
2c6216057f | ||
|
|
c5cbcf7418 | ||
|
|
5746e3bb57 | ||
|
|
fd3c87be22 | ||
|
|
866fff3e45 | ||
|
|
06df7befb6 | ||
|
|
8021f86473 | ||
|
|
c9919a8b39 | ||
|
|
386015a9ec | ||
|
|
cbce98b34e | ||
|
|
f69521fc1a | ||
|
|
01b4054368 | ||
|
|
e07aab4434 | ||
|
|
18caa87512 | ||
|
|
5676de24da | ||
|
|
57bbb1fe38 | ||
|
|
dcbfbc2e5f | ||
|
|
e1f473d301 | ||
|
|
a33256d29f | ||
|
|
6a81877b09 | ||
|
|
6cd7b85b94 | ||
|
|
df97a9c4b9 | ||
|
|
5cb91e9b45 | ||
|
|
94794cb0db | ||
|
|
a16c021000 | ||
|
|
49d4ae078f | ||
|
|
eb45068df3 | ||
|
|
1aa750d676 | ||
|
|
af85a68e3d | ||
|
|
a8fc4003a3 | ||
|
|
675df44745 | ||
|
|
7e271d7e8e | ||
|
|
acb2c720af | ||
|
|
5d0c2e3f05 | ||
|
|
3d5e3620dd | ||
|
|
43aefeedae | ||
|
|
c2cbcc009d | ||
|
|
2aa494d9b7 | ||
|
|
8f3a11f374 | ||
|
|
76298f15e4 | ||
|
|
58839e0def | ||
|
|
6fe62db5e4 | ||
|
|
9b9f25d405 | ||
|
|
e47d7378ec | ||
|
|
51b385408d | ||
|
|
8e7cced8d2 | ||
|
|
affeb3e106 | ||
|
|
a2377857e9 | ||
|
|
7714ff8938 | ||
|
|
352853b88c | ||
|
|
23d282e354 | ||
|
|
b93c12de89 | ||
|
|
e0e5a155aa | ||
|
|
e1f34a7b1b | ||
|
|
298e80eb9d | ||
|
|
3ecfd83f1e | ||
|
|
1dcf4e7f46 | ||
|
|
f1a856cb45 | ||
|
|
af32d7eea7 | ||
|
|
47be2754e5 | ||
|
|
39b16b6650 | ||
|
|
0a809a564c | ||
|
|
07e24013c8 | ||
|
|
8e8394985d | ||
|
|
e54b33aaaa | ||
|
|
e6a22d8ef4 | ||
|
|
ae25018574 | ||
|
|
28160608ec | ||
|
|
38651eba1d | ||
|
|
cc53fa5f1c | ||
|
|
70f8181752 | ||
|
|
9e506f6258 | ||
|
|
1304c728cf | ||
|
|
80e11c54e7 | ||
|
|
ff5d58b101 | ||
|
|
bf550d99bc | ||
|
|
7738b7bc29 | ||
|
|
01f51b1000 | ||
|
|
16dfacb285 | ||
|
|
a253ddf7e4 | ||
|
|
ffb488482c | ||
|
|
0e29754ce9 | ||
|
|
ee97720e1f | ||
|
|
29d4e6370c | ||
|
|
9040cb54d4 | ||
|
|
f082766f06 | ||
|
|
e2372891cb | ||
|
|
9dc5b5c539 | ||
|
|
886e025ac8 | ||
|
|
feb13cbd79 | ||
|
|
66baaad86f | ||
|
|
4142f28822 | ||
|
|
c578b16e27 | ||
|
|
32887acecd | ||
|
|
5cf5cc5e34 | ||
|
|
4dc0f7e02e | ||
|
|
6e9c6e6269 | ||
|
|
a22880a9b1 | ||
|
|
7357724392 | ||
|
|
fb1b08e7e8 | ||
|
|
1405b99ab8 | ||
|
|
f7aec69dae | ||
|
|
f15a0fc4bb | ||
|
|
6330fc3bdc | ||
|
|
01e3f3e9b7 | ||
|
|
e0c614cf71 | ||
|
|
b34764d4c2 | ||
|
|
f46f84e533 | ||
|
|
ca0eaf2053 | ||
|
|
25f283ee9c | ||
|
|
8ee23ab520 | ||
|
|
6964baf3f6 | ||
|
|
01aa7af175 | ||
|
|
5982bd2d4b | ||
|
|
d0fbb3539f | ||
|
|
284fb4242b | ||
|
|
beeb4bdbd4 | ||
|
|
76a52903e5 | ||
|
|
a19b8782a8 | ||
|
|
31f66fd05b | ||
|
|
bc3c4e969c | ||
|
|
4231402e54 | ||
|
|
ff4f76dfa5 | ||
|
|
2ff9df4c07 | ||
|
|
7cbc641178 | ||
|
|
08d5621b59 | ||
|
|
d39c2fdfa2 | ||
|
|
27e02ad642 | ||
|
|
11e9faef7c | ||
|
|
cbb8971dd1 | ||
|
|
1077f4d8cf | ||
|
|
594f9412e3 | ||
|
|
7674f6a0f7 | ||
|
|
aea6fbf4ab | ||
|
|
962f9bfff5 | ||
|
|
60532e2622 | ||
|
|
5c7a834342 | ||
|
|
ca940080a9 | ||
|
|
2273dd541a | ||
|
|
422062226d | ||
|
|
6a6da6f059 | ||
|
|
4cbb0eca4f | ||
|
|
ebb3eb1ab3 | ||
|
|
ebf645acaf | ||
|
|
ec787ebde2 | ||
|
|
4f190cb0ef | ||
|
|
2a36868d11 | ||
|
|
176d8be565 | ||
|
|
7b581e45e1 | ||
|
|
4888e29f79 | ||
|
|
e5f6d1b1aa | ||
|
|
d544bbfbeb | ||
|
|
ed20d6022b | ||
|
|
4191e2091c | ||
|
|
1eeb5a51f6 | ||
|
|
dd0776758f | ||
|
|
cf3ccf6155 | ||
|
|
fa6e4acad6 | ||
|
|
db33a72a8e | ||
|
|
0f8a0549b1 | ||
|
|
26ff30af6c | ||
|
|
c2fa32f430 | ||
|
|
2734de0588 | ||
|
|
aa219e4b71 | ||
|
|
645656b4cd | ||
|
|
ef16bdeb75 | ||
|
|
84299b4db7 | ||
|
|
953e216dde | ||
|
|
c57fd0a923 | ||
|
|
f9cb3d769f | ||
|
|
dd62571943 | ||
|
|
9edd33d88d | ||
|
|
b2fded76d5 | ||
|
|
1761493c8e | ||
|
|
6aa6ee2810 | ||
|
|
84728a1490 | ||
|
|
a1568467f0 | ||
|
|
32e2a03c31 | ||
|
|
d4609593e2 | ||
|
|
a2af7437c0 | ||
|
|
ab5af22fc3 | ||
|
|
39a159e5ca | ||
|
|
ec8a54906c | ||
|
|
73c0ce02ea | ||
|
|
7d2e0f7f7e | ||
|
|
7515a049c2 | ||
|
|
a317d5303e | ||
|
|
c4045ded70 | ||
|
|
3cb1b1eee4 | ||
|
|
5423af46c5 | ||
|
|
a2883840e0 | ||
|
|
14810b4c2c | ||
|
|
f5a9a41790 | ||
|
|
500d00d214 | ||
|
|
49810f8fd1 | ||
|
|
6ad3ab228f | ||
|
|
3a29ce291b | ||
|
|
1e28a4c7ab | ||
|
|
645467d20f | ||
|
|
b2a5fe9f52 | ||
|
|
2ea3c03360 | ||
|
|
0c147967d2 | ||
|
|
de1fab2595 | ||
|
|
b86eebf0a7 | ||
|
|
07345f90e6 | ||
|
|
5fe520e575 | ||
|
|
a9dd705ec5 | ||
|
|
e36520faad | ||
|
|
edaf0a4275 | ||
|
|
7735237ea7 | ||
|
|
7bbc4ea618 | ||
|
|
db99392082 | ||
|
|
7bf6a9f545 | ||
|
|
51c0efebc4 | ||
|
|
7ef70aa16e | ||
|
|
c5c0c6c4bd | ||
|
|
b26cba8ea9 | ||
|
|
90b4c16166 | ||
|
|
9b0f6fe433 | ||
|
|
e24735c341 | ||
|
|
9680ada9ee | ||
|
|
028a7c2352 | ||
|
|
80e5f36c4e | ||
|
|
e0b65184e2 | ||
|
|
395a48ddeb | ||
|
|
e22a39f549 | ||
|
|
e17d934ebf | ||
|
|
10746417b6 | ||
|
|
3ef45ba0cc | ||
|
|
a3e7b6ef43 | ||
|
|
c444a3b435 | ||
|
|
5161802465 | ||
|
|
8c27feec7a | ||
|
|
37e895cdca | ||
|
|
e1ad4e3b90 | ||
|
|
5f0be5ae43 | ||
|
|
9a7cabb221 | ||
|
|
6acc552058 | ||
|
|
b4f4d442eb | ||
|
|
21016dda53 | ||
|
|
576e5b1b58 | ||
|
|
17437a6882 | ||
|
|
04f9f087da | ||
|
|
c508947728 | ||
|
|
ce1dc55d3c | ||
|
|
e9f615819f | ||
|
|
8da3546558 | ||
|
|
d8f8dcffbd | ||
|
|
e79113b19e | ||
|
|
2a7f65a1bd | ||
|
|
e322878b95 | ||
|
|
11b09f187c | ||
|
|
926309733c | ||
|
|
c4b2bcab66 | ||
|
|
b88c96894d | ||
|
|
8502671203 | ||
|
|
4a7d6f7489 | ||
|
|
aec34c81aa | ||
|
|
1d4fe30e04 | ||
|
|
a2cfecd922 | ||
|
|
d36aef3214 | ||
|
|
c632231813 | ||
|
|
d8dd68c9b6 | ||
|
|
d6a58d5d34 | ||
|
|
41f8168c28 | ||
|
|
4513ead30a | ||
|
|
f4e23a5738 | ||
|
|
79b167002c | ||
|
|
a9a763ae5f | ||
|
|
a5fc29bc66 | ||
|
|
02c568fd7a | ||
|
|
ce55891bc2 | ||
|
|
87b1160c72 | ||
|
|
ccc6d004de | ||
|
|
20b145bee5 | ||
|
|
a37cc59a56 | ||
|
|
7cbffff12c | ||
|
|
4e5ae53775 | ||
|
|
a53de63695 | ||
|
|
8020c139ec | ||
|
|
3c5375a9a6 | ||
|
|
1d9008a1a6 | ||
|
|
fb577dc026 | ||
|
|
675eb1b7e2 | ||
|
|
6a3d522dfa | ||
|
|
47aa5c64e7 | ||
|
|
5ecf5c41ca | ||
|
|
bc6d12a873 | ||
|
|
4d983830df | ||
|
|
2f1e0d030e | ||
|
|
b7c873a172 | ||
|
|
9d5eb55290 | ||
|
|
78ffb1c4fe | ||
|
|
031d622d5d | ||
|
|
34da120a43 | ||
|
|
5285e1ccf2 | ||
|
|
e28efc1f50 | ||
|
|
1c043a81a6 | ||
|
|
9a769cc7b6 | ||
|
|
045c6abe40 | ||
|
|
4ad5cef412 | ||
|
|
c3ffab3c79 | ||
|
|
c82de0b779 | ||
|
|
4e36cbabc0 | ||
|
|
94f87b14b7 | ||
|
|
6bc80d1afa | ||
|
|
fe4d004083 | ||
|
|
28410befa3 | ||
|
|
515f7deb7e | ||
|
|
800e66261e | ||
|
|
65658c4a02 | ||
|
|
0488c7ebf4 | ||
|
|
c30c1b29ef | ||
|
|
876001ad99 | ||
|
|
60dc59adbc | ||
|
|
c994890f28 | ||
|
|
76223a2cd2 | ||
|
|
6274b39b68 | ||
|
|
cca3a3e350 | ||
|
|
86ef8f2b48 | ||
|
|
5d116d9477 | ||
|
|
6e73fcc8c9 | ||
|
|
3bd12a3047 | ||
|
|
c99d8e8275 | ||
|
|
4a12e71877 | ||
|
|
3043901b5a | ||
|
|
7cc28955d6 | ||
|
|
ef44bb4857 | ||
|
|
37660ef697 | ||
|
|
9821b3aaab | ||
|
|
1f5331af5a | ||
|
|
3ea6c82a6d | ||
|
|
ba53d01fa1 | ||
|
|
df8ae8d11a | ||
|
|
c817726ab3 | ||
|
|
9eb606810f | ||
|
|
fbb030ec8e | ||
|
|
28afebefb1 | ||
|
|
da2610657c | ||
|
|
6e3471ba1b | ||
|
|
5a9ab6c4eb | ||
|
|
1fe553d039 | ||
|
|
42e0b0aeab | ||
|
|
9e0ea32296 | ||
|
|
d3f5274b7e | ||
|
|
faff0733e9 | ||
|
|
7b32cde7dd | ||
|
|
648d1d10b3 | ||
|
|
52727100a0 | ||
|
|
8a2dbb99f0 | ||
|
|
d880b30ea9 | ||
|
|
0e2ca4e150 | ||
|
|
e0eef82ed8 | ||
|
|
09839086a2 | ||
|
|
3ca3ab7273 | ||
|
|
8386bf9c69 | ||
|
|
99b8ee1f78 | ||
|
|
0333a8c679 | ||
|
|
e0bb09e902 | ||
|
|
77677cead3 | ||
|
|
d4b2ff7de9 | ||
|
|
b81bae09d1 | ||
|
|
846e66a56c | ||
|
|
5cec635e52 | ||
|
|
8d0dc88125 | ||
|
|
51b58bb797 | ||
|
|
f79aa2350c | ||
|
|
719dddeab9 | ||
|
|
c1522568fd | ||
|
|
3ad6b9106b | ||
|
|
549387ac91 | ||
|
|
7d2f985ffa | ||
|
|
7e8b0a5800 | ||
|
|
69252ae8a4 | ||
|
|
eeed56b211 | ||
|
|
8aaad071ea | ||
|
|
7ef67e1bf9 | ||
|
|
1c15d9a956 |
No files matched your search
@@ -141,9 +141,6 @@ body:
|
||||
请确认您已经遵守所有必选项
|
||||
Please ensure you have obtained all needed options
|
||||
options:
|
||||
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
|
||||
required: false
|
||||
|
||||
- label: 您已搜索过 [Issue Tracker](https://github.com/FooIbar/EhViewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/FooIbar/EhViewer/issues), No duplicate or related open issue has been found
|
||||
required: true
|
||||
|
||||
|
||||
+118
-137
@@ -8,20 +8,17 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java 17
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
@@ -30,6 +27,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./app/src/main/rust"
|
||||
shared-key: ${{ runner.os }}
|
||||
|
||||
- name: Rustfmt Check
|
||||
working-directory: ./app/src/main/rust
|
||||
@@ -40,174 +38,157 @@ jobs:
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Spotless Check
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
default:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./app/src/main/rust"
|
||||
shared-key: ${{ runner.os }}
|
||||
save-if: "false"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
app/.cxx
|
||||
app/src/main/cpp/nettle/nettle
|
||||
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: ${{ runner.os }}-cmake-
|
||||
path: app/.cxx
|
||||
key: cmake-default-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-default-
|
||||
|
||||
- name: Build OSS
|
||||
run: ./gradlew assembleDefaultOssRelease
|
||||
- name: Build
|
||||
run: ./gradlew assembleDefaultRelease
|
||||
|
||||
- name: Upload OSS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload ARM64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk
|
||||
name: default-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload OSS x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload x86_64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk
|
||||
name: default-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
|
||||
|
||||
- name: Upload OSS ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload ARM32
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk
|
||||
name: default-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload OSS x86
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload x86
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk
|
||||
name: default-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-x86-release.apk
|
||||
|
||||
- name: Upload OSS mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultOssRelease/mapping.txt
|
||||
name: default-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultRelease/mapping.txt
|
||||
|
||||
- name: Upload OSS native debug symbols
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip
|
||||
name: default-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
|
||||
|
||||
- name: Build GMS
|
||||
run: ./gradlew assembleDefaultGmsRelease
|
||||
marshmallow:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Upload GMS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
name: default-gms-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Upload GMS x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
name: default-gms-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk
|
||||
workspaces: "./app/src/main/rust"
|
||||
shared-key: ${{ runner.os }}
|
||||
save-if: "false"
|
||||
|
||||
- name: Upload GMS ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
name: default-gms-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: Upload GMS x86
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: CMake Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
name: default-gms-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk
|
||||
path: app/.cxx
|
||||
key: cmake-marshmallow-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-marshmallow-
|
||||
|
||||
- name: Upload GMS mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Build
|
||||
run: ./gradlew assembleMarshmallowRelease
|
||||
|
||||
- name: Upload ARM64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultGmsRelease/mapping.txt
|
||||
name: marshmallow-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload GMS native debug symbols
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload x86_64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip
|
||||
name: marshmallow-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk
|
||||
|
||||
- name: Build Marshmallow OSS
|
||||
run: ./gradlew assembleMarshmallowOssRelease
|
||||
|
||||
- name: Upload Marshmallow OSS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload ARM32
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk
|
||||
name: marshmallow-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload x86
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk
|
||||
name: marshmallow-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk
|
||||
name: marshmallow-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowRelease/mapping.txt
|
||||
|
||||
- name: Upload Marshmallow OSS x86
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk
|
||||
|
||||
- name: 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: marshmallow-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
|
||||
@@ -18,5 +18,4 @@ jobs:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
issue-inactive-days: '1'
|
||||
include-any-issue-labels: 'duplicate, wontfix'
|
||||
process-only: 'issues'
|
||||
@@ -6,20 +6,17 @@ on:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java 17
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
|
||||
@@ -30,86 +27,52 @@ jobs:
|
||||
workspaces: "./app/src/main/rust"
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
app/.cxx
|
||||
app/src/main/cpp/nettle/nettle
|
||||
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: ${{ runner.os }}-cmake-
|
||||
path: app/.cxx
|
||||
key: cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
arguments: assembleRelease
|
||||
|
||||
- name: Rename Apks
|
||||
run: |
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-universal-release.apk EhViewer-${{ github.ref_name }}-default-oss-universal.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/defaultOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
|
||||
mv app/build/outputs/apk/default/release/app-default-universal-release.apk EhViewer-${{ github.ref_name }}-default-universal.apk
|
||||
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-x86_64.apk
|
||||
mv app/build/outputs/apk/default/release/app-default-x86-release.apk EhViewer-${{ github.ref_name }}-default-x86.apk
|
||||
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/defaultRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
||||
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-universal-release.apk EhViewer-${{ github.ref_name }}-default-gms-universal.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/defaultGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
|
||||
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/marshmallowOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
|
||||
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/marshmallowGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-x86.apk
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/marshmallowRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
||||
|
||||
- name: Releases
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body: Bump Version
|
||||
files: |
|
||||
EhViewer-${{ github.ref_name }}-default-oss-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
|
||||
EhViewer-${{ github.ref_name }}-default-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-default-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
||||
|
||||
EhViewer-${{ github.ref_name }}-default-gms-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
|
||||
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
|
||||
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
||||
+1
-2
@@ -2,8 +2,7 @@
|
||||
/local.properties
|
||||
.DS_Store
|
||||
/build
|
||||
/src
|
||||
/.kotlin
|
||||
*.iml
|
||||
.idea
|
||||
/captures
|
||||
google-services.json
|
||||
+154
-5
@@ -1,6 +1,155 @@
|
||||
# 变更日志
|
||||
|
||||
## 未发布
|
||||
## [1.11.3] - 2024-04-06
|
||||
|
||||
### 改进
|
||||
|
||||
* 优化预加载逻辑和图片加载顺序
|
||||
* 优化画廊详情和评论页面 UI
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 某些模式下搜索结果不显示画廊语言 [#923](https://github.com/FooIbar/EhViewer/issues/923)
|
||||
* 某些情况下在搜索结果中直接阅读画廊后崩溃 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
||||
* 更新至最新版本后仍提示有新版本 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
||||
* 从旧版本升级时包含不兼容数据导致崩溃/无法导出数据 [#940](https://github.com/FooIbar/EhViewer/issues/940) [#974](https://github.com/FooIbar/EhViewer/issues/974)
|
||||
* Android 10 以下的设备打开 设置-隐私 时崩溃 [#953](https://github.com/FooIbar/EhViewer/issues/953)
|
||||
* 下载标签无法拖动
|
||||
* 刷新 和 刷新(原图) 无法在已下载的图片上使用
|
||||
* 临时 IP 封禁错误显示为解析失败
|
||||
* 某些情况下下载时闪退 [#980](https://github.com/FooIbar/EhViewer/issues/980)
|
||||
* 某些设备上下载列表中的缩略图无法显示 [#977](https://github.com/FooIbar/EhViewer/issues/977)
|
||||
|
||||
|
||||
## [1.11.2] - 2024-03-17
|
||||
|
||||
### 重大变更
|
||||
|
||||
* Default 变种最低支持版本更改为 Android 8.0,重新添加了最低支持 Android 6.0 的 Marshmallow 变种
|
||||
|
||||
### 改进
|
||||
|
||||
* 重新添加了删除快速搜索/下载标签时的确认对话框
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 收藏页面 搜索栏中文标签转英文异常 [#902](https://github.com/FooIbar/EhViewer/issues/902)
|
||||
* 在收藏或者取消收藏之后,收藏按钮会无反应 [#908](https://github.com/FooIbar/EhViewer/issues/908)
|
||||
* 横屏页面下阅读设置UI重合 [#909](https://github.com/FooIbar/EhViewer/issues/909)
|
||||
* Search bar does not work properly in Japanese [#906](https://github.com/FooIbar/EhViewer/issues/906)
|
||||
* 搜索结果中的 Disowned 画廊不显示上传者
|
||||
* 使用 CI 频道更新时,有时会获取到错误的更新链接
|
||||
|
||||
|
||||
## [1.11.1] - 2024-03-12
|
||||
|
||||
### 重大变更
|
||||
|
||||
* 由于 E 站强制要求人机验证,仅支持通过网页登录
|
||||
* 默认支持 Android 6,不再提供 Marshmallow 变种
|
||||
* 集成 Cronet 库,不再提供 GMS 变种
|
||||
|
||||
### 改进
|
||||
|
||||
* 支持在排序下载列表前按下载标签分组
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 下载画廊时崩溃 [#893](https://github.com/FooIbar/EhViewer/issues/893)
|
||||
|
||||
|
||||
## [1.11.0] - 2024-03-09
|
||||
|
||||
### 重大变更
|
||||
|
||||
* 由于 E 站 Cookie 策略变更,不再支持 Cookie 登录
|
||||
* 登录时自动选择 exhentai (如果可用)
|
||||
* 不再提供默认下载路径
|
||||
* 手动排序更改为以添加时间/上传时间/标题/下载标签/页数对下载列表排序
|
||||
* 由于数据库结构变更,此版本导出的数据将无法被旧版本导入
|
||||
|
||||
### 新功能
|
||||
|
||||
* 下载画廊时生成 ComicInfo.xml 格式元数据
|
||||
* 归档下载时生成 ComicInfo.xml 格式元数据 [#711](https://github.com/FooIbar/EhViewer/issues/711)
|
||||
* 归档下载支持第三方下载管理器 [#79](https://github.com/FooIbar/EhViewer/issues/79)
|
||||
* 保存下载结果为 CBZ 压缩包 [#660](https://github.com/FooIbar/EhViewer/issues/660)
|
||||
* 清除 `igneous` Cookie (将于下次搜索时重新获取)
|
||||
* 随机打开本地收藏/下载中的画廊 [#695](https://github.com/FooIbar/EhViewer/issues/695)
|
||||
* 随机在搜索结果中跳转
|
||||
* 以网格模式查看下载列表
|
||||
* 本地收藏/历史/下载中的标签搜索 [#860](https://github.com/FooIbar/EhViewer/issues/860)
|
||||
|
||||
### 改进
|
||||
|
||||
* 搜索时优先显示已选择的类别卡片
|
||||
* 多处性能优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 显示模式为 Thumbnail 时无法点击上传者卡片 [#653](https://github.com/FooIbar/EhViewer/issues/653)
|
||||
* [Marshmallow] GIF 只显示第一帧
|
||||
* 无法在 `定时请求新闻页面` 关闭时开启 `隐藏 HV 事件通知`
|
||||
* 响应正文含有无效 UTF-8 字符导致解码失败 [#643](https://github.com/FooIbar/EhViewer/issues/643)
|
||||
* 某些情况下应用崩溃 [#710](https://github.com/FooIbar/EhViewer/issues/710)
|
||||
* 画廊名称包含非法路径字符时无法保存图片 [#815](https://github.com/FooIbar/EhViewer/issues/815)
|
||||
* 无法解码超长图片 [#732](https://github.com/FooIbar/EhViewer/issues/732)
|
||||
* E 站变更导致种子解析失败 [#880](https://github.com/FooIbar/EhViewer/issues/880)
|
||||
* E 站不再支持相似扫描导致图片搜索无法使用
|
||||
* 杂项修复
|
||||
|
||||
### 已知问题
|
||||
|
||||
* Android 14 上使用手势导航返回时过渡动画过早结束 [#775](https://github.com/FooIbar/EhViewer/issues/775)
|
||||
|
||||
|
||||
## [1.10.3] - 2024-01-15
|
||||
|
||||
### 新功能
|
||||
|
||||
* [Marshmallow] 支持裁剪边缘、打开压缩包和 GIF
|
||||
|
||||
### 改进
|
||||
|
||||
* 由于部分设备上的非预期行为 [#578](https://github.com/FooIbar/EhViewer/issues/578) [#596](https://github.com/FooIbar/EhViewer/issues/596),默认禁用预见式返回导航动画,可于 设置-高级 中开启
|
||||
* 杂项优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 刷新后页面无响应 [#579](https://github.com/FooIbar/EhViewer/issues/579)
|
||||
* 浏览中保存图片后点击操作被阻挡 [#581](https://github.com/FooIbar/EhViewer/issues/581)
|
||||
* 在画廊详情页面切换至后台后点击评论闪退 [#580](https://github.com/FooIbar/EhViewer/issues/580)
|
||||
* 已下载的画廊中未阅读缓存过的图片解码失败 [#592](https://github.com/FooIbar/EhViewer/issues/592)
|
||||
* 在收藏页多选并下载会使得下载状态出现问题 [#603](https://github.com/FooIbar/EhViewer/issues/603)
|
||||
* 未能在不重启情况下切换 Http 引擎 [#610](https://github.com/FooIbar/EhViewer/issues/610)
|
||||
* 在有进度的画廊阅读界面无法直接回到第一页 [#614](https://github.com/FooIbar/EhViewer/issues/614)
|
||||
* 请求新闻界面失效 [#605](https://github.com/FooIbar/EhViewer/issues/605)
|
||||
* 收藏夹为空解析失败 [#630](https://github.com/FooIbar/EhViewer/issues/630)
|
||||
|
||||
|
||||
## [1.10.2] - 2023-12-31
|
||||
|
||||
### 新功能
|
||||
|
||||
* [阅读器] 支持裁剪图片黑色/白色边缘
|
||||
|
||||
### 改进
|
||||
|
||||
* 切换 HTTP 引擎无需重启
|
||||
* 优化过渡动画
|
||||
* 杂项性能优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 修复搜索建议中出现关键词相同的条目时崩溃 [#533](https://github.com/FooIbar/EhViewer/issues/533) [#555](https://github.com/FooIbar/EhViewer/issues/555)
|
||||
* 修复重命名下载标签时不显示原标签 [#534](https://github.com/FooIbar/EhViewer/issues/534)
|
||||
* 修复调整下载项目顺序时卡顿 [#552](https://github.com/FooIbar/EhViewer/issues/552)
|
||||
* 修复新增或修改评论后黑屏 [#554](https://github.com/FooIbar/EhViewer/issues/554)
|
||||
* 修复剪贴板读取bug [#566](https://github.com/FooIbar/EhViewer/issues/566)
|
||||
* 修复画廊信息对话框无法通过返回按钮/手势关闭 [#413](https://github.com/FooIbar/EhViewer/issues/413)
|
||||
|
||||
|
||||
## [1.10.1] - 2023-12-19
|
||||
|
||||
### 重大变更
|
||||
|
||||
@@ -13,7 +162,7 @@
|
||||
* 修复 Android 9 以下的设备上无法退出搜索栏 [#513](https://github.com/FooIbar/EhViewer/issues/513)
|
||||
|
||||
|
||||
## 1.10.0
|
||||
## [1.10.0] - 2023-12-14 [YANKED]
|
||||
|
||||
### 重大变更
|
||||
|
||||
@@ -37,7 +186,7 @@
|
||||
* 修复 CI 版本不显示变更日志
|
||||
|
||||
|
||||
## 1.9.0
|
||||
## [1.9.0] - 2023-12-01
|
||||
|
||||
### 重大变更
|
||||
|
||||
@@ -51,7 +200,7 @@
|
||||
|
||||
### 改进
|
||||
|
||||
* \[阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
|
||||
* [阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
|
||||
* 优化原图下载重试策略
|
||||
* 优化小屏设备上的搜索类别布局
|
||||
* 优化过渡动画
|
||||
@@ -69,7 +218,7 @@
|
||||
* 修复手速过快页面切换过快使加载圈圈不自动消失 [#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)
|
||||
* [Marshmallow] 修复小内存设备上图片解码失败 [#229](https://github.com/FooIbar/EhViewer/issues/229) [#372](https://github.com/FooIbar/EhViewer/issues/372)
|
||||
* 修复已舍弃画廊上传者解析
|
||||
|
||||
### 已知问题
|
||||
|
||||
@@ -66,16 +66,10 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
|
||||
|
||||
# Download
|
||||
|
||||
| Flavor | Feature |
|
||||
|-------------|-----------------------------------|
|
||||
| Default | Android 9+, full support |
|
||||
| Marshmallow | Android 6.0-8.1, limited support* |
|
||||
| OSS | FOSS dependencies** |
|
||||
| GMS | Proprietary GMS component |
|
||||
|
||||
\* 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)
|
||||
| Flavor | Feature |
|
||||
|-------------|----------------------------------|
|
||||
| Default | Android 8.0+, full support |
|
||||
| Marshmallow | Android 6.0-7.1, limited support |
|
||||
|
||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
||||
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
|
||||
@@ -93,9 +87,7 @@ 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/)
|
||||
- [Ktor](https://ktor.io/)
|
||||
- [Coil](https://coil-kt.github.io/coil/)
|
||||
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
|
||||
@@ -109,7 +101,7 @@ Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementati
|
||||
Copyright 2014-2019 Hippo Seven
|
||||
Copyright 2020-2022 NekoInverter
|
||||
Copyright 2022-2023 Tarsin Norbin
|
||||
Copyright 2023 Foolbar
|
||||
Copyright 2023-2024 Foolbar
|
||||
|
||||
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
/build
|
||||
/src/main/java-gen
|
||||
manifest-merger-release-report.txt
|
||||
/src/main/libs
|
||||
/src/main/obj
|
||||
# Intellij
|
||||
*.iml
|
||||
/.cxx
|
||||
+43
-66
@@ -15,14 +15,12 @@ plugins {
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.spotless)
|
||||
alias(libs.plugins.aboutlibrariesPlugin)
|
||||
alias(libs.plugins.rustAndroidPlugin)
|
||||
alias(libs.plugins.composeCompilerReportGenerator)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
buildToolsVersion = "34.0.0"
|
||||
ndkVersion = "26.1.10909125"
|
||||
ndkVersion = "26.2.11394342"
|
||||
|
||||
splits {
|
||||
abi {
|
||||
@@ -62,10 +60,10 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 180048
|
||||
versionName = "1.10.1"
|
||||
versionCode = 180054
|
||||
versionName = "1.11.3"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
@@ -82,6 +80,7 @@ android {
|
||||
"nb-rNO",
|
||||
),
|
||||
)
|
||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||
ndk {
|
||||
@@ -89,32 +88,14 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += listOf("api", "oss")
|
||||
flavorDimensions += "api"
|
||||
|
||||
productFlavors {
|
||||
create("default") {
|
||||
dimension = "api"
|
||||
}
|
||||
create("default")
|
||||
create("marshmallow") {
|
||||
dimension = "api"
|
||||
minSdk = 23
|
||||
applicationIdSuffix = ".m"
|
||||
versionNameSuffix = "-M"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
targets += "noop"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
}
|
||||
create("oss") {
|
||||
dimension = "oss"
|
||||
}
|
||||
create("gms") {
|
||||
dimension = "oss"
|
||||
versionNameSuffix = "-gms"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,12 +105,17 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive",
|
||||
"-Xjvm-default=all",
|
||||
"-Xlambdas=indy",
|
||||
"-Xcontext-receivers",
|
||||
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
@@ -147,20 +133,18 @@ android {
|
||||
"-opt-in=splitties.preferences.DataStorePreferencesPreview",
|
||||
|
||||
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
|
||||
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:nonSkippingGroupOptimization=true",
|
||||
)
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += setOf("MissingTranslation", "MissingQuantity")
|
||||
fatal += setOf("NewApi", "InlineApi")
|
||||
fatal += setOf("NewApi", "InlinedApi")
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/**"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
dex {
|
||||
useLegacyPackaging = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +177,17 @@ android {
|
||||
namespace = "com.hippo.ehviewer"
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().withBuildType("release")) {
|
||||
it.packaging.resources.excludes.addAll(
|
||||
"/META-INF/**",
|
||||
"/kotlin/**",
|
||||
"**.txt",
|
||||
"**.bin",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@@ -214,10 +209,9 @@ dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||
implementation(libs.bundles.androidx.navigation)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||
implementation(libs.androidx.paging.compose)
|
||||
@@ -231,19 +225,19 @@ dependencies {
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.photoview) // Dead Dependency
|
||||
implementation(libs.directionalviewpager) // Dead Dependency
|
||||
implementation(libs.material)
|
||||
implementation(libs.material.motion.core)
|
||||
|
||||
implementation(libs.bundles.splitties)
|
||||
|
||||
// https://square.github.io/okhttp/changelogs/changelog/
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.bundles.okhttp)
|
||||
|
||||
implementation(libs.okio.jvm)
|
||||
|
||||
implementation(libs.logcat)
|
||||
|
||||
implementation(libs.diff)
|
||||
|
||||
implementation(libs.aboutlibraries.core)
|
||||
implementation(libs.aboutlibraries.compose.m3)
|
||||
implementation(libs.accompanist.drawable.painter)
|
||||
|
||||
implementation(libs.insetter) // Dead Dependency
|
||||
|
||||
@@ -256,7 +250,9 @@ dependencies {
|
||||
implementation(platform(libs.coil.bom))
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(libs.telephoto.zoomable)
|
||||
|
||||
implementation(libs.ktor.client.core)
|
||||
|
||||
implementation(libs.bundles.kotlinx.serialization)
|
||||
|
||||
@@ -266,22 +262,20 @@ dependencies {
|
||||
|
||||
coreLibraryDesugaring(libs.desugar)
|
||||
|
||||
"gmsImplementation"(libs.bundles.cronet)
|
||||
}
|
||||
implementation(libs.cronet.embedded)
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
exclude("androidx.navigation", "navigation-compose")
|
||||
}
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.generateKotlin", "true")
|
||||
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
@@ -289,30 +283,13 @@ aboutLibraries {
|
||||
duplicationRule = GROUP
|
||||
}
|
||||
|
||||
cargo {
|
||||
module = "src/main/rust"
|
||||
libname = "ehviewer_rust"
|
||||
targets = if (isRelease) listOf("arm", "x86", "arm64", "x86_64") else listOf("arm64", "x86_64")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.configureEach {
|
||||
if (name.startsWith("merge") && name.endsWith("JniLibFolders")) {
|
||||
dependsOn("cargoBuild")
|
||||
// fix mergeDebugJniLibFolders UP-TO-DATE
|
||||
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
|
||||
ktlint()
|
||||
}
|
||||
}
|
||||
Vendored
+7
@@ -11,10 +11,17 @@
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
# Workaround for https://issuetracker.google.com/331556916
|
||||
-keepclassmembers class androidx.biometric.BiometricManager {
|
||||
public int canAuthenticate(int);
|
||||
}
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
|
||||
-keepattributes LineNumberTable
|
||||
|
||||
-allowaccessmodification
|
||||
-repackageclasses
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 17,
|
||||
"identityHash": "d6375317a9298b7feae5925a23272bb9",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "GALLERIES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "TOKEN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbKey",
|
||||
"columnName": "THUMB",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "posted",
|
||||
"columnName": "POSTED",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "UPLOADER",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "rating",
|
||||
"columnName": "RATING",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleLanguage",
|
||||
"columnName": "SIMPLE_LANGUAGE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favoriteSlot",
|
||||
"columnName": "FAVORITE_SLOT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_LABELS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOAD_LABELS_LABEL",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOADS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "STATE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "legacy",
|
||||
"columnName": "LEGACY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOADS_LABEL",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "DOWNLOAD_LABELS",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"LABEL"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"LABEL"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_DIRNAME",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dirname",
|
||||
"columnName": "DIRNAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "FILTER",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "TEXT",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "ENABLE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "HISTORY",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LOCAL_FAVORITES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "PROGRESS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `PAGE` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "page",
|
||||
"columnName": "PAGE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "QUICK_SEARCH",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "NAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keyword",
|
||||
"columnName": "KEYWORD",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "advanceSearch",
|
||||
"columnName": "ADVANCE_SEARCH",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minRating",
|
||||
"columnName": "MIN_RATING",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageFrom",
|
||||
"columnName": "PAGE_FROM",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageTo",
|
||||
"columnName": "PAGE_TO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6375317a9298b7feae5925a23272bb9')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 18,
|
||||
"identityHash": "bc90acf4ffb4e7cce9a16cd5a0f662f3",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "GALLERIES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "TOKEN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbKey",
|
||||
"columnName": "THUMB",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "posted",
|
||||
"columnName": "POSTED",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "UPLOADER",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "rating",
|
||||
"columnName": "RATING",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleLanguage",
|
||||
"columnName": "SIMPLE_LANGUAGE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favoriteSlot",
|
||||
"columnName": "FAVORITE_SLOT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_LABELS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOAD_LABELS_LABEL",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOADS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "STATE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "legacy",
|
||||
"columnName": "LEGACY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOADS_LABEL",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "DOWNLOAD_LABELS",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"LABEL"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"LABEL"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_DIRNAME",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dirname",
|
||||
"columnName": "DIRNAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "FILTER",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "TEXT",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "ENABLE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "HISTORY",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LOCAL_FAVORITES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "PROGRESS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `PAGE` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "page",
|
||||
"columnName": "PAGE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "QUICK_SEARCH",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "NAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keyword",
|
||||
"columnName": "KEYWORD",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "advanceSearch",
|
||||
"columnName": "ADVANCE_SEARCH",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minRating",
|
||||
"columnName": "MIN_RATING",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageFrom",
|
||||
"columnName": "PAGE_FROM",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageTo",
|
||||
"columnName": "PAGE_TO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc90acf4ffb4e7cce9a16cd5a0f662f3')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 19,
|
||||
"identityHash": "edd2f8e628072889f7702b50eb2c44a1",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "GALLERIES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "TOKEN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbKey",
|
||||
"columnName": "THUMB",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "posted",
|
||||
"columnName": "POSTED",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "UPLOADER",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "rating",
|
||||
"columnName": "RATING",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleLanguage",
|
||||
"columnName": "SIMPLE_LANGUAGE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favoriteSlot",
|
||||
"columnName": "FAVORITE_SLOT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_LABELS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOAD_LABELS_LABEL",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOADS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "STATE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "legacy",
|
||||
"columnName": "LEGACY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOADS_LABEL",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "DOWNLOAD_LABELS",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"LABEL"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"LABEL"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_DIRNAME",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dirname",
|
||||
"columnName": "DIRNAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "FILTER",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "TEXT",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "ENABLE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "HISTORY",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LOCAL_FAVORITES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "PROGRESS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `PAGE` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "page",
|
||||
"columnName": "PAGE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "QUICK_SEARCH",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "NAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keyword",
|
||||
"columnName": "KEYWORD",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "advanceSearch",
|
||||
"columnName": "ADVANCE_SEARCH",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minRating",
|
||||
"columnName": "MIN_RATING",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageFrom",
|
||||
"columnName": "PAGE_FROM",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageTo",
|
||||
"columnName": "PAGE_TO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'edd2f8e628072889f7702b50eb2c44a1')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 20,
|
||||
"identityHash": "a8dd0226b286cc57a8c99ba7ad416730",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "GALLERIES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `PAGES` INTEGER NOT NULL DEFAULT 0, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "TOKEN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbKey",
|
||||
"columnName": "THUMB",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "posted",
|
||||
"columnName": "POSTED",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "UPLOADER",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "rating",
|
||||
"columnName": "RATING",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pages",
|
||||
"columnName": "PAGES",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleLanguage",
|
||||
"columnName": "SIMPLE_LANGUAGE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favoriteSlot",
|
||||
"columnName": "FAVORITE_SLOT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_LABELS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOAD_LABELS_LABEL",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOADS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "STATE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "legacy",
|
||||
"columnName": "LEGACY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOADS_LABEL",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "DOWNLOAD_LABELS",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"LABEL"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"LABEL"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_DIRNAME",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dirname",
|
||||
"columnName": "DIRNAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "FILTER",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "TEXT",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "ENABLE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "HISTORY",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LOCAL_FAVORITES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "PROGRESS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `PAGE` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "page",
|
||||
"columnName": "PAGE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "QUICK_SEARCH",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "NAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keyword",
|
||||
"columnName": "KEYWORD",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "advanceSearch",
|
||||
"columnName": "ADVANCE_SEARCH",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minRating",
|
||||
"columnName": "MIN_RATING",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageFrom",
|
||||
"columnName": "PAGE_FROM",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageTo",
|
||||
"columnName": "PAGE_TO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a8dd0226b286cc57a8c99ba7ad416730')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 21,
|
||||
"identityHash": "6a9926f6722f8fc6f9c4426b31260e33",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "GALLERIES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `PAGES` INTEGER NOT NULL DEFAULT 0, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "TOKEN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbKey",
|
||||
"columnName": "THUMB",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "posted",
|
||||
"columnName": "POSTED",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "UPLOADER",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "rating",
|
||||
"columnName": "RATING",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pages",
|
||||
"columnName": "PAGES",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleLanguage",
|
||||
"columnName": "SIMPLE_LANGUAGE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favoriteSlot",
|
||||
"columnName": "FAVORITE_SLOT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_LABELS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOAD_LABELS_LABEL",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOADS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "STATE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "legacy",
|
||||
"columnName": "LEGACY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOADS_LABEL",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "DOWNLOAD_LABELS",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"LABEL"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"LABEL"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_DIRNAME",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dirname",
|
||||
"columnName": "DIRNAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "FILTER",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "TEXT",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "ENABLE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "HISTORY",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LOCAL_FAVORITES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "PROGRESS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `PAGE` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "page",
|
||||
"columnName": "PAGE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "QUICK_SEARCH",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "NAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keyword",
|
||||
"columnName": "KEYWORD",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "advanceSearch",
|
||||
"columnName": "ADVANCE_SEARCH",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minRating",
|
||||
"columnName": "MIN_RATING",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageFrom",
|
||||
"columnName": "PAGE_FROM",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageTo",
|
||||
"columnName": "PAGE_TO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"ftsVersion": "FTS4",
|
||||
"ftsOptions": {
|
||||
"tokenizer": "simple",
|
||||
"tokenizerArgs": [],
|
||||
"contentTable": "GALLERIES",
|
||||
"languageIdColumnName": "",
|
||||
"matchInfo": "FTS4",
|
||||
"notIndexedColumns": [],
|
||||
"prefixSizes": [],
|
||||
"preferredOrder": "ASC"
|
||||
},
|
||||
"contentSyncTriggers": [
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GalleryFts_BEFORE_UPDATE BEFORE UPDATE ON `GALLERIES` BEGIN DELETE FROM `GalleryFts` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GalleryFts_BEFORE_DELETE BEFORE DELETE ON `GALLERIES` BEGIN DELETE FROM `GalleryFts` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GalleryFts_AFTER_UPDATE AFTER UPDATE ON `GALLERIES` BEGIN INSERT INTO `GalleryFts`(`docid`, `TITLE`, `TITLE_JPN`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`); END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GalleryFts_AFTER_INSERT AFTER INSERT ON `GALLERIES` BEGIN INSERT INTO `GalleryFts`(`docid`, `TITLE`, `TITLE_JPN`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`); END"
|
||||
],
|
||||
"tableName": "GalleryFts",
|
||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`TITLE` TEXT NOT NULL, `TITLE_JPN` TEXT NOT NULL, content=`GALLERIES`)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": []
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6a9926f6722f8fc6f9c4426b31260e33')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 22,
|
||||
"identityHash": "f61ae3beef3739c0de7db945d2c9a4a4",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "GALLERIES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_TAGS` TEXT, `PAGES` INTEGER NOT NULL DEFAULT 0, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "TOKEN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbKey",
|
||||
"columnName": "THUMB",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "posted",
|
||||
"columnName": "POSTED",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "UPLOADER",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "rating",
|
||||
"columnName": "RATING",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleTags",
|
||||
"columnName": "SIMPLE_TAGS",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pages",
|
||||
"columnName": "PAGES",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleLanguage",
|
||||
"columnName": "SIMPLE_LANGUAGE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favoriteSlot",
|
||||
"columnName": "FAVORITE_SLOT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_LABELS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOAD_LABELS_LABEL",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOADS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "STATE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "legacy",
|
||||
"columnName": "LEGACY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOADS_LABEL",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "DOWNLOAD_LABELS",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"LABEL"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"LABEL"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_DIRNAME",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dirname",
|
||||
"columnName": "DIRNAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "FILTER",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "TEXT",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "ENABLE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "HISTORY",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LOCAL_FAVORITES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "PROGRESS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `PAGE` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "page",
|
||||
"columnName": "PAGE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "QUICK_SEARCH",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "NAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keyword",
|
||||
"columnName": "KEYWORD",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "advanceSearch",
|
||||
"columnName": "ADVANCE_SEARCH",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minRating",
|
||||
"columnName": "MIN_RATING",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageFrom",
|
||||
"columnName": "PAGE_FROM",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageTo",
|
||||
"columnName": "PAGE_TO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"ftsVersion": "FTS4",
|
||||
"ftsOptions": {
|
||||
"tokenizer": "simple",
|
||||
"tokenizerArgs": [],
|
||||
"contentTable": "GALLERIES",
|
||||
"languageIdColumnName": "",
|
||||
"matchInfo": "FTS4",
|
||||
"notIndexedColumns": [],
|
||||
"prefixSizes": [],
|
||||
"preferredOrder": "ASC"
|
||||
},
|
||||
"contentSyncTriggers": [
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_BEFORE_UPDATE BEFORE UPDATE ON `GALLERIES` BEGIN DELETE FROM `GALLERIES_FTS` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_BEFORE_DELETE BEFORE DELETE ON `GALLERIES` BEGIN DELETE FROM `GALLERIES_FTS` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_AFTER_UPDATE AFTER UPDATE ON `GALLERIES` BEGIN INSERT INTO `GALLERIES_FTS`(`docid`, `TITLE`, `TITLE_JPN`, `SIMPLE_TAGS`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`, NEW.`SIMPLE_TAGS`); END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_AFTER_INSERT AFTER INSERT ON `GALLERIES` BEGIN INSERT INTO `GALLERIES_FTS`(`docid`, `TITLE`, `TITLE_JPN`, `SIMPLE_TAGS`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`, NEW.`SIMPLE_TAGS`); END"
|
||||
],
|
||||
"tableName": "GALLERIES_FTS",
|
||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`TITLE` TEXT NOT NULL, `TITLE_JPN` TEXT NOT NULL, `SIMPLE_TAGS` TEXT, content=`GALLERIES`)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleTags",
|
||||
"columnName": "SIMPLE_TAGS",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": []
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f61ae3beef3739c0de7db945d2c9a4a4')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import com.google.android.gms.net.CronetProviderInstaller
|
||||
import org.chromium.net.CronetProvider
|
||||
import splitties.init.appCtx
|
||||
|
||||
val isCronetAvailable: Boolean
|
||||
get() = CronetProvider.getAllProviders(appCtx).any { it.isEnabled }.also {
|
||||
if (!it) {
|
||||
CronetProviderInstaller.installProvider(appCtx)
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,6 @@
|
||||
tools:targetApi="tiramisu">
|
||||
<profileable android:shell="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.ConfigureActivity"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:banner="@mipmap/ic_leanback_banner"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
From d1db03ebd9989d10a5a905b792b712c6487a0057 Mon Sep 17 00:00:00 2001
|
||||
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||
Date: Sat, 24 Feb 2024 12:03:15 +0000
|
||||
Subject: [PATCH] Don't build other binaries
|
||||
|
||||
---
|
||||
CMakeLists.txt | 8 ++++----
|
||||
1 file changed, 4 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index a346dd02..52b5d0d8 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -1567,7 +1567,7 @@ endif()
|
||||
# xzdec and lzmadec
|
||||
#############################################################################
|
||||
|
||||
-if(HAVE_DECODERS AND (NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900))
|
||||
+if(0)
|
||||
foreach(XZDEC xzdec lzmadec)
|
||||
add_executable("${XZDEC}"
|
||||
src/common/sysdefs.h
|
||||
@@ -1624,7 +1624,7 @@ endif()
|
||||
# lzmainfo
|
||||
#############################################################################
|
||||
|
||||
-if(HAVE_DECODERS AND (NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900))
|
||||
+if(0)
|
||||
add_executable(lzmainfo
|
||||
src/common/sysdefs.h
|
||||
src/common/tuklib_common.h
|
||||
@@ -1680,7 +1680,7 @@ endif()
|
||||
# xz
|
||||
#############################################################################
|
||||
|
||||
-if(NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900)
|
||||
+if(0)
|
||||
add_executable(xz
|
||||
src/common/mythread.h
|
||||
src/common/sysdefs.h
|
||||
@@ -1914,7 +1914,7 @@ endif()
|
||||
# Scripts
|
||||
#############################################################################
|
||||
|
||||
-if(UNIX)
|
||||
+if(0)
|
||||
# NOTE: This isn't as sophisticated as in the Autotools build which
|
||||
# uses posix-shell.m4 but hopefully this doesn't need to be either.
|
||||
# CMake likely won't be used on as many (old) obscure systems as the
|
||||
--
|
||||
2.34.1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(ehviewer C ASM)
|
||||
project(ehviewer C)
|
||||
include(FetchContent)
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
@@ -16,32 +16,32 @@ if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
-mllvm -polly-invariant-load-hoisting")
|
||||
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
|
||||
# Change the default path of fetch content to avoid downloading the same dependencies per architecture
|
||||
get_filename_component(deps "../_deps" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
|
||||
set(FETCHCONTENT_BASE_DIR ${deps})
|
||||
option(BUILD_TESTING OFF)
|
||||
|
||||
set(BUILD_TESTING 0)
|
||||
|
||||
FetchContent_Declare(liblzma
|
||||
GIT_REPOSITORY https://github.com/FooIbar/xz.git
|
||||
GIT_TAG v5.4.5
|
||||
)
|
||||
FetchContent_Declare(
|
||||
liblzma
|
||||
GIT_REPOSITORY https://git.tukaani.org/xz.git
|
||||
GIT_TAG v5.6.1
|
||||
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch || cd .
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(liblzma)
|
||||
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
|
||||
|
||||
# Build GNUTLS libnettle
|
||||
FetchContent_Declare(nettle
|
||||
FetchContent_Declare(
|
||||
nettle
|
||||
URL https://ftp.gnu.org/gnu/nettle/nettle-3.9.1.tar.gz
|
||||
URL_MD5 29fcd2dec6bf5b48e5e3ffb3cbc4779e
|
||||
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/nettle/nettle
|
||||
)
|
||||
SOURCE_DIR nettle-src/nettle
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(nettle)
|
||||
set(NETTLE_DIR ${nettle_SOURCE_DIR})
|
||||
add_subdirectory(nettle)
|
||||
|
||||
# Configure libnettle support for libarchive
|
||||
include_directories(nettle)
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR}/nettle-src)
|
||||
include_directories(.)
|
||||
set(HAVE_LIBNETTLE 1)
|
||||
set(HAVE_NETTLE_AES_H 1)
|
||||
@@ -67,16 +67,24 @@ option(ENABLE_TEST OFF)
|
||||
# Configure libarchive link's static lib
|
||||
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
|
||||
|
||||
FetchContent_Declare(libarchive
|
||||
FetchContent_Declare(
|
||||
libarchive
|
||||
GIT_REPOSITORY https://github.com/FooIbar/libarchive.git
|
||||
GIT_TAG v3.7.2
|
||||
)
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(libarchive)
|
||||
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)
|
||||
FetchContent_Declare(
|
||||
Corrosion
|
||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
||||
GIT_TAG 0a3bdf452995803c334d79dc9a2affbfad51b720
|
||||
)
|
||||
|
||||
add_custom_target("noop")
|
||||
FetchContent_MakeAvailable(Corrosion)
|
||||
corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml)
|
||||
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED archive.c border.c gifutils.c natsort/strnatcmp.c)
|
||||
target_link_libraries(${PROJECT_NAME} ehviewer_rust archive_static log jnigraphics)
|
||||
+125
-91
@@ -34,7 +34,6 @@
|
||||
|
||||
#include "natsort/strnatcmp.h"
|
||||
#include "ehviewer.h"
|
||||
#include "spinlock.h"
|
||||
|
||||
typedef struct {
|
||||
int using;
|
||||
@@ -49,7 +48,7 @@ typedef struct {
|
||||
size_t size;
|
||||
} entry;
|
||||
|
||||
_Atomic mcs_lock ctx_lock;
|
||||
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static archive_ctx **ctx_pool;
|
||||
#define CTX_POOL_SIZE 20
|
||||
|
||||
@@ -60,7 +59,6 @@ static size_t *mempoolofs = NULL;
|
||||
|
||||
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
|
||||
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
|
||||
#define MEMPOOL_ENTITY_SIZE(x) PAGE_ALIGN(entries[x].size)
|
||||
|
||||
#define PROT_RW (PROT_WRITE | PROT_READ)
|
||||
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
|
||||
@@ -86,6 +84,8 @@ const char supportExt[10][6] = {
|
||||
};
|
||||
|
||||
static inline int filename_is_playable_file(const char *name) {
|
||||
if (!name)
|
||||
return false;
|
||||
const char *dotptr = strrchr(name, '.');
|
||||
if (!dotptr++)
|
||||
return false;
|
||||
@@ -96,26 +96,39 @@ static inline int filename_is_playable_file(const char *name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline const char *archive_entry_pathname2(struct archive_entry *entry) {
|
||||
const char *name = archive_entry_pathname_utf8(entry);
|
||||
return name ? name : archive_entry_pathname(entry);
|
||||
}
|
||||
|
||||
static inline bool archive_entry_is_file(struct archive_entry *entry) {
|
||||
return archive_entry_filetype(entry) == AE_IFREG;
|
||||
}
|
||||
|
||||
static inline bool archive_entry_is_playable(struct archive_entry *entry) {
|
||||
return archive_entry_is_file(entry) &&
|
||||
filename_is_playable_file(archive_entry_pathname2(entry));
|
||||
}
|
||||
|
||||
static inline int compare_entries(const void *a, const void *b) {
|
||||
const char *fa = ((entry *) a)->filename;
|
||||
const char *fb = ((entry *) b)->filename;
|
||||
return strnatcmp(fa, fb);
|
||||
}
|
||||
|
||||
static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
static long archive_map_entries_index(archive_ctx *ctx, bool sort) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
const char *name = archive_entry_pathname(ctx->entry);
|
||||
if (filename_is_playable_file(name)) {
|
||||
const char *name = archive_entry_pathname2(ctx->entry);
|
||||
if (archive_entry_is_file(ctx->entry) && filename_is_playable_file(name)) {
|
||||
entries[count].filename = strdup(name);
|
||||
entries[count].index = count;
|
||||
entries[count].size = archive_entry_size(ctx->entry);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||
if (sort)
|
||||
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -135,39 +148,27 @@ static bool archive_prealloc_mempool() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Mincore does not support anon mapping yet
|
||||
// Now that we use MADV_FREE, how can we detect whether pages is reclaimed?
|
||||
static bool is_mempool_pages_present_and_lock(int index) {
|
||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||
size_t size = MEMPOOL_ENTITY_SIZE(index);
|
||||
mlock(addr, size);
|
||||
#if 0
|
||||
unsigned char vec[size / PAGE_SIZE];
|
||||
unsigned char r = 1;
|
||||
mincore(addr, size, vec);
|
||||
for (int i = 0; i < size / PAGE_SIZE; ++i) r &= vec[i];
|
||||
return r;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#define MEMPOOL_IN_CORE_AND_LOCK(x) is_mempool_pages_present_and_lock(x)
|
||||
#define ADDR_IN_FILE_MAPPING(addr) (addr >= archiveAddr && addr < archiveAddr + archiveSize)
|
||||
|
||||
static void mempool_release_pages(void *addr, size_t size) {
|
||||
size = PAGE_ALIGN(size);
|
||||
munlock(addr, size);
|
||||
int ret = madvise(addr, size, MADV_FREE);
|
||||
if (ret == -EINVAL) madvise(addr, size, MADV_DONTNEED);
|
||||
madvise_log_if_error(addr, size, MADV_DONTNEED);
|
||||
}
|
||||
|
||||
static bool kernel_can_prefault = true;
|
||||
|
||||
static void mempool_prefault_pages(void *addr, size_t size) {
|
||||
size = PAGE_ALIGN(size);
|
||||
if (kernel_can_prefault) {
|
||||
if (madvise(addr, size, MADV_POPULATE_WRITE)) kernel_can_prefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
if (filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
if (archive_entry_is_playable(ctx->entry))
|
||||
count++;
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -212,7 +213,7 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
|
||||
|
||||
static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
if (!archive_entry_is_playable(ctx->entry))
|
||||
continue;
|
||||
if (ctx->next_index++ == index) {
|
||||
return ctx->next_index - 1;
|
||||
@@ -224,8 +225,7 @@ static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
int ret;
|
||||
archive_ctx *ctx = NULL;
|
||||
mcs_lock_t local_lock;
|
||||
lock_mcs(&ctx_lock, &local_lock);
|
||||
pthread_mutex_lock(&mutex);
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||
if (!ctx_pool[i])
|
||||
continue;
|
||||
@@ -240,7 +240,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
}
|
||||
if (ctx)
|
||||
ctx->using = 1;
|
||||
unlock_mcs(&ctx_lock, &local_lock);
|
||||
pthread_mutex_unlock(&mutex);
|
||||
|
||||
if (!ctx) {
|
||||
archive_ctx *victimCtx = NULL;
|
||||
@@ -249,7 +249,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
ret = archive_alloc_ctx(&ctx);
|
||||
if (ret)
|
||||
return ret;
|
||||
lock_mcs(&ctx_lock, &local_lock);
|
||||
pthread_mutex_lock(&mutex);
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||
if (!ctx_pool[i]) {
|
||||
ctx_pool[i] = ctx;
|
||||
@@ -264,7 +264,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
}
|
||||
}
|
||||
if (replace) ctx_pool[victimIdx] = ctx;
|
||||
unlock_mcs(&ctx_lock, &local_lock);
|
||||
pthread_mutex_unlock(&mutex);
|
||||
if (replace) archive_release_ctx(victimCtx);
|
||||
}
|
||||
ret = archive_skip_to_index(ctx, idx);
|
||||
@@ -279,19 +279,16 @@ 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,
|
||||
jlong size) {
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd, jlong size, jboolean sort_entries) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
archive_ctx *ctx = NULL;
|
||||
int mmap_flags = MAP_PRIVATE;
|
||||
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
|
||||
archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (archiveAddr == MAP_FAILED) {
|
||||
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
|
||||
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
archiveSize = size;
|
||||
madvise_log_if_error(archiveAddr, archiveSize, MADV_WILLNEED);
|
||||
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
|
||||
if (!ctx_pool) {
|
||||
LOGE("Allocate archive ctx pool failed:ENOMEM");
|
||||
@@ -305,6 +302,11 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
} else {
|
||||
entryCount = archive_list_all_entries(ctx);
|
||||
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
|
||||
if (!entryCount) {
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
archive_release_ctx(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We must read through the file|vm then we can know whether it is encrypted
|
||||
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
||||
@@ -340,7 +342,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
entries = calloc(entryCount, sizeof(entry));
|
||||
r = archive_map_entries_index(ctx);
|
||||
r = archive_map_entries_index(ctx, sort_entries);
|
||||
if (!archive_prealloc_mempool()) {
|
||||
r = 0;
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
@@ -354,40 +356,47 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
return r;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "ConstantConditionsOC"
|
||||
#pragma ide diagnostic ignored "UnreachableCode"
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
|
||||
jint index) {
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||
size_t size = entries[index].size;
|
||||
if (!MEMPOOL_IN_CORE_AND_LOCK(index)) {
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret) return 0;
|
||||
ret = archive_read_data(ctx->arc, addr, size);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret = archive_get_ctx(&ctx, index);
|
||||
if (ret) return 0;
|
||||
void *buffer = NULL;
|
||||
size_t buffer_size = 0;
|
||||
la_int64_t output_ofs = 0;
|
||||
archive_read_data_block(ctx->arc, (const void **) &buffer, &buffer_size, &output_ofs);
|
||||
bool zero_copy = ADDR_IN_FILE_MAPPING(buffer) && !output_ofs && buffer_size == size;
|
||||
if (zero_copy) {
|
||||
ctx->using = 0;
|
||||
if (ret == size)
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
if (ret != size)
|
||||
LOGE("%s", "No enough data read, WTF?");
|
||||
if (ret < 0)
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
mempool_release_pages(addr, size);
|
||||
return (*env)->NewDirectByteBuffer(env, buffer, buffer_size);
|
||||
} else {
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
size_t bytes = 0;
|
||||
mempool_prefault_pages(addr, size);
|
||||
do {
|
||||
memcpy(addr + output_ofs, buffer, buffer_size);
|
||||
bytes += buffer_size;
|
||||
ret = archive_read_data_block(ctx->arc, (const void **) &buffer, &buffer_size, &output_ofs);
|
||||
} while (bytes != size && !ret);
|
||||
ctx->using = 0;
|
||||
if (bytes == size) {
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
} else {
|
||||
if (ret < 0) {
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
LOGE("%s", "No enough data read, WTF?");
|
||||
}
|
||||
}
|
||||
mempool_release_pages(addr, size);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
||||
EH_UNUSED(env);
|
||||
@@ -415,8 +424,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
jstring str) {
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz, jstring str) {
|
||||
EH_UNUSED(thiz);
|
||||
struct archive_entry *entry;
|
||||
archive_ctx *ctx;
|
||||
@@ -427,11 +435,11 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
LOGE("Allocate passwd buffer failed");
|
||||
return false;
|
||||
}
|
||||
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
|
||||
(*env)->GetStringUTFRegion(env, str, 0, len, passwd);
|
||||
archive_alloc_ctx(&ctx);
|
||||
void *tmpBuf = alloca(4096);
|
||||
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(entry)))
|
||||
if (!archive_entry_is_playable(entry))
|
||||
continue;
|
||||
if (!archive_entry_is_encrypted(entry))
|
||||
continue;
|
||||
@@ -446,24 +454,15 @@ 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,
|
||||
jint index) {
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_getExtension(JNIEnv *env, jclass thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret)
|
||||
return NULL;
|
||||
jstring str = (*env)->NewStringUTF(env, archive_entry_pathname(ctx->entry));
|
||||
ctx->using = 0;
|
||||
return str;
|
||||
const char *ext = strrchr(entries[index].filename, '.') + 1;
|
||||
return (*env)->NewStringUTF(env, ext);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
||||
jint index, jint fd) {
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz, jint index, jint fd) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
@@ -471,16 +470,51 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (!ret) {
|
||||
archive_read_data_into_fd(ctx->arc, fd);
|
||||
ret = archive_read_data_into_fd(ctx->arc, fd);
|
||||
ctx->using = 0;
|
||||
}
|
||||
return ret == ARCHIVE_OK;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
|
||||
jobject buffer) {
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz, jobject buffer) {
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
mempool_release_pages(addr, size);
|
||||
if (!ADDR_IN_FILE_MAPPING(addr)) mempool_release_pages(addr, size);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_archiveFdBatch(JNIEnv *env, jclass clazz, jintArray fd_batch, jobjectArray names, jint arc_fd, jint size) {
|
||||
EH_UNUSED(clazz);
|
||||
struct archive *arc = archive_write_new();
|
||||
struct stat st;
|
||||
char buff[8192];
|
||||
jint fdBatch[size];
|
||||
(*env)->GetIntArrayRegion(env, fd_batch, 0, size, fdBatch);
|
||||
archive_write_set_format_zip(arc);
|
||||
archive_write_zip_set_compression_store(arc);
|
||||
archive_write_open_fd(arc, arc_fd);
|
||||
struct archive_entry *entry = archive_entry_new();
|
||||
for (int i = 0; i < size; i++) {
|
||||
int fd = fdBatch[i];
|
||||
jobject name = (*env)->GetObjectArrayElement(env, names, i);
|
||||
const char *cname = (*env)->GetStringUTFChars(env, name, false);
|
||||
archive_entry_set_pathname(entry, cname);
|
||||
(*env)->ReleaseStringUTFChars(env, name, cname);
|
||||
fstat(fd, &st);
|
||||
archive_entry_copy_stat(entry, &st);
|
||||
archive_entry_set_perm(entry, 0644);
|
||||
archive_write_header(arc, entry);
|
||||
int len = read(fd, buff, sizeof(buff));
|
||||
while (len > 0) {
|
||||
archive_write_data(arc, buff, len);
|
||||
len = read(fd, buff, sizeof(buff));
|
||||
}
|
||||
archive_write_finish_entry(arc);
|
||||
archive_entry_clear(entry);
|
||||
}
|
||||
archive_write_close(arc);
|
||||
archive_write_free(arc);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
#include <jni.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <android/bitmap.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/** A line will be considered as having content if 0.25% of it is filled. */
|
||||
const float filledRatioLimit = 0.0025;
|
||||
|
||||
/** When the threshold is closer to 1, less content will be cropped. **/
|
||||
#define THRESHOLD 0.75
|
||||
|
||||
const uint8_t thresholdForBlack = (uint8_t) (255.0 * THRESHOLD);
|
||||
|
||||
const uint8_t thresholdForWhite = (uint8_t) (255.0 - 255.0 * THRESHOLD);
|
||||
|
||||
static uint8_t inline grey(uint32_t pixel) {
|
||||
const uint8_t r = ((uint8_t *) &pixel)[0];
|
||||
const uint8_t g = ((uint8_t *) &pixel)[1];
|
||||
const uint8_t b = ((uint8_t *) &pixel)[2];
|
||||
return (r * 77 + g * 151 + b * 28) >> 8;
|
||||
}
|
||||
|
||||
static bool inline isBlackPixel(const uint32_t *pixels, uint32_t width, uint32_t x, uint32_t y) {
|
||||
const uint32_t pixel = *((uint32_t *) pixels + (y * width + x));
|
||||
return grey(pixel) < thresholdForBlack;
|
||||
}
|
||||
|
||||
static bool inline isWhitePixel(const uint32_t *pixels, uint32_t width, uint32_t x, uint32_t y) {
|
||||
const uint32_t pixel = *((uint32_t *) pixels + (y * width + x));
|
||||
return grey(pixel) > thresholdForWhite;
|
||||
}
|
||||
|
||||
/** Return the first x position where there is a substantial amount of fill,
|
||||
* starting the search from the left. */
|
||||
uint32_t findBorderLeft(uint32_t *pixels, uint32_t width, uint32_t height,
|
||||
uint32_t top, uint32_t bottom) {
|
||||
int x, y;
|
||||
const uint32_t filledLimit = round(height * filledRatioLimit / 2);
|
||||
|
||||
// Scan first line to detect dominant color
|
||||
uint32_t whitePixels = 0;
|
||||
uint32_t blackPixels = 0;
|
||||
|
||||
for (y = top; y < bottom; y += 2) {
|
||||
if (isBlackPixel(pixels, width, 0, y)) {
|
||||
blackPixels++;
|
||||
} else if (isWhitePixel(pixels, width, 0, y)) {
|
||||
whitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel;
|
||||
if (whitePixels > filledLimit && blackPixels > filledLimit) {
|
||||
// Mixed fill found... don't crop anything
|
||||
return 0;
|
||||
} else if (blackPixels > filledLimit) {
|
||||
detectFunc = isWhitePixel;
|
||||
}
|
||||
|
||||
// Scan vertical lines in search of filled lines
|
||||
for (x = 1; x < width; x++) {
|
||||
uint32_t filledCount = 0;
|
||||
|
||||
for (y = top; y < bottom; y += 2) {
|
||||
if (detectFunc(pixels, width, x, y)) {
|
||||
filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filledCount > filledLimit) {
|
||||
// This line contains enough fill
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
// No fill found... don't crop anything
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Return the first x position where there is a substantial amount of fill,
|
||||
* starting the search from the right. */
|
||||
uint32_t findBorderRight(uint32_t *pixels, uint32_t width, uint32_t height,
|
||||
uint32_t top, uint32_t bottom) {
|
||||
int x, y;
|
||||
const uint32_t filledLimit = round(height * filledRatioLimit / 2);
|
||||
|
||||
// Scan first line to detect dominant color
|
||||
uint32_t whitePixels = 0;
|
||||
uint32_t blackPixels = 0;
|
||||
|
||||
uint32_t lastX = width - 1;
|
||||
for (y = top; y < bottom; y += 2) {
|
||||
if (isBlackPixel(pixels, width, lastX, y)) {
|
||||
blackPixels++;
|
||||
} else if (isWhitePixel(pixels, width, lastX, y)) {
|
||||
whitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel;
|
||||
if (whitePixels > filledLimit && blackPixels > filledLimit) {
|
||||
// Mixed fill found... don't crop anything
|
||||
return width;
|
||||
} else if (blackPixels > filledLimit) {
|
||||
detectFunc = isWhitePixel;
|
||||
}
|
||||
|
||||
// Scan vertical lines in search of filled lines
|
||||
for (x = width - 2; x > 0; x--) {
|
||||
uint32_t filledCount = 0;
|
||||
|
||||
for (y = top; y < bottom; y += 2) {
|
||||
if (detectFunc(pixels, width, x, y)) {
|
||||
filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filledCount > filledLimit) {
|
||||
// This line contains enough fill
|
||||
return x + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// No fill found... don't crop anything
|
||||
return width;
|
||||
}
|
||||
|
||||
/** Return the first y position where there is a substantial amount of fill,
|
||||
* starting the search from the top. */
|
||||
uint32_t findBorderTop(uint32_t *pixels, uint32_t width, uint32_t height) {
|
||||
int x, y;
|
||||
const uint32_t filledLimit = round(width * filledRatioLimit / 2);
|
||||
|
||||
// Scan first line to detect dominant color
|
||||
uint32_t whitePixels = 0;
|
||||
uint32_t blackPixels = 0;
|
||||
|
||||
for (x = 0; x < width; x += 2) {
|
||||
if (isBlackPixel(pixels, width, x, 0)) {
|
||||
blackPixels++;
|
||||
} else if (isWhitePixel(pixels, width, x, 0)) {
|
||||
whitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel;
|
||||
if (whitePixels > filledLimit && blackPixels > filledLimit) {
|
||||
// Mixed fill found... don't crop anything
|
||||
return 0;
|
||||
} else if (blackPixels > filledLimit) {
|
||||
detectFunc = isWhitePixel;
|
||||
}
|
||||
|
||||
// Scan horizontal lines in search of filled lines
|
||||
for (y = 1; y < height; y++) {
|
||||
uint32_t filledCount = 0;
|
||||
|
||||
for (x = 0; x < width; x += 2) {
|
||||
if (detectFunc(pixels, width, x, y)) {
|
||||
filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filledCount > filledLimit) {
|
||||
// This line contains enough fill
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
// No fill found... don't crop anything
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Return the first y position where there is a substantial amount of fill,
|
||||
* starting the search from the bottom. */
|
||||
uint32_t findBorderBottom(uint32_t *pixels, uint32_t width, uint32_t height) {
|
||||
int x, y;
|
||||
const uint32_t filledLimit = round(width * filledRatioLimit / 2);
|
||||
|
||||
// Scan first line to detect dominant color
|
||||
uint32_t whitePixels = 0;
|
||||
uint32_t blackPixels = 0;
|
||||
uint32_t lastY = height - 1;
|
||||
|
||||
for (x = 0; x < width; x += 2) {
|
||||
if (isBlackPixel(pixels, width, x, lastY)) {
|
||||
blackPixels++;
|
||||
} else if (isWhitePixel(pixels, width, x, lastY)) {
|
||||
whitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel;
|
||||
if (whitePixels > filledLimit && blackPixels > filledLimit) {
|
||||
// Mixed fill found... don't crop anything
|
||||
return height;
|
||||
} else if (blackPixels > filledLimit) {
|
||||
detectFunc = isWhitePixel;
|
||||
}
|
||||
|
||||
// Scan horizontal lines in search of filled lines
|
||||
for (y = height - 2; y > 0; y--) {
|
||||
uint32_t filledCount = 0;
|
||||
|
||||
for (x = 0; x < width; x += 2) {
|
||||
if (detectFunc(pixels, width, x, y)) {
|
||||
filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filledCount > filledLimit) {
|
||||
// This line contains enough fill
|
||||
return y + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// No fill found... don't crop anything
|
||||
return height;
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_com_hippo_ehviewer_image_ImageKt_detectBorder(JNIEnv *env, jclass clazz, jobject bitmap) {
|
||||
void *pixels = NULL;
|
||||
AndroidBitmapInfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
AndroidBitmap_getInfo(env, bitmap, &info);
|
||||
AndroidBitmap_lockPixels(env, bitmap, &pixels);
|
||||
uint32_t width = info.width;
|
||||
uint32_t height = info.height;
|
||||
uint32_t top = findBorderTop(pixels, width, height);
|
||||
uint32_t bottom = findBorderBottom(pixels, width, height);
|
||||
uint32_t left = findBorderLeft(pixels, width, height, top, bottom);
|
||||
uint32_t right = findBorderRight(pixels, width, height, top, bottom);
|
||||
int array[4] = {left, top, right, bottom};
|
||||
AndroidBitmap_unlockPixels(env, bitmap);
|
||||
jintArray ret = (*env)->NewIntArray(env, 4);
|
||||
(*env)->SetIntArrayRegion(env, ret, 0, 4, array);
|
||||
return ret;
|
||||
}
|
||||
@@ -84,30 +84,6 @@ compare_right(nat_char const *a, nat_char const *b)
|
||||
} else if (!*a && !*b)
|
||||
return bias;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
compare_left(nat_char const *a, nat_char const *b)
|
||||
{
|
||||
/* Compare two left-aligned numbers: the first to have a
|
||||
different value wins. */
|
||||
for (;; a++, b++) {
|
||||
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||
return 0;
|
||||
if (!nat_isdigit(*a))
|
||||
return -1;
|
||||
if (!nat_isdigit(*b))
|
||||
return +1;
|
||||
if (*a < *b)
|
||||
return -1;
|
||||
if (*a > *b)
|
||||
return +1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -116,30 +92,23 @@ strnatcmp0(nat_char const *a, nat_char const *b, int fold_case)
|
||||
{
|
||||
int ai, bi;
|
||||
nat_char ca, cb;
|
||||
int fractional, result;
|
||||
int result;
|
||||
|
||||
ai = bi = 0;
|
||||
while (1) {
|
||||
ca = a[ai]; cb = b[bi];
|
||||
|
||||
/* skip over leading spaces or zeros */
|
||||
while (nat_isspace(ca))
|
||||
while (nat_isspace(ca) || ca == '0')
|
||||
ca = a[++ai];
|
||||
|
||||
while (nat_isspace(cb))
|
||||
while (nat_isspace(cb) || cb == '0')
|
||||
cb = b[++bi];
|
||||
|
||||
/* process run of digits */
|
||||
if (nat_isdigit(ca) && nat_isdigit(cb)) {
|
||||
fractional = (ca == '0' || cb == '0');
|
||||
|
||||
if (fractional) {
|
||||
if ((result = compare_left(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
} else {
|
||||
if ((result = compare_right(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
}
|
||||
if ((result = compare_right(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!ca && !cb) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/nettle
|
||||
@@ -5,232 +5,232 @@ set(LIBNETTLE_DEFINITIONS
|
||||
-DHAVE_CONFIG_H)
|
||||
|
||||
set(LIBNETTLE_SOURCES
|
||||
nettle/aes-decrypt-internal.c
|
||||
nettle/aes-decrypt.c
|
||||
nettle/aes-decrypt-table.c
|
||||
nettle/aes128-decrypt.c
|
||||
nettle/aes192-decrypt.c
|
||||
nettle/aes256-decrypt.c
|
||||
nettle/aes-encrypt-internal.c
|
||||
nettle/aes-encrypt.c
|
||||
nettle/aes-encrypt-table.c
|
||||
nettle/aes128-encrypt.c
|
||||
nettle/aes192-encrypt.c
|
||||
nettle/aes256-encrypt.c
|
||||
nettle/aes-invert-internal.c
|
||||
nettle/aes-set-key-internal.c
|
||||
nettle/aes-set-encrypt-key.c
|
||||
nettle/aes-set-decrypt-key.c
|
||||
nettle/aes128-set-encrypt-key.c
|
||||
nettle/aes128-set-decrypt-key.c
|
||||
nettle/aes128-meta.c
|
||||
nettle/aes192-set-encrypt-key.c
|
||||
nettle/aes192-set-decrypt-key.c
|
||||
nettle/aes192-meta.c
|
||||
nettle/aes256-set-encrypt-key.c
|
||||
nettle/aes256-set-decrypt-key.c
|
||||
nettle/aes256-meta.c
|
||||
nettle/nist-keywrap.c
|
||||
nettle/arcfour.c
|
||||
nettle/arctwo.c
|
||||
nettle/arctwo-meta.c
|
||||
nettle/blowfish.c
|
||||
nettle/blowfish-bcrypt.c
|
||||
nettle/base16-encode.c
|
||||
nettle/base16-decode.c
|
||||
nettle/base16-meta.c
|
||||
nettle/base64-encode.c
|
||||
nettle/base64-decode.c
|
||||
nettle/base64-meta.c
|
||||
nettle/base64url-encode.c
|
||||
nettle/base64url-decode.c
|
||||
nettle/base64url-meta.c
|
||||
nettle/buffer.c
|
||||
nettle/buffer-init.c
|
||||
nettle/camellia-crypt-internal.c
|
||||
nettle/camellia-table.c
|
||||
nettle/camellia-absorb.c
|
||||
nettle/camellia-invert-key.c
|
||||
nettle/camellia128-set-encrypt-key.c
|
||||
nettle/camellia128-crypt.c
|
||||
nettle/camellia128-set-decrypt-key.c
|
||||
nettle/camellia128-meta.c
|
||||
nettle/camellia192-meta.c
|
||||
nettle/camellia256-set-encrypt-key.c
|
||||
nettle/camellia256-crypt.c
|
||||
nettle/camellia256-set-decrypt-key.c
|
||||
nettle/camellia256-meta.c
|
||||
nettle/cast128.c
|
||||
nettle/cast128-meta.c
|
||||
nettle/cbc.c
|
||||
nettle/cbc-aes128-encrypt.c
|
||||
nettle/cbc-aes192-encrypt.c
|
||||
nettle/cbc-aes256-encrypt.c
|
||||
nettle/ccm.c
|
||||
nettle/ccm-aes128.c
|
||||
nettle/ccm-aes192.c
|
||||
nettle/ccm-aes256.c
|
||||
nettle/cfb.c
|
||||
nettle/siv-cmac.c
|
||||
nettle/siv-cmac-aes128.c
|
||||
nettle/siv-cmac-aes256.c
|
||||
nettle/cnd-memcpy.c
|
||||
nettle/chacha-crypt.c
|
||||
nettle/chacha-core-internal.c
|
||||
nettle/chacha-poly1305.c
|
||||
nettle/chacha-poly1305-meta.c
|
||||
nettle/chacha-set-key.c
|
||||
nettle/chacha-set-nonce.c
|
||||
nettle/ctr.c
|
||||
nettle/ctr16.c
|
||||
nettle/des.c
|
||||
nettle/des3.c
|
||||
nettle/eax.c
|
||||
nettle/eax-aes128.c
|
||||
nettle/eax-aes128-meta.c
|
||||
nettle/ghash-set-key.c
|
||||
nettle/ghash-update.c
|
||||
nettle/gcm.c
|
||||
nettle/gcm-aes.c
|
||||
nettle/gcm-aes128.c
|
||||
nettle/gcm-aes128-meta.c
|
||||
nettle/gcm-aes192.c
|
||||
nettle/gcm-aes192-meta.c
|
||||
nettle/gcm-aes256.c
|
||||
nettle/gcm-aes256-meta.c
|
||||
nettle/gcm-camellia128.c
|
||||
nettle/gcm-camellia128-meta.c
|
||||
nettle/gcm-camellia256.c
|
||||
nettle/gcm-camellia256-meta.c
|
||||
nettle/cmac.c
|
||||
nettle/cmac64.c
|
||||
nettle/cmac-aes128.c
|
||||
nettle/cmac-aes256.c
|
||||
nettle/cmac-des3.c
|
||||
nettle/cmac-aes128-meta.c
|
||||
nettle/cmac-aes256-meta.c
|
||||
nettle/cmac-des3-meta.c
|
||||
nettle/gost28147.c
|
||||
nettle/gosthash94.c
|
||||
nettle/gosthash94-meta.c
|
||||
nettle/hmac.c
|
||||
nettle/hmac-gosthash94.c
|
||||
nettle/hmac-md5.c
|
||||
nettle/hmac-ripemd160.c
|
||||
nettle/hmac-sha1.c
|
||||
nettle/hmac-sha224.c
|
||||
nettle/hmac-sha256.c
|
||||
nettle/hmac-sha384.c
|
||||
nettle/hmac-sha512.c
|
||||
nettle/hmac-streebog.c
|
||||
nettle/hmac-sm3.c
|
||||
nettle/hmac-md5-meta.c
|
||||
nettle/hmac-ripemd160-meta.c
|
||||
nettle/hmac-sha1-meta.c
|
||||
nettle/hmac-sha224-meta.c
|
||||
nettle/hmac-sha256-meta.c
|
||||
nettle/hmac-sha384-meta.c
|
||||
nettle/hmac-sha512-meta.c
|
||||
nettle/hmac-streebog-meta.c
|
||||
nettle/hmac-sm3-meta.c
|
||||
nettle/knuth-lfib.c
|
||||
nettle/hkdf.c
|
||||
nettle/md2.c
|
||||
nettle/md2-meta.c
|
||||
nettle/md4.c
|
||||
nettle/md4-meta.c
|
||||
nettle/md5.c
|
||||
nettle/md5-compress.c
|
||||
nettle/md5-compat.c
|
||||
nettle/md5-meta.c
|
||||
nettle/memeql-sec.c
|
||||
nettle/memxor.c
|
||||
nettle/memxor3.c
|
||||
nettle/nettle-lookup-hash.c
|
||||
nettle/nettle-meta-aeads.c
|
||||
nettle/nettle-meta-armors.c
|
||||
nettle/nettle-meta-ciphers.c
|
||||
nettle/nettle-meta-hashes.c
|
||||
nettle/nettle-meta-macs.c
|
||||
nettle/pbkdf2.c
|
||||
nettle/pbkdf2-hmac-gosthash94.c
|
||||
nettle/pbkdf2-hmac-sha1.c
|
||||
nettle/pbkdf2-hmac-sha256.c
|
||||
nettle/pbkdf2-hmac-sha384.c
|
||||
nettle/pbkdf2-hmac-sha512.c
|
||||
nettle/poly1305-aes.c
|
||||
nettle/poly1305-internal.c
|
||||
nettle/realloc.c
|
||||
nettle/ripemd160.c
|
||||
nettle/ripemd160-compress.c
|
||||
nettle/ripemd160-meta.c
|
||||
nettle/salsa20-core-internal.c
|
||||
nettle/salsa20-crypt-internal.c
|
||||
nettle/salsa20-crypt.c
|
||||
nettle/salsa20r12-crypt.c
|
||||
nettle/salsa20-set-key.c
|
||||
nettle/salsa20-set-nonce.c
|
||||
nettle/salsa20-128-set-key.c
|
||||
nettle/salsa20-256-set-key.c
|
||||
nettle/sha1.c
|
||||
nettle/sha1-compress.c
|
||||
nettle/sha1-meta.c
|
||||
nettle/sha256.c
|
||||
nettle/sha256-compress-n.c
|
||||
nettle/sha224-meta.c
|
||||
nettle/sha256-meta.c
|
||||
nettle/sha512.c
|
||||
nettle/sha512-compress.c
|
||||
nettle/sha384-meta.c
|
||||
nettle/sha512-meta.c
|
||||
nettle/sha512-224-meta.c
|
||||
nettle/sha512-256-meta.c
|
||||
nettle/sha3.c
|
||||
nettle/sha3-permute.c
|
||||
nettle/sha3-224.c
|
||||
nettle/sha3-224-meta.c
|
||||
nettle/sha3-256.c
|
||||
nettle/sha3-256-meta.c
|
||||
nettle/sha3-384.c
|
||||
nettle/sha3-384-meta.c
|
||||
nettle/sha3-512.c
|
||||
nettle/sha3-512-meta.c
|
||||
nettle/shake256.c
|
||||
nettle/sm3.c
|
||||
nettle/sm3-meta.c
|
||||
nettle/serpent-set-key.c
|
||||
nettle/serpent-encrypt.c
|
||||
nettle/serpent-decrypt.c
|
||||
nettle/serpent-meta.c
|
||||
nettle/streebog.c
|
||||
nettle/streebog-meta.c
|
||||
nettle/twofish.c
|
||||
nettle/twofish-meta.c
|
||||
nettle/umac-nh.c
|
||||
nettle/umac-nh-n.c
|
||||
nettle/umac-l2.c
|
||||
nettle/umac-l3.c
|
||||
nettle/umac-poly64.c
|
||||
nettle/umac-poly128.c
|
||||
nettle/umac-set-key.c
|
||||
nettle/umac32.c
|
||||
nettle/umac64.c
|
||||
nettle/umac96.c
|
||||
nettle/umac128.c
|
||||
nettle/version.c
|
||||
nettle/write-be32.c
|
||||
nettle/write-le32.c
|
||||
nettle/write-le64.c
|
||||
nettle/yarrow256.c
|
||||
nettle/yarrow_key_event.c
|
||||
nettle/xts.c
|
||||
nettle/xts-aes128.c
|
||||
nettle/xts-aes256.c
|
||||
${NETTLE_DIR}/aes-decrypt-internal.c
|
||||
${NETTLE_DIR}/aes-decrypt.c
|
||||
${NETTLE_DIR}/aes-decrypt-table.c
|
||||
${NETTLE_DIR}/aes128-decrypt.c
|
||||
${NETTLE_DIR}/aes192-decrypt.c
|
||||
${NETTLE_DIR}/aes256-decrypt.c
|
||||
${NETTLE_DIR}/aes-encrypt-internal.c
|
||||
${NETTLE_DIR}/aes-encrypt.c
|
||||
${NETTLE_DIR}/aes-encrypt-table.c
|
||||
${NETTLE_DIR}/aes128-encrypt.c
|
||||
${NETTLE_DIR}/aes192-encrypt.c
|
||||
${NETTLE_DIR}/aes256-encrypt.c
|
||||
${NETTLE_DIR}/aes-invert-internal.c
|
||||
${NETTLE_DIR}/aes-set-key-internal.c
|
||||
${NETTLE_DIR}/aes-set-encrypt-key.c
|
||||
${NETTLE_DIR}/aes-set-decrypt-key.c
|
||||
${NETTLE_DIR}/aes128-set-encrypt-key.c
|
||||
${NETTLE_DIR}/aes128-set-decrypt-key.c
|
||||
${NETTLE_DIR}/aes128-meta.c
|
||||
${NETTLE_DIR}/aes192-set-encrypt-key.c
|
||||
${NETTLE_DIR}/aes192-set-decrypt-key.c
|
||||
${NETTLE_DIR}/aes192-meta.c
|
||||
${NETTLE_DIR}/aes256-set-encrypt-key.c
|
||||
${NETTLE_DIR}/aes256-set-decrypt-key.c
|
||||
${NETTLE_DIR}/aes256-meta.c
|
||||
${NETTLE_DIR}/nist-keywrap.c
|
||||
${NETTLE_DIR}/arcfour.c
|
||||
${NETTLE_DIR}/arctwo.c
|
||||
${NETTLE_DIR}/arctwo-meta.c
|
||||
${NETTLE_DIR}/blowfish.c
|
||||
${NETTLE_DIR}/blowfish-bcrypt.c
|
||||
${NETTLE_DIR}/base16-encode.c
|
||||
${NETTLE_DIR}/base16-decode.c
|
||||
${NETTLE_DIR}/base16-meta.c
|
||||
${NETTLE_DIR}/base64-encode.c
|
||||
${NETTLE_DIR}/base64-decode.c
|
||||
${NETTLE_DIR}/base64-meta.c
|
||||
${NETTLE_DIR}/base64url-encode.c
|
||||
${NETTLE_DIR}/base64url-decode.c
|
||||
${NETTLE_DIR}/base64url-meta.c
|
||||
${NETTLE_DIR}/buffer.c
|
||||
${NETTLE_DIR}/buffer-init.c
|
||||
${NETTLE_DIR}/camellia-crypt-internal.c
|
||||
${NETTLE_DIR}/camellia-table.c
|
||||
${NETTLE_DIR}/camellia-absorb.c
|
||||
${NETTLE_DIR}/camellia-invert-key.c
|
||||
${NETTLE_DIR}/camellia128-set-encrypt-key.c
|
||||
${NETTLE_DIR}/camellia128-crypt.c
|
||||
${NETTLE_DIR}/camellia128-set-decrypt-key.c
|
||||
${NETTLE_DIR}/camellia128-meta.c
|
||||
${NETTLE_DIR}/camellia192-meta.c
|
||||
${NETTLE_DIR}/camellia256-set-encrypt-key.c
|
||||
${NETTLE_DIR}/camellia256-crypt.c
|
||||
${NETTLE_DIR}/camellia256-set-decrypt-key.c
|
||||
${NETTLE_DIR}/camellia256-meta.c
|
||||
${NETTLE_DIR}/cast128.c
|
||||
${NETTLE_DIR}/cast128-meta.c
|
||||
${NETTLE_DIR}/cbc.c
|
||||
${NETTLE_DIR}/cbc-aes128-encrypt.c
|
||||
${NETTLE_DIR}/cbc-aes192-encrypt.c
|
||||
${NETTLE_DIR}/cbc-aes256-encrypt.c
|
||||
${NETTLE_DIR}/ccm.c
|
||||
${NETTLE_DIR}/ccm-aes128.c
|
||||
${NETTLE_DIR}/ccm-aes192.c
|
||||
${NETTLE_DIR}/ccm-aes256.c
|
||||
${NETTLE_DIR}/cfb.c
|
||||
${NETTLE_DIR}/siv-cmac.c
|
||||
${NETTLE_DIR}/siv-cmac-aes128.c
|
||||
${NETTLE_DIR}/siv-cmac-aes256.c
|
||||
${NETTLE_DIR}/cnd-memcpy.c
|
||||
${NETTLE_DIR}/chacha-crypt.c
|
||||
${NETTLE_DIR}/chacha-core-internal.c
|
||||
${NETTLE_DIR}/chacha-poly1305.c
|
||||
${NETTLE_DIR}/chacha-poly1305-meta.c
|
||||
${NETTLE_DIR}/chacha-set-key.c
|
||||
${NETTLE_DIR}/chacha-set-nonce.c
|
||||
${NETTLE_DIR}/ctr.c
|
||||
${NETTLE_DIR}/ctr16.c
|
||||
${NETTLE_DIR}/des.c
|
||||
${NETTLE_DIR}/des3.c
|
||||
${NETTLE_DIR}/eax.c
|
||||
${NETTLE_DIR}/eax-aes128.c
|
||||
${NETTLE_DIR}/eax-aes128-meta.c
|
||||
${NETTLE_DIR}/ghash-set-key.c
|
||||
${NETTLE_DIR}/ghash-update.c
|
||||
${NETTLE_DIR}/gcm.c
|
||||
${NETTLE_DIR}/gcm-aes.c
|
||||
${NETTLE_DIR}/gcm-aes128.c
|
||||
${NETTLE_DIR}/gcm-aes128-meta.c
|
||||
${NETTLE_DIR}/gcm-aes192.c
|
||||
${NETTLE_DIR}/gcm-aes192-meta.c
|
||||
${NETTLE_DIR}/gcm-aes256.c
|
||||
${NETTLE_DIR}/gcm-aes256-meta.c
|
||||
${NETTLE_DIR}/gcm-camellia128.c
|
||||
${NETTLE_DIR}/gcm-camellia128-meta.c
|
||||
${NETTLE_DIR}/gcm-camellia256.c
|
||||
${NETTLE_DIR}/gcm-camellia256-meta.c
|
||||
${NETTLE_DIR}/cmac.c
|
||||
${NETTLE_DIR}/cmac64.c
|
||||
${NETTLE_DIR}/cmac-aes128.c
|
||||
${NETTLE_DIR}/cmac-aes256.c
|
||||
${NETTLE_DIR}/cmac-des3.c
|
||||
${NETTLE_DIR}/cmac-aes128-meta.c
|
||||
${NETTLE_DIR}/cmac-aes256-meta.c
|
||||
${NETTLE_DIR}/cmac-des3-meta.c
|
||||
${NETTLE_DIR}/gost28147.c
|
||||
${NETTLE_DIR}/gosthash94.c
|
||||
${NETTLE_DIR}/gosthash94-meta.c
|
||||
${NETTLE_DIR}/hmac.c
|
||||
${NETTLE_DIR}/hmac-gosthash94.c
|
||||
${NETTLE_DIR}/hmac-md5.c
|
||||
${NETTLE_DIR}/hmac-ripemd160.c
|
||||
${NETTLE_DIR}/hmac-sha1.c
|
||||
${NETTLE_DIR}/hmac-sha224.c
|
||||
${NETTLE_DIR}/hmac-sha256.c
|
||||
${NETTLE_DIR}/hmac-sha384.c
|
||||
${NETTLE_DIR}/hmac-sha512.c
|
||||
${NETTLE_DIR}/hmac-streebog.c
|
||||
${NETTLE_DIR}/hmac-sm3.c
|
||||
${NETTLE_DIR}/hmac-md5-meta.c
|
||||
${NETTLE_DIR}/hmac-ripemd160-meta.c
|
||||
${NETTLE_DIR}/hmac-sha1-meta.c
|
||||
${NETTLE_DIR}/hmac-sha224-meta.c
|
||||
${NETTLE_DIR}/hmac-sha256-meta.c
|
||||
${NETTLE_DIR}/hmac-sha384-meta.c
|
||||
${NETTLE_DIR}/hmac-sha512-meta.c
|
||||
${NETTLE_DIR}/hmac-streebog-meta.c
|
||||
${NETTLE_DIR}/hmac-sm3-meta.c
|
||||
${NETTLE_DIR}/knuth-lfib.c
|
||||
${NETTLE_DIR}/hkdf.c
|
||||
${NETTLE_DIR}/md2.c
|
||||
${NETTLE_DIR}/md2-meta.c
|
||||
${NETTLE_DIR}/md4.c
|
||||
${NETTLE_DIR}/md4-meta.c
|
||||
${NETTLE_DIR}/md5.c
|
||||
${NETTLE_DIR}/md5-compress.c
|
||||
${NETTLE_DIR}/md5-compat.c
|
||||
${NETTLE_DIR}/md5-meta.c
|
||||
${NETTLE_DIR}/memeql-sec.c
|
||||
${NETTLE_DIR}/memxor.c
|
||||
${NETTLE_DIR}/memxor3.c
|
||||
${NETTLE_DIR}/nettle-lookup-hash.c
|
||||
${NETTLE_DIR}/nettle-meta-aeads.c
|
||||
${NETTLE_DIR}/nettle-meta-armors.c
|
||||
${NETTLE_DIR}/nettle-meta-ciphers.c
|
||||
${NETTLE_DIR}/nettle-meta-hashes.c
|
||||
${NETTLE_DIR}/nettle-meta-macs.c
|
||||
${NETTLE_DIR}/pbkdf2.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-gosthash94.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-sha1.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-sha256.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-sha384.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-sha512.c
|
||||
${NETTLE_DIR}/poly1305-aes.c
|
||||
${NETTLE_DIR}/poly1305-internal.c
|
||||
${NETTLE_DIR}/realloc.c
|
||||
${NETTLE_DIR}/ripemd160.c
|
||||
${NETTLE_DIR}/ripemd160-compress.c
|
||||
${NETTLE_DIR}/ripemd160-meta.c
|
||||
${NETTLE_DIR}/salsa20-core-internal.c
|
||||
${NETTLE_DIR}/salsa20-crypt-internal.c
|
||||
${NETTLE_DIR}/salsa20-crypt.c
|
||||
${NETTLE_DIR}/salsa20r12-crypt.c
|
||||
${NETTLE_DIR}/salsa20-set-key.c
|
||||
${NETTLE_DIR}/salsa20-set-nonce.c
|
||||
${NETTLE_DIR}/salsa20-128-set-key.c
|
||||
${NETTLE_DIR}/salsa20-256-set-key.c
|
||||
${NETTLE_DIR}/sha1.c
|
||||
${NETTLE_DIR}/sha1-compress.c
|
||||
${NETTLE_DIR}/sha1-meta.c
|
||||
${NETTLE_DIR}/sha256.c
|
||||
${NETTLE_DIR}/sha256-compress-n.c
|
||||
${NETTLE_DIR}/sha224-meta.c
|
||||
${NETTLE_DIR}/sha256-meta.c
|
||||
${NETTLE_DIR}/sha512.c
|
||||
${NETTLE_DIR}/sha512-compress.c
|
||||
${NETTLE_DIR}/sha384-meta.c
|
||||
${NETTLE_DIR}/sha512-meta.c
|
||||
${NETTLE_DIR}/sha512-224-meta.c
|
||||
${NETTLE_DIR}/sha512-256-meta.c
|
||||
${NETTLE_DIR}/sha3.c
|
||||
${NETTLE_DIR}/sha3-permute.c
|
||||
${NETTLE_DIR}/sha3-224.c
|
||||
${NETTLE_DIR}/sha3-224-meta.c
|
||||
${NETTLE_DIR}/sha3-256.c
|
||||
${NETTLE_DIR}/sha3-256-meta.c
|
||||
${NETTLE_DIR}/sha3-384.c
|
||||
${NETTLE_DIR}/sha3-384-meta.c
|
||||
${NETTLE_DIR}/sha3-512.c
|
||||
${NETTLE_DIR}/sha3-512-meta.c
|
||||
${NETTLE_DIR}/shake256.c
|
||||
${NETTLE_DIR}/sm3.c
|
||||
${NETTLE_DIR}/sm3-meta.c
|
||||
${NETTLE_DIR}/serpent-set-key.c
|
||||
${NETTLE_DIR}/serpent-encrypt.c
|
||||
${NETTLE_DIR}/serpent-decrypt.c
|
||||
${NETTLE_DIR}/serpent-meta.c
|
||||
${NETTLE_DIR}/streebog.c
|
||||
${NETTLE_DIR}/streebog-meta.c
|
||||
${NETTLE_DIR}/twofish.c
|
||||
${NETTLE_DIR}/twofish-meta.c
|
||||
${NETTLE_DIR}/umac-nh.c
|
||||
${NETTLE_DIR}/umac-nh-n.c
|
||||
${NETTLE_DIR}/umac-l2.c
|
||||
${NETTLE_DIR}/umac-l3.c
|
||||
${NETTLE_DIR}/umac-poly64.c
|
||||
${NETTLE_DIR}/umac-poly128.c
|
||||
${NETTLE_DIR}/umac-set-key.c
|
||||
${NETTLE_DIR}/umac32.c
|
||||
${NETTLE_DIR}/umac64.c
|
||||
${NETTLE_DIR}/umac96.c
|
||||
${NETTLE_DIR}/umac128.c
|
||||
${NETTLE_DIR}/version.c
|
||||
${NETTLE_DIR}/write-be32.c
|
||||
${NETTLE_DIR}/write-le32.c
|
||||
${NETTLE_DIR}/write-le64.c
|
||||
${NETTLE_DIR}/yarrow256.c
|
||||
${NETTLE_DIR}/yarrow_key_event.c
|
||||
${NETTLE_DIR}/xts.c
|
||||
${NETTLE_DIR}/xts-aes128.c
|
||||
${NETTLE_DIR}/xts-aes256.c
|
||||
)
|
||||
|
||||
set(LIBNETTLE_INCLUDES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nettle
|
||||
${NETTLE_DIR}
|
||||
)
|
||||
|
||||
add_library(nettle STATIC ${LIBNETTLE_SOURCES})
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#ifndef EHVIEWER_SPINLOCK_H
|
||||
#define EHVIEWER_SPINLOCK_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#define cmpxchg(P, O, N) __sync_val_compare_and_swap((P), (O), (N))
|
||||
|
||||
#define barrier() __asm__ __volatile__("":::"memory")
|
||||
|
||||
#if defined(__aarch64__) || defined(__arm__)
|
||||
#define cpu_relax() __asm__ volatile ("wfe");
|
||||
#else
|
||||
#define cpu_relax() __asm__ volatile ("pause" ::: "memory");
|
||||
#endif
|
||||
|
||||
typedef struct mcs_lock_t mcs_lock_t;
|
||||
struct mcs_lock_t {
|
||||
mcs_lock_t *next;
|
||||
int spin;
|
||||
};
|
||||
typedef struct mcs_lock_t *mcs_lock;
|
||||
|
||||
static inline void lock_mcs(_Atomic mcs_lock *m, mcs_lock_t *me) {
|
||||
mcs_lock_t *tail;
|
||||
me->next = NULL;
|
||||
me->spin = 0;
|
||||
tail = atomic_exchange(m, me);
|
||||
if (!tail) return;
|
||||
tail->next = me;
|
||||
barrier();
|
||||
while (!me->spin) cpu_relax();
|
||||
return;
|
||||
}
|
||||
|
||||
static inline void unlock_mcs(mcs_lock *m, mcs_lock_t *me) {
|
||||
if (!me->next) {
|
||||
if (cmpxchg(m, me, NULL) == me) return;
|
||||
while (!me->next) cpu_relax();
|
||||
}
|
||||
me->next->spin = 1;
|
||||
}
|
||||
|
||||
#endif //EHVIEWER_SPINLOCK_H
|
||||
@@ -1,989 +0,0 @@
|
||||
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.OnBackPressedDispatcherOwner
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.animation.core.SpringSpec
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.gestures.animateTo
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.snapTo
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionContext
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCompositionContext
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.ViewRootForInspector
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.popup
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.compose.ui.window.SecureFlagPolicy
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.findViewTreeViewModelStoreOwner
|
||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
||||
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
import com.hippo.ehviewer.ui.tools.animateFloatMergeOneWayPredictiveBackAsState
|
||||
import com.hippo.ehviewer.util.isAtLeastT
|
||||
import java.util.UUID
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
@ExperimentalMaterial3Api
|
||||
internal object AnchoredDraggableDefaults2 {
|
||||
/**
|
||||
* The default animation used by [AnchoredDraggableState].
|
||||
*/
|
||||
@get:ExperimentalMaterial3Api
|
||||
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
|
||||
@ExperimentalMaterial3Api
|
||||
val AnimationSpec = SpringSpec<Float>()
|
||||
}
|
||||
|
||||
/**
|
||||
* State of a sheet composable, such as [ModalBottomSheetFix]
|
||||
*
|
||||
* Contains states relating to its swipe position as well as animations between state values.
|
||||
*
|
||||
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large
|
||||
* enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move
|
||||
* to the [Hidden] state if available when hiding the sheet, either programmatically or by user
|
||||
* interaction.
|
||||
* @param initialValue The initial value of the state.
|
||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||
* @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always
|
||||
* expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either
|
||||
* programmatically or by user interaction.
|
||||
*/
|
||||
@Stable
|
||||
@ExperimentalMaterial3Api
|
||||
class SheetState2
|
||||
@Deprecated(
|
||||
message = "This constructor is deprecated. " +
|
||||
"Please use the constructor that provides a [Density]",
|
||||
replaceWith = ReplaceWith(
|
||||
"SheetState(" +
|
||||
"skipPartiallyExpanded, LocalDensity.current, initialValue, " +
|
||||
"confirmValueChange, skipHiddenState)",
|
||||
),
|
||||
)
|
||||
constructor(
|
||||
internal val skipPartiallyExpanded: Boolean,
|
||||
initialValue: SheetValue = SheetValue.Hidden,
|
||||
val confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
internal val skipHiddenState: Boolean = false,
|
||||
) {
|
||||
|
||||
/**
|
||||
* State of a sheet composable, such as [ModalBottomSheetFix]
|
||||
*
|
||||
* Contains states relating to its swipe position as well as animations between state values.
|
||||
*
|
||||
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large
|
||||
* enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move
|
||||
* to the [Hidden] state if available when hiding the sheet, either programmatically or by user
|
||||
* interaction.
|
||||
* @param initialValue The initial value of the state.
|
||||
* @param density The density that this state can use to convert values to and from dp.
|
||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||
* @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always
|
||||
* expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either
|
||||
* programmatically or by user interaction.
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
@Suppress("Deprecation")
|
||||
constructor(
|
||||
skipPartiallyExpanded: Boolean,
|
||||
density: Density,
|
||||
initialValue: SheetValue = SheetValue.Hidden,
|
||||
confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
skipHiddenState: Boolean = false,
|
||||
) : this(skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState) {
|
||||
this.density = density
|
||||
}
|
||||
init {
|
||||
if (skipPartiallyExpanded) {
|
||||
require(initialValue != SheetValue.PartiallyExpanded) {
|
||||
"The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " +
|
||||
"is set to true."
|
||||
}
|
||||
}
|
||||
if (skipHiddenState) {
|
||||
require(initialValue != SheetValue.Hidden) {
|
||||
"The initial value must not be set to Hidden if skipHiddenState is set to true."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current value of the state.
|
||||
*
|
||||
* If no swipe or animation is in progress, this corresponds to the state the bottom sheet is
|
||||
* currently in. If a swipe or an animation is in progress, this corresponds the state the sheet
|
||||
* was in before the swipe or animation started.
|
||||
*/
|
||||
|
||||
val currentValue: SheetValue get() = anchoredDraggableState.currentValue
|
||||
|
||||
/**
|
||||
* The target value of the bottom sheet state.
|
||||
*
|
||||
* If a swipe is in progress, this is the value that the sheet would animate to if the
|
||||
* swipe finishes. If an animation is running, this is the target value of that animation.
|
||||
* Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
|
||||
*/
|
||||
val targetValue: SheetValue get() = anchoredDraggableState.targetValue
|
||||
|
||||
/**
|
||||
* Whether the modal bottom sheet is visible.
|
||||
*/
|
||||
val isVisible: Boolean
|
||||
get() = anchoredDraggableState.currentValue != SheetValue.Hidden
|
||||
|
||||
/**
|
||||
* Require the current offset (in pixels) of the bottom sheet.
|
||||
*
|
||||
* The offset will be initialized during the first measurement phase of the provided sheet
|
||||
* content.
|
||||
*
|
||||
* These are the phases:
|
||||
* Composition { -> Effects } -> Layout { Measurement -> Placement } -> Drawing
|
||||
*
|
||||
* During the first composition, an [IllegalStateException] is thrown. In subsequent
|
||||
* compositions, the offset will be derived from the anchors of the previous pass. Always prefer
|
||||
* accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next
|
||||
* frame, after layout.
|
||||
*
|
||||
* @throws IllegalStateException If the offset has not been initialized yet
|
||||
*/
|
||||
fun requireOffset(): Float = anchoredDraggableState.requireOffset()
|
||||
|
||||
/**
|
||||
* Whether the sheet has an expanded state defined.
|
||||
*/
|
||||
|
||||
val hasExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded)
|
||||
|
||||
/**
|
||||
* Whether the modal bottom sheet has a partially expanded state defined.
|
||||
*/
|
||||
val hasPartiallyExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded)
|
||||
|
||||
/**
|
||||
* Fully expand the bottom sheet with animation and suspend until it is fully expanded or
|
||||
* animation has been cancelled.
|
||||
* *
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
*/
|
||||
suspend fun expand() {
|
||||
anchoredDraggableState.animateTo(SheetValue.Expanded)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the bottom sheet and suspend until it is partially expanded or animation has been
|
||||
* cancelled.
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
* @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true
|
||||
*/
|
||||
suspend fun partialExpand() {
|
||||
check(!skipPartiallyExpanded) {
|
||||
"Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" +
|
||||
" skipPartiallyExpanded to false to use this function."
|
||||
}
|
||||
animateTo(SheetValue.PartiallyExpanded)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined
|
||||
* else [Expanded].
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
*/
|
||||
suspend fun show() {
|
||||
val targetValue = when {
|
||||
hasPartiallyExpandedState -> SheetValue.PartiallyExpanded
|
||||
else -> SheetValue.Expanded
|
||||
}
|
||||
animateTo(targetValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the bottom sheet with animation and suspend until it is fully hidden or animation has
|
||||
* been cancelled.
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
*/
|
||||
suspend fun hide() {
|
||||
check(!skipHiddenState) {
|
||||
"Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" +
|
||||
" to false to use this function."
|
||||
}
|
||||
animateTo(SheetValue.Hidden)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate to a [targetValue].
|
||||
* If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
|
||||
* [targetValue] without updating the offset.
|
||||
*
|
||||
* @throws CancellationException if the interaction interrupted by another interaction like a
|
||||
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
|
||||
*
|
||||
* @param targetValue The target value of the animation
|
||||
*/
|
||||
internal suspend fun animateTo(
|
||||
targetValue: SheetValue,
|
||||
velocity: Float = anchoredDraggableState.lastVelocity,
|
||||
) {
|
||||
anchoredDraggableState.animateTo(targetValue, velocity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap to a [targetValue] without any animation.
|
||||
*
|
||||
* @throws CancellationException if the interaction interrupted by another interaction like a
|
||||
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
|
||||
*
|
||||
* @param targetValue The target value of the animation
|
||||
*/
|
||||
internal suspend fun snapTo(targetValue: SheetValue) {
|
||||
anchoredDraggableState.snapTo(targetValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest anchor taking into account the velocity and settle at it with an animation.
|
||||
*/
|
||||
internal suspend fun settle(velocity: Float) {
|
||||
anchoredDraggableState.settle(velocity)
|
||||
}
|
||||
|
||||
internal var anchoredDraggableState = AnchoredDraggableState(
|
||||
initialValue = initialValue,
|
||||
animationSpec = AnchoredDraggableDefaults2.AnimationSpec,
|
||||
confirmValueChange = confirmValueChange,
|
||||
positionalThreshold = { with(requireDensity()) { 56.dp.toPx() } },
|
||||
velocityThreshold = { with(requireDensity()) { 125.dp.toPx() } },
|
||||
)
|
||||
|
||||
internal val offset: Float get() = anchoredDraggableState.offset
|
||||
|
||||
internal var density: Density? = null
|
||||
private fun requireDensity() = requireNotNull(density) {
|
||||
"SheetState did not have a density attached. Are you using SheetState with " +
|
||||
"BottomSheetScaffold or ModalBottomSheet component?"
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The default [Saver] implementation for [SheetState2].
|
||||
*/
|
||||
fun Saver(
|
||||
skipPartiallyExpanded: Boolean,
|
||||
confirmValueChange: (SheetValue) -> Boolean,
|
||||
density: Density,
|
||||
) = Saver<SheetState2, SheetValue>(
|
||||
save = { it.currentValue },
|
||||
restore = { savedValue ->
|
||||
SheetState2(skipPartiallyExpanded, density, savedValue, confirmValueChange)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* The default [Saver] implementation for [SheetState2].
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "This function is deprecated. Please use the overload where Density is" +
|
||||
" provided.",
|
||||
replaceWith = ReplaceWith(
|
||||
"Saver(skipPartiallyExpanded, confirmValueChange, LocalDensity.current)",
|
||||
),
|
||||
)
|
||||
@Suppress("Deprecation")
|
||||
fun Saver(
|
||||
skipPartiallyExpanded: Boolean,
|
||||
confirmValueChange: (SheetValue) -> Boolean,
|
||||
) = Saver<SheetState2, SheetValue>(
|
||||
save = { it.currentValue },
|
||||
restore = { savedValue ->
|
||||
SheetState2(skipPartiallyExpanded, savedValue, confirmValueChange)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <a href="https://m3.material.io/components/bottom-sheets/overview" class="external" target="_blank">Material Design modal bottom sheet</a>.
|
||||
*
|
||||
* Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile,
|
||||
* especially when offering a long list of action items, or when items require longer descriptions
|
||||
* and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other
|
||||
* app functionality when they appear, and remaining on screen until confirmed, dismissed, or a
|
||||
* required action has been taken.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* A simple example of a modal bottom sheet looks like this:
|
||||
*
|
||||
* @sample androidx.compose.material3.samples.ModalBottomSheetSample
|
||||
*
|
||||
* @param onDismissRequest Executes when the user clicks outside of the bottom sheet, after sheet
|
||||
* animates to [Hidden].
|
||||
* @param modifier Optional [Modifier] for the bottom sheet.
|
||||
* @param sheetState The state of the bottom sheet.
|
||||
* @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take.
|
||||
* Pass in [Dp.Unspecified] for a sheet that spans the entire screen width.
|
||||
* @param shape The shape of the bottom sheet.
|
||||
* @param containerColor The color used for the background of this bottom sheet
|
||||
* @param contentColor The preferred color for content inside this bottom sheet. Defaults to either
|
||||
* the matching content color for [containerColor], or to the current [LocalContentColor] if
|
||||
* [containerColor] is not a color from the theme.
|
||||
* @param tonalElevation The tonal elevation of this bottom sheet.
|
||||
* @param scrimColor Color of the scrim that obscures content when the bottom sheet is open.
|
||||
* @param dragHandle Optional visual marker to swipe the bottom sheet.
|
||||
* @param windowInsets window insets to be passed to the bottom sheet window via [PaddingValues]
|
||||
* params.
|
||||
* @param properties [ModalBottomSheetProperties2] for further customization of this
|
||||
* modal bottom sheet's behavior.
|
||||
* @param content The content to be displayed inside the bottom sheet.
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
fun ModalBottomSheetFix(
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState2 = rememberModalBottomSheetState(),
|
||||
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
|
||||
shape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||
containerColor: Color = BottomSheetDefaults.ContainerColor,
|
||||
contentColor: Color = contentColorFor(containerColor),
|
||||
tonalElevation: Dp = BottomSheetDefaults.Elevation,
|
||||
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
||||
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||
windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
|
||||
properties: ModalBottomSheetProperties2 = ModalBottomSheetDefaults2.properties(),
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
// b/291735717 Remove this once deprecated methods without density are removed
|
||||
val density = LocalDensity.current
|
||||
SideEffect {
|
||||
sheetState.density = density
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val animateToDismiss: () -> Unit = {
|
||||
if (sheetState.confirmValueChange(SheetValue.Hidden)) {
|
||||
scope.launch { sheetState.hide() }.invokeOnCompletion {
|
||||
if (!sheetState.isVisible) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(sheetState) {
|
||||
snapshotFlow {
|
||||
sheetState.targetValue == SheetValue.Hidden && !sheetState.isVisible
|
||||
}.drop(1).collectLatest {
|
||||
if (it) onDismissRequest()
|
||||
}
|
||||
}
|
||||
val settleToDismiss: (velocity: Float) -> Unit = {
|
||||
scope.launch { sheetState.settle(it) }.invokeOnCompletion {
|
||||
if (!sheetState.isVisible) onDismissRequest()
|
||||
}
|
||||
}
|
||||
|
||||
val popupOnDismissRequest: () -> Unit = {
|
||||
if (sheetState.currentValue == SheetValue.Expanded && sheetState.hasPartiallyExpandedState) {
|
||||
scope.launch { sheetState.partialExpand() }
|
||||
} else { // Is expanded without collapsed state or is collapsed.
|
||||
scope.launch { sheetState.hide() }.invokeOnCompletion { onDismissRequest() }
|
||||
}
|
||||
}
|
||||
|
||||
ModalBottomSheetPopup(
|
||||
properties = properties,
|
||||
onDismissRequest = popupOnDismissRequest,
|
||||
windowInsets = windowInsets,
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val widthDp = configuration.screenWidthDp.dp
|
||||
val height = configuration.screenHeightDp.dp
|
||||
val predictiveState by animateFloatMergeOneWayPredictiveBackAsState(true) {
|
||||
sheetState.hide()
|
||||
onDismissRequest()
|
||||
}
|
||||
val predictiveModifier = Modifier.graphicsLayer {
|
||||
val scale = lerp(1f, 0.90f, predictiveState.absoluteValue)
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}.offset {
|
||||
val ofsY = lerp(0f, height.toPx() * 0.0625f, predictiveState.absoluteValue).roundToInt()
|
||||
val ofsX = lerp(0f, widthDp.toPx() * 0.025f, predictiveState).roundToInt()
|
||||
IntOffset(ofsX, ofsY)
|
||||
}
|
||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||
val fullHeight = constraints.maxHeight
|
||||
Scrim(
|
||||
color = scrimColor,
|
||||
onDismissRequest = animateToDismiss,
|
||||
visible = sheetState.targetValue != SheetValue.Hidden,
|
||||
)
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.widthIn(max = sheetMaxWidth)
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter)
|
||||
.offset {
|
||||
IntOffset(
|
||||
0,
|
||||
sheetState
|
||||
.requireOffset()
|
||||
.toInt(),
|
||||
)
|
||||
} then predictiveModifier
|
||||
.nestedScroll(
|
||||
remember(sheetState) {
|
||||
ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
||||
sheetState = sheetState,
|
||||
orientation = Orientation.Vertical,
|
||||
onFling = settleToDismiss,
|
||||
)
|
||||
},
|
||||
)
|
||||
.anchoredDraggable(
|
||||
state = sheetState.anchoredDraggableState,
|
||||
orientation = Orientation.Vertical,
|
||||
enabled = sheetState.isVisible,
|
||||
startDragImmediately = sheetState.anchoredDraggableState.isAnimationRunning,
|
||||
)
|
||||
.modalBottomSheetAnchors(
|
||||
sheetState = sheetState,
|
||||
fullHeight = fullHeight.toFloat(),
|
||||
),
|
||||
shape = shape,
|
||||
color = containerColor,
|
||||
contentColor = contentColor,
|
||||
tonalElevation = tonalElevation,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
if (dragHandle != null) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||
dragHandle()
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sheetState.hasExpandedState) {
|
||||
LaunchedEffect(sheetState) {
|
||||
sheetState.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
||||
sheetState: SheetState2,
|
||||
orientation: Orientation,
|
||||
onFling: (velocity: Float) -> Unit,
|
||||
): NestedScrollConnection = object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val delta = available.toFloat()
|
||||
return if (delta < 0 && source == NestedScrollSource.Drag) {
|
||||
sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset {
|
||||
return if (source == NestedScrollSource.Drag) {
|
||||
sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
val toFling = available.toFloat()
|
||||
val currentOffset = sheetState.requireOffset()
|
||||
val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor()
|
||||
return if (toFling < 0 && currentOffset > minAnchor) {
|
||||
onFling(toFling)
|
||||
// since we go to the anchor with tween settling, consume all for the best UX
|
||||
available
|
||||
} else {
|
||||
Velocity.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
onFling(available.toFloat())
|
||||
return available
|
||||
}
|
||||
|
||||
private fun Float.toOffset(): Offset = Offset(
|
||||
x = if (orientation == Orientation.Horizontal) this else 0f,
|
||||
y = if (orientation == Orientation.Vertical) this else 0f,
|
||||
)
|
||||
|
||||
@JvmName("velocityToFloat")
|
||||
private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y
|
||||
|
||||
@JvmName("offsetToFloat")
|
||||
private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties used to customize the behavior of a [ModalBottomSheetFix].
|
||||
*
|
||||
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
|
||||
* sheet's window.
|
||||
* @param isFocusable Whether the modal bottom sheet is focusable. When true,
|
||||
* the modal bottom sheet will receive IME events and key presses, such as when
|
||||
* the back button is pressed.
|
||||
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing
|
||||
* the back button. If true, pressing the back button will call onDismissRequest.
|
||||
* Note that [isFocusable] must be set to true in order to receive key events such as
|
||||
* the back button - if the modal bottom sheet is not focusable then this property does nothing.
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
class ModalBottomSheetProperties2(
|
||||
val securePolicy: SecureFlagPolicy,
|
||||
val isFocusable: Boolean,
|
||||
val shouldDismissOnBackPress: Boolean,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ModalBottomSheetProperties2) return false
|
||||
|
||||
if (securePolicy != other.securePolicy) return false
|
||||
if (isFocusable != other.isFocusable) return false
|
||||
if (shouldDismissOnBackPress != other.shouldDismissOnBackPress) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = securePolicy.hashCode()
|
||||
result = 31 * result + isFocusable.hashCode()
|
||||
result = 31 * result + shouldDismissOnBackPress.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values for [ModalBottomSheetFix]
|
||||
*/
|
||||
@Immutable
|
||||
@ExperimentalMaterial3Api
|
||||
object ModalBottomSheetDefaults2 {
|
||||
/**
|
||||
* Properties used to customize the behavior of a [ModalBottomSheetFix].
|
||||
*
|
||||
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
|
||||
* sheet's window.
|
||||
* @param isFocusable Whether the modal bottom sheet is focusable. When true,
|
||||
* the modal bottom sheet will receive IME events and key presses, such as when
|
||||
* the back button is pressed.
|
||||
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing
|
||||
* the back button. If true, pressing the back button will call onDismissRequest.
|
||||
* Note that [isFocusable] must be set to true in order to receive key events such as
|
||||
* the back button - if the modal bottom sheet is not focusable then this property does nothing.
|
||||
*/
|
||||
fun properties(
|
||||
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
|
||||
isFocusable: Boolean = true,
|
||||
shouldDismissOnBackPress: Boolean = true,
|
||||
) = ModalBottomSheetProperties2(securePolicy, isFocusable, shouldDismissOnBackPress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and [remember] a [SheetState2] for [ModalBottomSheetFix].
|
||||
*
|
||||
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is tall enough,
|
||||
* should be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
|
||||
* [Hidden] state when hiding the sheet, either programmatically or by user interaction.
|
||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
fun rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded: Boolean = false,
|
||||
confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
) = rememberSheetState(skipPartiallyExpanded, confirmValueChange, SheetValue.Hidden)
|
||||
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
internal fun rememberSheetState(
|
||||
skipPartiallyExpanded: Boolean = false,
|
||||
confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
initialValue: SheetValue = SheetValue.Hidden,
|
||||
skipHiddenState: Boolean = false,
|
||||
): SheetState2 {
|
||||
val density = LocalDensity.current
|
||||
return rememberSaveable(
|
||||
skipPartiallyExpanded,
|
||||
confirmValueChange,
|
||||
saver = SheetState2.Saver(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
confirmValueChange = confirmValueChange,
|
||||
density = density,
|
||||
),
|
||||
) {
|
||||
SheetState2(
|
||||
skipPartiallyExpanded,
|
||||
density,
|
||||
initialValue,
|
||||
confirmValueChange,
|
||||
skipHiddenState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Scrim(
|
||||
color: Color,
|
||||
onDismissRequest: () -> Unit,
|
||||
visible: Boolean,
|
||||
) {
|
||||
if (color.isSpecified) {
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (visible) 1f else 0f,
|
||||
animationSpec = TweenSpec(),
|
||||
label = "scrim",
|
||||
)
|
||||
val dismissSheet = if (visible) {
|
||||
Modifier
|
||||
.pointerInput(onDismissRequest) {
|
||||
detectTapGestures {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
.clearAndSetSemantics {}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.then(dismissSheet),
|
||||
) {
|
||||
drawRect(color = color, alpha = alpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalMaterial3Api
|
||||
private fun Modifier.modalBottomSheetAnchors(
|
||||
sheetState: SheetState2,
|
||||
fullHeight: Float,
|
||||
) = onSizeChanged { sheetSize ->
|
||||
|
||||
val newAnchors = DraggableAnchors {
|
||||
SheetValue.Hidden at fullHeight
|
||||
if (sheetSize.height > (fullHeight / 2) && !sheetState.skipPartiallyExpanded) {
|
||||
SheetValue.PartiallyExpanded at fullHeight / 2f
|
||||
}
|
||||
if (sheetSize.height != 0) {
|
||||
SheetValue.Expanded at max(0f, fullHeight - sheetSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
val newTarget = when (sheetState.anchoredDraggableState.targetValue) {
|
||||
SheetValue.Hidden -> SheetValue.Hidden
|
||||
SheetValue.PartiallyExpanded, SheetValue.Expanded -> {
|
||||
val hasPartiallyExpandedState = newAnchors.hasAnchorFor(SheetValue.PartiallyExpanded)
|
||||
val newTarget = if (hasPartiallyExpandedState) {
|
||||
SheetValue.PartiallyExpanded
|
||||
} else if (newAnchors.hasAnchorFor(SheetValue.Expanded)) SheetValue.Expanded else SheetValue.Hidden
|
||||
newTarget
|
||||
}
|
||||
}
|
||||
|
||||
sheetState.anchoredDraggableState.updateAnchors(newAnchors, newTarget)
|
||||
}
|
||||
|
||||
/**
|
||||
* Popup specific for modal bottom sheet.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ModalBottomSheetPopup(
|
||||
properties: ModalBottomSheetProperties2,
|
||||
onDismissRequest: () -> Unit,
|
||||
windowInsets: WindowInsets,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val id = rememberSaveable { UUID.randomUUID() }
|
||||
val parentComposition = rememberCompositionContext()
|
||||
val currentContent by rememberUpdatedState(content)
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val modalBottomSheetWindow = remember(configuration) {
|
||||
ModalBottomSheetWindow2(
|
||||
properties = properties,
|
||||
onDismissRequest = onDismissRequest,
|
||||
composeView = view,
|
||||
saveId = id,
|
||||
).apply {
|
||||
setCustomContent(
|
||||
parent = parentComposition,
|
||||
content = {
|
||||
Box(
|
||||
Modifier
|
||||
.semantics { this.popup() }
|
||||
.windowInsetsPadding(windowInsets)
|
||||
.then(
|
||||
// TODO(b/290893168): Figure out a solution for APIs < 30.
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
Modifier.imePadding()
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
currentContent()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(modalBottomSheetWindow) {
|
||||
modalBottomSheetWindow.show()
|
||||
modalBottomSheetWindow.superSetLayoutDirection(layoutDirection)
|
||||
onDispose {
|
||||
modalBottomSheetWindow.disposeComposition()
|
||||
modalBottomSheetWindow.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Custom compose view for [ModalBottomSheetFix] */
|
||||
@SuppressLint("ViewConstructor")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private class ModalBottomSheetWindow2(
|
||||
private val properties: ModalBottomSheetProperties2,
|
||||
private var onDismissRequest: () -> Unit,
|
||||
private val composeView: View,
|
||||
saveId: UUID,
|
||||
) : AbstractComposeView(composeView.context),
|
||||
ViewTreeObserver.OnGlobalLayoutListener,
|
||||
ViewRootForInspector,
|
||||
OnBackPressedDispatcherOwner {
|
||||
|
||||
init {
|
||||
id = android.R.id.content
|
||||
// Set up view owners
|
||||
setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
|
||||
setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
|
||||
setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner())
|
||||
setTag(androidx.compose.ui.R.id.compose_view_saveable_id_tag, "Popup:$saveId")
|
||||
// Enable children to draw their shadow by not clipping them
|
||||
clipChildren = false
|
||||
}
|
||||
|
||||
override val lifecycle = composeView.findViewTreeLifecycleOwner()!!.lifecycle
|
||||
override val onBackPressedDispatcher = OnBackPressedDispatcher {
|
||||
onDismissRequest()
|
||||
}
|
||||
|
||||
private val windowManager =
|
||||
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
|
||||
private val displayWidth: Int
|
||||
get() = context.resources.displayMetrics.widthPixels
|
||||
|
||||
private val params: WindowManager.LayoutParams =
|
||||
WindowManager.LayoutParams().apply {
|
||||
// Position bottom sheet from the bottom of the screen
|
||||
gravity = Gravity.BOTTOM or Gravity.START
|
||||
// Application panel window
|
||||
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
|
||||
// Fill up the entire app view
|
||||
width = displayWidth
|
||||
height = WindowManager.LayoutParams.MATCH_PARENT
|
||||
|
||||
// Format of screen pixels
|
||||
format = PixelFormat.TRANSLUCENT
|
||||
// Title used as fallback for a11y services
|
||||
// TODO: Provide bottom sheet window resource
|
||||
title = composeView.context.resources.getString(
|
||||
androidx.compose.ui.R.string.default_popup_window_title,
|
||||
)
|
||||
// Get the Window token from the parent view
|
||||
token = composeView.applicationWindowToken
|
||||
|
||||
// Flags specific to modal bottom sheet.
|
||||
flags = flags and (
|
||||
WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES or
|
||||
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
|
||||
).inv()
|
||||
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||
|
||||
// Security flag
|
||||
val secureFlagEnabled =
|
||||
properties.securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled())
|
||||
if (secureFlagEnabled) {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_SECURE
|
||||
} else {
|
||||
flags = flags and (WindowManager.LayoutParams.FLAG_SECURE.inv())
|
||||
}
|
||||
|
||||
// Focusable
|
||||
if (!properties.isFocusable) {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
} else {
|
||||
flags = flags and (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv())
|
||||
}
|
||||
}
|
||||
|
||||
private var content: @Composable () -> Unit by mutableStateOf({})
|
||||
|
||||
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
|
||||
private set
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides this) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCustomContent(
|
||||
parent: CompositionContext? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
parent?.let { setParentCompositionContext(it) }
|
||||
this.content = content
|
||||
shouldCreateCompositionOnAttachedToWindow = true
|
||||
}
|
||||
|
||||
fun show() {
|
||||
windowManager.addView(this, params)
|
||||
if (isAtLeastT) {
|
||||
onBackPressedDispatcher.setOnBackInvokedDispatcher(findOnBackInvokedDispatcher()!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
setViewTreeLifecycleOwner(null)
|
||||
setViewTreeSavedStateRegistryOwner(null)
|
||||
composeView.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
windowManager.removeViewImmediate(this)
|
||||
}
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
override fun setLayoutDirection(layoutDirection: Int) {
|
||||
// Do nothing. ViewRootImpl will call this method attempting to set the layout direction
|
||||
// from the context's locale, but we have one already from the parent composition.
|
||||
}
|
||||
|
||||
// Sets the "real" layout direction for our content that we obtain from the parent composition.
|
||||
fun superSetLayoutDirection(layoutDirection: LayoutDirection) {
|
||||
val direction = when (layoutDirection) {
|
||||
LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
|
||||
LayoutDirection.Rtl -> android.util.LayoutDirection.RTL
|
||||
}
|
||||
super.setLayoutDirection(direction)
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from AndroidPopup.android.kt
|
||||
private fun View.isFlagSecureEnabled(): Boolean {
|
||||
val windowParams = rootView.layoutParams as? WindowManager.LayoutParams
|
||||
if (windowParams != null) {
|
||||
return (windowParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Taken from AndroidPopup.android.kt
|
||||
private fun SecureFlagPolicy.shouldApplySecureFlag(isSecureFlagSetOnParent: Boolean): Boolean {
|
||||
return when (this) {
|
||||
SecureFlagPolicy.SecureOff -> false
|
||||
SecureFlagPolicy.SecureOn -> true
|
||||
SecureFlagPolicy.Inherit -> isSecureFlagSetOnParent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.SearchBarDefaults.InputFieldHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun SearchBarInputField(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val textColor = LocalTextStyle.current.color.takeOrElse {
|
||||
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
state = state,
|
||||
modifier = modifier.sizeIn(
|
||||
minWidth = SearchBarMinWidth,
|
||||
maxWidth = SearchBarMaxWidth,
|
||||
minHeight = InputFieldHeight,
|
||||
).focusRequester(focusRequester).onFocusChanged {
|
||||
if (it.isFocused) onExpandedChange(true)
|
||||
}.fillMaxWidth(),
|
||||
enabled = enabled,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
|
||||
cursorBrush = SolidColor(colors.cursorColor),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
onKeyboardAction = { onSearch() },
|
||||
interactionSource = interactionSource,
|
||||
decorator = @Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
innerTextField = innerTextField,
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon?.let { leading -> { Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() } } },
|
||||
trailingIcon = trailingIcon?.let { trailing -> { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } } },
|
||||
shape = SearchBarDefaults.inputFieldShape,
|
||||
colors = colors,
|
||||
contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
|
||||
container = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val shouldClearFocus = !expanded && focused
|
||||
LaunchedEffect(expanded) {
|
||||
if (shouldClearFocus) {
|
||||
// Not strictly needed according to the motion spec, but since the animation
|
||||
// already has a delay, this works around b/261632544.
|
||||
delay(AnimationDelayMillis.toLong())
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val SearchBarIconOffsetX = 4.dp
|
||||
val SearchBarMinWidth = 360.dp
|
||||
val SearchBarMaxWidth = 720.dp
|
||||
val SearchBarHorizontalPadding = 16.dp
|
||||
const val AnimationDelayMillis = 100
|
||||
@@ -1,502 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.MutatorMutex
|
||||
import androidx.compose.foundation.interaction.Interaction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.MutableWindowInsets
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.GenericShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text2.BasicTextField2
|
||||
import androidx.compose.foundation.text2.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text2.input.TextFieldState
|
||||
import androidx.compose.material3.SearchBarDefaults.InputFieldHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.geometry.toRect
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.semantics.onClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.lerp
|
||||
import androidx.compose.ui.unit.offset
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.compose.ui.zIndex
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sign
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* <a href="https://m3.material.io/components/search/overview" class="external" target="_blank">Material Design search</a>.
|
||||
*
|
||||
* A search bar represents a floating search field that allows users to enter a keyword or phrase
|
||||
* and get relevant information. It can be used as a way to navigate through an app via search
|
||||
* queries.
|
||||
*
|
||||
* An active search bar expands into a search "view" and can be used to display dynamic suggestions.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* A [SearchBar] expands to occupy the entirety of its allowed size when active. For full-screen
|
||||
* behavior as specified by Material guidelines, parent layouts of the [SearchBar] must not pass
|
||||
* any [Constraints] that limit its size, and the host activity should set
|
||||
* `WindowCompat.setDecorFitsSystemWindows(window, false)`.
|
||||
*
|
||||
* If this expansion behavior is undesirable, for example on large tablet screens, [DockedSearchBar]
|
||||
* can be used instead.
|
||||
*
|
||||
* @param state the query text to be shown in the search bar's input field
|
||||
* @param onSearch the callback to be invoked when the input service triggers the [ImeAction.Search]
|
||||
* action.
|
||||
* @param active whether this search bar is active
|
||||
* @param onActiveChange the callback to be invoked when this search bar's active state is changed
|
||||
* @param modifier the [Modifier] to be applied to this search bar
|
||||
* @param enabled controls the enabled state of this search bar. When `false`, this component will
|
||||
* not respond to user input, and it will appear visually disabled and disabled to accessibility
|
||||
* services.
|
||||
* @param placeholder the placeholder to be displayed when the search bar's query is empty.
|
||||
* @param leadingIcon the leading icon to be displayed at the beginning of the search bar container
|
||||
* @param trailingIcon the trailing icon to be displayed at the end of the search bar container
|
||||
* @param shape the shape of this search bar when it is not [active]. When [active], the shape will
|
||||
* always be [SearchBarDefaults.fullScreenShape].
|
||||
* @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
|
||||
* in different states. See [SearchBarDefaults.colors].
|
||||
* @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
|
||||
* translucent primary color overlay is applied on top of the container. A higher tonal elevation
|
||||
* value will result in a darker color in light theme and lighter color in dark theme. See also:
|
||||
* [Surface].
|
||||
* @param shadowElevation the elevation for the shadow below the search bar
|
||||
* @param windowInsets the window insets that the search bar will respect
|
||||
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
|
||||
* for this search bar. You can create and pass in your own `remember`ed instance to observe
|
||||
* [Interaction]s and customize the appearance / behavior of this search bar in different states.
|
||||
* @param content the content of this search bar that will be displayed below the input field
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
active: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
||||
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
|
||||
shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val animationProgress = remember { Animatable(initialValue = if (active) 1f else 0f) }
|
||||
var firstBackEvent by remember { mutableStateOf<BackEventCompat?>(null) }
|
||||
var currentBackEvent by remember { mutableStateOf<BackEventCompat?>(null) }
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val defaultInputFieldShape = SearchBarDefaults.inputFieldShape
|
||||
val defaultFullScreenShape = SearchBarDefaults.fullScreenShape
|
||||
val useFullScreenShape by remember {
|
||||
derivedStateOf(structuralEqualityPolicy()) { animationProgress.value == 1f }
|
||||
}
|
||||
val animatedShape = remember(useFullScreenShape, shape) {
|
||||
when {
|
||||
shape == defaultInputFieldShape ->
|
||||
// The shape can only be animated if it's the default spec value
|
||||
GenericShape { size, _ ->
|
||||
val radius = with(density) {
|
||||
(SearchBarCornerRadius * (1 - animationProgress.value)).toPx()
|
||||
}
|
||||
addRoundRect(RoundRect(size.toRect(), CornerRadius(radius)))
|
||||
}
|
||||
useFullScreenShape -> defaultFullScreenShape
|
||||
else -> shape
|
||||
}
|
||||
}
|
||||
|
||||
// The main animation complexity is allowing the component to smoothly expand while keeping the
|
||||
// input field at the same relative location on screen. `Modifier.windowInsetsPadding` does not
|
||||
// support animation and thus is not suitable. Instead, we convert the insets to a padding
|
||||
// applied to the Surface, which gradually becomes padding applied to the input field as the
|
||||
// animation proceeds.
|
||||
val unconsumedInsets = remember { MutableWindowInsets() }
|
||||
val topPadding = remember(density) {
|
||||
derivedStateOf {
|
||||
SearchBarVerticalPadding +
|
||||
unconsumedInsets.asPaddingValues(density).calculateTopPadding()
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
shape = animatedShape,
|
||||
color = colors.containerColor,
|
||||
contentColor = contentColorFor(colors.containerColor),
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation,
|
||||
modifier = modifier
|
||||
.padding(horizontal = lerp(SearchBarHorizontalPadding, 0.dp, animationProgress.value))
|
||||
.zIndex(1f)
|
||||
.onConsumedWindowInsetsChanged { consumedInsets ->
|
||||
unconsumedInsets.insets = windowInsets.exclude(consumedInsets)
|
||||
}
|
||||
.consumeWindowInsets(unconsumedInsets)
|
||||
.layout { measurable, constraints ->
|
||||
val animatedTopPadding =
|
||||
lerp(topPadding.value, 0.dp, animationProgress.value).roundToPx()
|
||||
|
||||
val startWidth: Int
|
||||
val startHeight: Int
|
||||
if (currentBackEvent == null) {
|
||||
startWidth = max(constraints.minWidth, SearchBarMinWidth.roundToPx())
|
||||
.coerceAtMost(min(constraints.maxWidth, SearchBarMaxWidth.roundToPx()))
|
||||
startHeight = max(constraints.minHeight, InputFieldHeight.roundToPx())
|
||||
.coerceAtMost(constraints.maxHeight)
|
||||
} else {
|
||||
startWidth =
|
||||
(constraints.maxWidth * SearchBarPredictiveBackMinScale).roundToInt()
|
||||
startHeight =
|
||||
(constraints.maxHeight * SearchBarPredictiveBackMinScale).roundToInt()
|
||||
}
|
||||
val endWidth = constraints.maxWidth
|
||||
val endHeight = constraints.maxHeight
|
||||
|
||||
val width = lerp(startWidth, endWidth, animationProgress.value)
|
||||
val height =
|
||||
lerp(startHeight, endHeight, animationProgress.value) + animatedTopPadding
|
||||
|
||||
val minOffsetMargin = SearchBarPredictiveBackMinMargin.roundToPx()
|
||||
val predictiveBackOffsetX = calculatePredictiveBackOffsetX(
|
||||
constraints,
|
||||
minOffsetMargin,
|
||||
currentBackEvent,
|
||||
animationProgress.value,
|
||||
)
|
||||
val predictiveBackOffsetY = calculatePredictiveBackOffsetY(
|
||||
constraints,
|
||||
minOffsetMargin,
|
||||
currentBackEvent,
|
||||
firstBackEvent,
|
||||
height,
|
||||
SearchBarPredictiveBackMaxOffsetY.roundToPx(),
|
||||
)
|
||||
val placeable = measurable.measure(
|
||||
Constraints
|
||||
.fixed(width, height)
|
||||
.offset(
|
||||
vertical = -animatedTopPadding,
|
||||
),
|
||||
)
|
||||
layout(width, height) {
|
||||
placeable.placeRelative(
|
||||
predictiveBackOffsetX,
|
||||
animatedTopPadding + predictiveBackOffsetY,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
val animatedInputFieldPadding = remember {
|
||||
AnimatedPaddingValues2(animationProgress.asState(), topPadding)
|
||||
}
|
||||
SearchBarInputField(
|
||||
state = state,
|
||||
onSearch = onSearch,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = Modifier.padding(paddingValues = animatedInputFieldPadding),
|
||||
enabled = enabled,
|
||||
label = title.takeUnless { active || animationProgress.value > 0 || state.text.isNotEmpty() },
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
colors = colors.inputFieldColors,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
||||
val showResults by remember {
|
||||
derivedStateOf(structuralEqualityPolicy()) { animationProgress.value > 0 }
|
||||
}
|
||||
if (showResults) {
|
||||
Column(Modifier.graphicsLayer { alpha = animationProgress.value }) {
|
||||
HorizontalDivider(color = colors.dividerColor)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
val shouldClearFocus = !active && isFocused
|
||||
if (shouldClearFocus) {
|
||||
LaunchedEffect(true) {
|
||||
// Not strictly needed according to the motion spec, but since the animation already has
|
||||
// a delay, this works around b/261632544.
|
||||
delay(AnimationDelayMillis.toLong())
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(active) {
|
||||
val animationInProgress = animationProgress.value > 0 && animationProgress.value < 1
|
||||
val animationSpec =
|
||||
if (animationInProgress) {
|
||||
AnimationPredictiveBackExitFloatSpec
|
||||
} else if (active) {
|
||||
AnimationEnterFloatSpec
|
||||
} else {
|
||||
AnimationExitFloatSpec
|
||||
}
|
||||
animationProgress.animateTo(
|
||||
targetValue = if (active) 1f else 0f,
|
||||
animationSpec = animationSpec,
|
||||
)
|
||||
}
|
||||
val mutatorMutex = remember { MutatorMutex() }
|
||||
if (active) {
|
||||
PredictiveBackHandler(true) { progress ->
|
||||
mutatorMutex.mutate {
|
||||
var canceled = false
|
||||
try {
|
||||
progress.collect { backEvent ->
|
||||
if (firstBackEvent == null) {
|
||||
firstBackEvent = backEvent
|
||||
}
|
||||
currentBackEvent = backEvent
|
||||
val interpolatedProgress = EaseOut.transform(backEvent.progress)
|
||||
animationProgress.snapTo(targetValue = 1 - interpolatedProgress)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
canceled = true
|
||||
} finally {
|
||||
firstBackEvent = null
|
||||
currentBackEvent = null
|
||||
onActiveChange(canceled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackOffsetX(
|
||||
constraints: Constraints,
|
||||
minMargin: Int,
|
||||
currentBackEvent: BackEventCompat?,
|
||||
progress: Float,
|
||||
): Int {
|
||||
if (currentBackEvent == null) {
|
||||
return 0
|
||||
}
|
||||
val directionMultiplier = if (currentBackEvent.swipeEdge == BackEventCompat.EDGE_LEFT) 1 else -1
|
||||
val maxOffsetX =
|
||||
(constraints.maxWidth * SearchBarPredictiveBackMaxOffsetXRatio) - minMargin
|
||||
return (maxOffsetX * (1 - progress)).toInt() * directionMultiplier
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackOffsetY(
|
||||
constraints: Constraints,
|
||||
minMargin: Int,
|
||||
currentBackEvent: BackEventCompat?,
|
||||
firstBackEvent: BackEventCompat?,
|
||||
height: Int,
|
||||
maxOffsetY: Int,
|
||||
): Int {
|
||||
if (firstBackEvent == null || currentBackEvent == null) {
|
||||
return 0
|
||||
}
|
||||
val availableVerticalSpace = max(0, (constraints.maxHeight - height) / 2 - minMargin)
|
||||
val adjustedMaxOffsetY = min(availableVerticalSpace, maxOffsetY)
|
||||
val yDelta = currentBackEvent.touchY - firstBackEvent.touchY
|
||||
val yProgress = abs(yDelta) / constraints.maxHeight
|
||||
val directionMultiplier = sign(yDelta)
|
||||
return (lerp(0, adjustedMaxOffsetY, yProgress) * directionMultiplier).roundToInt()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchBarInputField(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val textColor = LocalTextStyle.current.color.takeOrElse {
|
||||
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
|
||||
}
|
||||
|
||||
BasicTextField2(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
.height(InputFieldHeight)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { if (it.isFocused) onActiveChange(true) }
|
||||
.semantics {
|
||||
onClick {
|
||||
focusRequester.requestFocus()
|
||||
true
|
||||
}
|
||||
},
|
||||
enabled = enabled,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
|
||||
cursorBrush = SolidColor(colors.cursorColor),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { onSearch() }),
|
||||
interactionSource = interactionSource,
|
||||
decorator = @Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
innerTextField = innerTextField,
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon?.let { leading ->
|
||||
{
|
||||
Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() }
|
||||
}
|
||||
},
|
||||
trailingIcon = trailingIcon?.let { trailing ->
|
||||
{
|
||||
Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() }
|
||||
}
|
||||
},
|
||||
shape = SearchBarDefaults.inputFieldShape,
|
||||
colors = colors,
|
||||
contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
|
||||
container = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
private class AnimatedPaddingValues2(
|
||||
val animationProgress: State<Float>,
|
||||
val topPadding: State<Dp>,
|
||||
) : PaddingValues {
|
||||
override fun calculateTopPadding(): Dp = topPadding.value * animationProgress.value
|
||||
override fun calculateBottomPadding(): Dp = SearchBarVerticalPadding * animationProgress.value
|
||||
|
||||
override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = 0.dp
|
||||
override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = 0.dp
|
||||
}
|
||||
|
||||
// Measurement specs
|
||||
private val SearchBarCornerRadius: Dp = InputFieldHeight / 2
|
||||
internal val SearchBarMinWidth: Dp = 360.dp
|
||||
private val SearchBarMaxWidth: Dp = 720.dp
|
||||
internal val SearchBarVerticalPadding: Dp = 8.dp
|
||||
internal val SearchBarHorizontalPadding: Dp = 16.dp
|
||||
|
||||
// Search bar has 16dp padding between icons and start/end, while by default text field has 12dp.
|
||||
private val SearchBarIconOffsetX: Dp = 4.dp
|
||||
|
||||
private const val SearchBarPredictiveBackMinScale: Float = 9f / 10f
|
||||
private val SearchBarPredictiveBackMinMargin: Dp = 8.dp
|
||||
private const val SearchBarPredictiveBackMaxOffsetXRatio: Float = 1f / 20f
|
||||
private val SearchBarPredictiveBackMaxOffsetY: Dp = 24.dp
|
||||
|
||||
// Animation specs
|
||||
private const val AnimationEnterDurationMillis: Int = 600
|
||||
private const val AnimationExitDurationMillis: Int = 350
|
||||
private const val AnimationDelayMillis: Int = 100
|
||||
private val AnimationEnterEasing = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1.0f)
|
||||
private val AnimationExitEasing = CubicBezierEasing(0.0f, 1.0f, 0.0f, 1.0f)
|
||||
private val AnimationEnterFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationEnterDurationMillis,
|
||||
delayMillis = AnimationDelayMillis,
|
||||
easing = AnimationEnterEasing,
|
||||
)
|
||||
private val AnimationExitFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationExitDurationMillis,
|
||||
delayMillis = AnimationDelayMillis,
|
||||
easing = AnimationExitEasing,
|
||||
)
|
||||
private val AnimationPredictiveBackExitFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationExitDurationMillis,
|
||||
easing = AnimationExitEasing,
|
||||
)
|
||||
+8
-143
@@ -5,6 +5,7 @@ package androidx.compose.material3
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.animation.core.exponentialDecay
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
@@ -25,6 +26,7 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -52,7 +54,6 @@ import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.lerp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import com.hippo.ehviewer.ui.tools.animateFloatMergeOneWayPredictiveBackAsState
|
||||
import kotlin.math.roundToInt
|
||||
@@ -73,7 +74,8 @@ class DrawerState2(
|
||||
|
||||
val anchoredDraggableState = AnchoredDraggableState(
|
||||
initialValue = initialValue,
|
||||
animationSpec = AnimationSpec,
|
||||
snapAnimationSpec = AnimationSpec,
|
||||
decayAnimationSpec = exponentialDecay(7.5f),
|
||||
confirmValueChange = confirmStateChange,
|
||||
positionalThreshold = { distance -> distance * DrawerPositionalThreshold },
|
||||
velocityThreshold = { with(requireDensity()) { DrawerVelocityThreshold.toPx() } },
|
||||
@@ -234,7 +236,7 @@ class DrawerState2(
|
||||
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDrawerState(
|
||||
fun rememberDrawerState2(
|
||||
initialValue: DrawerValue,
|
||||
confirmStateChange: (DrawerValue) -> Boolean = { true },
|
||||
): DrawerState2 {
|
||||
@@ -244,154 +246,17 @@ fun rememberDrawerState(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalNavigationDrawer(
|
||||
fun ModalSideDrawer(
|
||||
drawerContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
drawerState: DrawerState2 = rememberDrawerState(DrawerValue.Closed),
|
||||
drawerState: DrawerState2 = rememberDrawerState2(DrawerValue.Closed),
|
||||
gesturesEnabled: Boolean = true,
|
||||
scrimColor: Color = DrawerDefaults.scrimColor,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
var minValue by remember { mutableStateOf(-with(density) { ContainerWidth.toPx() }) }
|
||||
val maxValue = 0f
|
||||
|
||||
SideEffect {
|
||||
drawerState.density = density
|
||||
drawerState.anchoredDraggableState.updateAnchors(
|
||||
DraggableAnchors {
|
||||
DrawerValue.Closed at minValue
|
||||
DrawerValue.Open at maxValue
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
val velocityTracker = remember { VelocityTracker() }
|
||||
val dragModifier = if (gesturesEnabled) {
|
||||
Modifier.pointerInput(isRtl) {
|
||||
val multiplier = if (isRtl) -1 else 1
|
||||
awaitEachGesture {
|
||||
fun canConsume(slop: Float) = (slop > 0 && drawerState.isClosed) || (slop < 0 && drawerState.isOpen)
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
var ignore = false
|
||||
val drag = awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
|
||||
val overSlop = over * multiplier
|
||||
val canConsume = canConsume(overSlop)
|
||||
if (canConsume && !ignore) {
|
||||
velocityTracker.addPointerInputChange(change)
|
||||
change.consume()
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(overSlop)
|
||||
} else {
|
||||
ignore = true
|
||||
}
|
||||
}
|
||||
if (drag != null) {
|
||||
horizontalDrag(drag.id) {
|
||||
velocityTracker.addPointerInputChange(it)
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(it.positionChange().x * multiplier)
|
||||
}
|
||||
val velocity = velocityTracker.calculateVelocity()
|
||||
velocityTracker.resetTracking()
|
||||
scope.launch {
|
||||
drawerState.anchoredDraggableState.settle(velocity.x * multiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(modifier.fillMaxSize().then(dragModifier)) {
|
||||
val radius by remember {
|
||||
snapshotFlow {
|
||||
val step = calculateFraction(minValue, maxValue, drawerState.currentOffset)
|
||||
lerp(0, 10, step).dp
|
||||
}
|
||||
}.collectAsState(0.dp)
|
||||
val blurModifier = Modifier.graphicsLayer {
|
||||
if (radius != 0.dp) {
|
||||
renderEffect = BlurEffect(radius.toPx(), radius.toPx(), TileMode.Clamp)
|
||||
shape = RectangleShape
|
||||
clip = true
|
||||
}
|
||||
}
|
||||
Box(modifier = blurModifier) {
|
||||
content()
|
||||
}
|
||||
Scrim(
|
||||
open = drawerState.isOpen,
|
||||
onClose = {
|
||||
if (gesturesEnabled && drawerState.confirmStateChange(DrawerValue.Closed)) {
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
},
|
||||
fraction = {
|
||||
calculateFraction(minValue, maxValue, drawerState.requireOffset())
|
||||
},
|
||||
color = scrimColor,
|
||||
)
|
||||
val predictiveState by animateFloatMergeOneWayPredictiveBackAsState(drawerState.isOpen) {
|
||||
drawerState.close()
|
||||
}
|
||||
val absoluteElevation = LocalAbsoluteTonalElevation.current + DrawerDefaults.ModalDrawerElevation
|
||||
val predictiveModifier = if (drawerState.isOpen) {
|
||||
if (predictiveState > 0) {
|
||||
Modifier.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
val multiplierX = lerp(1f, 1.05f, predictiveState)
|
||||
val multiplierY = lerp(1f, 0.95f, predictiveState)
|
||||
val scaledWidth = (multiplierX * placeable.width).roundToInt()
|
||||
val scaledHeight = (multiplierY * placeable.height).roundToInt()
|
||||
val reMeasured = measurable.measure(Constraints.fixed(scaledWidth, scaledHeight))
|
||||
layout(scaledWidth, scaledHeight) {
|
||||
reMeasured.placeRelative(scaledWidth - placeable.width, 0)
|
||||
}
|
||||
}.background(
|
||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
|
||||
shape = DrawerDefaults.shape,
|
||||
)
|
||||
} else {
|
||||
Modifier.graphicsLayer {
|
||||
val scale = lerp(1f, 0.95f, -predictiveState)
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}.offset {
|
||||
IntOffset(lerp(0f, minValue * 0.05f, -predictiveState).roundToInt(), 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier.onGloballyPositioned {
|
||||
val w = it.size.width
|
||||
if (w != 0) {
|
||||
minValue = -it.size.width.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(drawerState.requireOffset().roundToInt(), 0)
|
||||
} then predictiveModifier.align(Alignment.CenterStart),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
drawerContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SideDrawer(
|
||||
drawerContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
drawerState: DrawerState2 = rememberDrawerState(DrawerValue.Closed),
|
||||
gesturesEnabled: Boolean = true,
|
||||
scrimColor: Color = DrawerDefaults.scrimColor,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
var minValue by remember { mutableStateOf(with(density) { ContainerWidth.toPx() }) }
|
||||
var minValue by remember { mutableFloatStateOf(with(density) { ContainerWidth.toPx() }) }
|
||||
val maxValue = 0f
|
||||
|
||||
SideEffect {
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavGraph
|
||||
import androidx.navigation.NavGraphNavigator
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.NavigatorProvider
|
||||
|
||||
/**
|
||||
* Custom subclass of [NavGraphNavigator] that adds support for defining
|
||||
* transitions at the navigation graph level.
|
||||
*/
|
||||
@Navigator.Name("navigation")
|
||||
internal class ComposeNavGraphNavigator(
|
||||
navigatorProvider: NavigatorProvider,
|
||||
) : NavGraphNavigator(navigatorProvider) {
|
||||
override fun createDestination(): NavGraph {
|
||||
return ComposeNavGraph(this)
|
||||
}
|
||||
|
||||
internal class ComposeNavGraph(
|
||||
navGraphNavigator: Navigator<out NavGraph>,
|
||||
) : NavGraph(navGraphNavigator) {
|
||||
internal var enterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null
|
||||
|
||||
internal var exitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null
|
||||
|
||||
internal var popEnterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null
|
||||
|
||||
internal var popExitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.compose.ComposeNavigator.Destination
|
||||
|
||||
/**
|
||||
* Navigator that navigates through [Composable]s. Every destination using this Navigator must
|
||||
* set a valid [Composable] by setting it directly on an instantiated [Destination] or calling
|
||||
* [composable].
|
||||
*/
|
||||
@Navigator.Name("composable")
|
||||
public class ComposeNavigator : Navigator<Destination>() {
|
||||
|
||||
/**
|
||||
* Get the map of transitions currently in progress from the [state].
|
||||
*/
|
||||
internal val transitionsInProgress get() = state.transitionsInProgress
|
||||
|
||||
/**
|
||||
* Get the back stack from the [state].
|
||||
*/
|
||||
public val backStack get() = state.backStack
|
||||
|
||||
internal val isPop = mutableStateOf(false)
|
||||
|
||||
override fun navigate(
|
||||
entries: List<NavBackStackEntry>,
|
||||
navOptions: NavOptions?,
|
||||
navigatorExtras: Extras?,
|
||||
) {
|
||||
entries.forEach { entry ->
|
||||
state.pushWithTransition(entry)
|
||||
}
|
||||
isPop.value = false
|
||||
}
|
||||
|
||||
override fun createDestination(): Destination {
|
||||
return Destination(this) { }
|
||||
}
|
||||
|
||||
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
|
||||
state.popWithTransition(popUpTo, savedState)
|
||||
isPop.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to mark a navigation in transition as complete.
|
||||
*
|
||||
* This should be called in conjunction with [navigate] and [popBackStack] as those
|
||||
* calls merely start a transition to the target destination, and requires manually marking
|
||||
* the transition as complete by calling this method.
|
||||
*
|
||||
* Failing to call this method could result in entries being prevented from reaching their
|
||||
* final [Lifecycle.State].
|
||||
*/
|
||||
public fun onTransitionComplete(entry: NavBackStackEntry) {
|
||||
state.markTransitionComplete(entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* NavDestination specific to [ComposeNavigator]
|
||||
*/
|
||||
@NavDestination.ClassType(Composable::class)
|
||||
public class Destination(
|
||||
navigator: ComposeNavigator,
|
||||
internal val content:
|
||||
@Composable AnimatedContentScope.(@JvmSuppressWildcards NavBackStackEntry) -> Unit,
|
||||
) : NavDestination(navigator) {
|
||||
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of Destination that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
constructor(
|
||||
navigator: ComposeNavigator,
|
||||
content: @Composable (NavBackStackEntry) -> @JvmSuppressWildcards Unit,
|
||||
) : this(navigator, content = { entry -> content(entry) })
|
||||
|
||||
internal var enterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null
|
||||
|
||||
internal var exitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null
|
||||
|
||||
internal var popEnterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null
|
||||
|
||||
internal var popExitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
internal const val NAME = "composable"
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.compose.DialogNavigator.Destination
|
||||
|
||||
/**
|
||||
* Show each [Destination] on the [DialogNavigator]'s back stack as a [Dialog].
|
||||
*
|
||||
* Note that [NavHost] will call this for you; you do not need to call it manually.
|
||||
*/
|
||||
@Composable
|
||||
public fun DialogHost(dialogNavigator: DialogNavigator) {
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
val dialogBackStack by dialogNavigator.backStack.collectAsState()
|
||||
val visibleBackStack = rememberVisibleList(dialogBackStack)
|
||||
visibleBackStack.PopulateVisibleList(dialogBackStack)
|
||||
|
||||
val transitionInProgress by dialogNavigator.transitionInProgress.collectAsState()
|
||||
val dialogsToDispose = remember { mutableStateListOf<NavBackStackEntry>() }
|
||||
|
||||
visibleBackStack.forEach { backStackEntry ->
|
||||
val destination = backStackEntry.destination as Destination
|
||||
Dialog(
|
||||
onDismissRequest = { dialogNavigator.dismiss(backStackEntry) },
|
||||
properties = destination.dialogProperties,
|
||||
) {
|
||||
DisposableEffect(backStackEntry) {
|
||||
dialogsToDispose.add(backStackEntry)
|
||||
onDispose {
|
||||
dialogNavigator.onTransitionComplete(backStackEntry)
|
||||
dialogsToDispose.remove(backStackEntry)
|
||||
}
|
||||
}
|
||||
|
||||
// while in the scope of the composable, we provide the navBackStackEntry as the
|
||||
// ViewModelStoreOwner and LifecycleOwner
|
||||
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
|
||||
destination.content(backStackEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dialogs may have been popped before it was composed. To prevent leakage, we need to
|
||||
// mark popped entries as complete here. Check that we don't accidentally complete popped
|
||||
// entries that were composed, unless they were disposed of already.
|
||||
LaunchedEffect(transitionInProgress, dialogsToDispose) {
|
||||
transitionInProgress.forEach { entry ->
|
||||
if (!dialogNavigator.backStack.value.contains(entry) &&
|
||||
!dialogsToDispose.contains(entry)
|
||||
) {
|
||||
dialogNavigator.onTransitionComplete(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MutableList<NavBackStackEntry>.PopulateVisibleList(
|
||||
backStack: Collection<NavBackStackEntry>,
|
||||
) {
|
||||
val isInspecting = LocalInspectionMode.current
|
||||
backStack.forEach { entry ->
|
||||
DisposableEffect(entry.lifecycle) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
// show dialog in preview
|
||||
if (isInspecting && !contains(entry)) {
|
||||
add(entry)
|
||||
}
|
||||
// ON_START -> add to visibleBackStack, ON_STOP -> remove from visibleBackStack
|
||||
if (event == Lifecycle.Event.ON_START) {
|
||||
// We want to treat the visible lists as Sets but we want to keep
|
||||
// the functionality of mutableStateListOf() so that we recompose in response
|
||||
// to adds and removes.
|
||||
if (!contains(entry)) {
|
||||
add(entry)
|
||||
}
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_STOP) {
|
||||
remove(entry)
|
||||
}
|
||||
}
|
||||
entry.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
entry.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun rememberVisibleList(
|
||||
backStack: Collection<NavBackStackEntry>,
|
||||
): SnapshotStateList<NavBackStackEntry> {
|
||||
// show dialog in preview
|
||||
val isInspecting = LocalInspectionMode.current
|
||||
return remember(backStack) {
|
||||
mutableStateListOf<NavBackStackEntry>().also {
|
||||
it.addAll(
|
||||
backStack.filter { entry ->
|
||||
if (isInspecting) {
|
||||
true
|
||||
} else {
|
||||
entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.navigation.FloatingWindow
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.compose.DialogNavigator.Destination
|
||||
|
||||
/**
|
||||
* Navigator that navigates through [Composable]s that will be hosted within a
|
||||
* [Dialog]. Every destination using this Navigator must set a valid [Composable] by setting it
|
||||
* directly on an instantiated [Destination] or calling [dialog].
|
||||
*/
|
||||
@Navigator.Name("dialog")
|
||||
public class DialogNavigator : Navigator<Destination>() {
|
||||
|
||||
/**
|
||||
* Get the back stack from the [state].
|
||||
*/
|
||||
internal val backStack get() = state.backStack
|
||||
|
||||
/**
|
||||
* Get the transitioning dialogs from the [state].
|
||||
*/
|
||||
internal val transitionInProgress get() = state.transitionsInProgress
|
||||
|
||||
/**
|
||||
* Dismiss the dialog destination associated with the given [backStackEntry].
|
||||
*/
|
||||
internal fun dismiss(backStackEntry: NavBackStackEntry) {
|
||||
popBackStack(backStackEntry, false)
|
||||
}
|
||||
|
||||
override fun navigate(
|
||||
entries: List<NavBackStackEntry>,
|
||||
navOptions: NavOptions?,
|
||||
navigatorExtras: Extras?,
|
||||
) {
|
||||
entries.forEach { entry ->
|
||||
state.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createDestination(): Destination {
|
||||
return Destination(this) { }
|
||||
}
|
||||
|
||||
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
|
||||
state.popWithTransition(popUpTo, savedState)
|
||||
// When popping, the incoming dialog is marked transitioning to hold it in
|
||||
// STARTED. With pop complete, we can remove it from transition so it can move to RESUMED.
|
||||
val popIndex = state.transitionsInProgress.value.indexOf(popUpTo)
|
||||
// do not mark complete for entries up to and including popUpTo
|
||||
state.transitionsInProgress.value.forEachIndexed { index, entry ->
|
||||
if (index > popIndex) onTransitionComplete(entry)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onTransitionComplete(entry: NavBackStackEntry) {
|
||||
state.markTransitionComplete(entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* NavDestination specific to [DialogNavigator]
|
||||
*/
|
||||
@NavDestination.ClassType(Composable::class)
|
||||
public class Destination(
|
||||
navigator: DialogNavigator,
|
||||
internal val dialogProperties: DialogProperties = DialogProperties(),
|
||||
internal val content: @Composable (NavBackStackEntry) -> Unit,
|
||||
) : NavDestination(navigator), FloatingWindow
|
||||
|
||||
internal companion object {
|
||||
internal const val NAME = "dialog"
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.saveable.SaveableStateHolder
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Provides [this] [NavBackStackEntry] as [LocalViewModelStoreOwner], [LocalLifecycleOwner] and
|
||||
* [LocalSavedStateRegistryOwner] to the [content] and saves the [content]'s saveable states with
|
||||
* the given [saveableStateHolder].
|
||||
*
|
||||
* @param saveableStateHolder The [SaveableStateHolder] that holds the saved states. The same
|
||||
* holder should be used for all [NavBackStackEntry]s in the encapsulating [Composable] and the
|
||||
* holder should be hoisted.
|
||||
* @param content The content [Composable]
|
||||
*/
|
||||
@Composable
|
||||
public fun NavBackStackEntry.LocalOwnersProvider(
|
||||
saveableStateHolder: SaveableStateHolder,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalViewModelStoreOwner provides this,
|
||||
LocalLifecycleOwner provides this,
|
||||
LocalSavedStateRegistryOwner provides this,
|
||||
) {
|
||||
saveableStateHolder.SaveableStateProvider(content)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
|
||||
val viewModel = viewModel<BackStackEntryIdViewModel>()
|
||||
// Stash a reference to the SaveableStateHolder in the ViewModel so that
|
||||
// it is available when the ViewModel is cleared, marking the permanent removal of this
|
||||
// NavBackStackEntry from the back stack. Which, because of animations,
|
||||
// only happens after this leaves composition. Which means we can't rely on
|
||||
// DisposableEffect to clean up this reference (as it'll be cleaned up too early)
|
||||
viewModel.saveableStateHolderRef = WeakReference(this)
|
||||
SaveableStateProvider(viewModel.id, content)
|
||||
}
|
||||
|
||||
internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() {
|
||||
|
||||
private val IdKey = "SaveableStateHolder_BackStackEntryKey"
|
||||
|
||||
// we create our own id for each back stack entry to support multiple entries of the same
|
||||
// destination. this id will be restored by SavedStateHandle
|
||||
val id: UUID = handle.get<UUID>(IdKey) ?: UUID.randomUUID().also { handle.set(IdKey, it) }
|
||||
|
||||
lateinit var saveableStateHolderRef: WeakReference<SaveableStateHolder>
|
||||
|
||||
// onCleared will be called on the entries removed from the back stack. here we notify
|
||||
// SaveableStateProvider that we should remove any state is had associated with this
|
||||
// destination as it is no longer needed.
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
saveableStateHolderRef.get()?.removeState(id)
|
||||
saveableStateHolderRef.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.navigation.NamedNavArgument
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDeepLink
|
||||
import androidx.navigation.NavGraph
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.get
|
||||
|
||||
/**
|
||||
* Add the [Composable] to the [NavGraphBuilder]
|
||||
*
|
||||
* @param route route for the destination
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param content composable for the destination
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of composable builder that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
public fun NavGraphBuilder.composable(
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
content: @Composable (NavBackStackEntry) -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
ComposeNavigator.Destination(provider[ComposeNavigator::class]) {
|
||||
entry ->
|
||||
content(entry)
|
||||
}.apply {
|
||||
this.route = route
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the [Composable] to the [NavGraphBuilder]
|
||||
*
|
||||
* @param route route for the destination
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param enterTransition callback to determine the destination's enter transition
|
||||
* @param exitTransition callback to determine the destination's exit transition
|
||||
* @param popEnterTransition callback to determine the destination's popEnter transition
|
||||
* @param popExitTransition callback to determine the destination's popExit transition
|
||||
* @param content composable for the destination
|
||||
*/
|
||||
public fun NavGraphBuilder.composable(
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
enterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null,
|
||||
exitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null,
|
||||
popEnterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? =
|
||||
enterTransition,
|
||||
popExitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? =
|
||||
exitTransition,
|
||||
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
ComposeNavigator.Destination(
|
||||
provider[ComposeNavigator::class],
|
||||
content,
|
||||
).apply {
|
||||
this.route = route
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
this.enterTransition = enterTransition
|
||||
this.exitTransition = exitTransition
|
||||
this.popEnterTransition = popEnterTransition
|
||||
this.popExitTransition = popExitTransition
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a nested [NavGraph]
|
||||
*
|
||||
* @sample androidx.navigation.compose.samples.NestedNavInGraphWithArgs
|
||||
*
|
||||
* @param startDestination the starting destination's route for this NavGraph
|
||||
* @param route the destination's unique route
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param builder the builder used to construct the graph
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of navigation builder that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
public fun NavGraphBuilder.navigation(
|
||||
startDestination: String,
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
builder: NavGraphBuilder.() -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
NavGraphBuilder(provider, startDestination, route).apply(builder).build().apply {
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a nested [NavGraph]
|
||||
*
|
||||
* @param startDestination the starting destination's route for this NavGraph
|
||||
* @param route the destination's unique route
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param enterTransition callback to define enter transitions for destination in this NavGraph
|
||||
* @param exitTransition callback to define exit transitions for destination in this NavGraph
|
||||
* @param popEnterTransition callback to define pop enter transitions for destination in this
|
||||
* NavGraph
|
||||
* @param popExitTransition callback to define pop exit transitions for destination in this NavGraph
|
||||
* @param builder the builder used to construct the graph
|
||||
*
|
||||
* @return the newly constructed nested NavGraph
|
||||
*/
|
||||
public fun NavGraphBuilder.navigation(
|
||||
startDestination: String,
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? =
|
||||
null,
|
||||
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? =
|
||||
null,
|
||||
popEnterTransition: (
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = enterTransition,
|
||||
popExitTransition: (
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = exitTransition,
|
||||
builder: NavGraphBuilder.() -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
NavGraphBuilder(provider, startDestination, route).apply(builder).build().apply {
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
if (this is ComposeNavGraphNavigator.ComposeNavGraph) {
|
||||
this.enterTransition = enterTransition
|
||||
this.exitTransition = exitTransition
|
||||
this.popEnterTransition = popEnterTransition
|
||||
this.popExitTransition = popExitTransition
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the [Composable] to the [NavGraphBuilder] that will be hosted within a
|
||||
* [androidx.compose.ui.window.Dialog]. This is suitable only when this dialog represents
|
||||
* a separate screen in your app that needs its own lifecycle and saved state, independent
|
||||
* of any other destination in your navigation graph. For use cases such as `AlertDialog`,
|
||||
* you should use those APIs directly in the [composable] destination that wants to show that
|
||||
* dialog.
|
||||
*
|
||||
* @param route route for the destination
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param dialogProperties properties that should be passed to [androidx.compose.ui.window.Dialog].
|
||||
* @param content composable content for the destination that will be hosted within the Dialog
|
||||
*/
|
||||
public fun NavGraphBuilder.dialog(
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
dialogProperties: DialogProperties = DialogProperties(),
|
||||
content: @Composable (NavBackStackEntry) -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
DialogNavigator.Destination(
|
||||
provider[DialogNavigator::class],
|
||||
dialogProperties,
|
||||
content,
|
||||
).apply {
|
||||
this.route = route
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.createGraph
|
||||
import androidx.navigation.get
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import kotlin.math.roundToLong
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Provides in place in the Compose hierarchy for self contained navigation to occur.
|
||||
*
|
||||
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
|
||||
* the provided [navController].
|
||||
*
|
||||
* The builder passed into this method is [remember]ed. This means that for this NavHost, the
|
||||
* contents of the builder cannot be changed.
|
||||
*
|
||||
* @sample androidx.navigation.compose.samples.NavScaffold
|
||||
*
|
||||
* @param navController the navController for this host
|
||||
* @param startDestination the route for the start destination
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
* @param route the route for the graph
|
||||
* @param builder the builder used to construct the graph
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of NavHost that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
@Composable
|
||||
public fun NavHost(
|
||||
navController: NavHostController,
|
||||
startDestination: String,
|
||||
modifier: Modifier = Modifier,
|
||||
route: String? = null,
|
||||
builder: NavGraphBuilder.() -> Unit,
|
||||
) {
|
||||
NavHost(
|
||||
navController,
|
||||
remember(route, startDestination, builder) {
|
||||
navController.createGraph(startDestination, route, builder)
|
||||
},
|
||||
modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides in place in the Compose hierarchy for self contained navigation to occur.
|
||||
*
|
||||
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
|
||||
* the provided [navController].
|
||||
*
|
||||
* The builder passed into this method is [remember]ed. This means that for this NavHost, the
|
||||
* contents of the builder cannot be changed.
|
||||
*
|
||||
* @param navController the navController for this host
|
||||
* @param startDestination the route for the start destination
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
* @param contentAlignment The [Alignment] of the [AnimatedContent]
|
||||
* @param route the route for the graph
|
||||
* @param enterTransition callback to define enter transitions for destination in this host
|
||||
* @param exitTransition callback to define exit transitions for destination in this host
|
||||
* @param popEnterTransition callback to define popEnter transitions for destination in this host
|
||||
* @param popExitTransition callback to define popExit transitions for destination in this host
|
||||
* @param builder the builder used to construct the graph
|
||||
*/
|
||||
@Composable
|
||||
public fun NavHost(
|
||||
navController: NavHostController,
|
||||
startDestination: String,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.Center,
|
||||
route: String? = null,
|
||||
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
|
||||
{ fadeIn(animationSpec = tween(700)) },
|
||||
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
|
||||
{ fadeOut(animationSpec = tween(700)) },
|
||||
popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
|
||||
enterTransition,
|
||||
popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
|
||||
exitTransition,
|
||||
builder: NavGraphBuilder.() -> Unit,
|
||||
) {
|
||||
NavHost(
|
||||
navController,
|
||||
remember(route, startDestination, builder) {
|
||||
navController.createGraph(startDestination, route, builder)
|
||||
},
|
||||
modifier,
|
||||
contentAlignment,
|
||||
enterTransition,
|
||||
exitTransition,
|
||||
popEnterTransition,
|
||||
popExitTransition,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides in place in the Compose hierarchy for self contained navigation to occur.
|
||||
*
|
||||
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
|
||||
* the provided [navController].
|
||||
*
|
||||
* The graph passed into this method is [remember]ed. This means that for this NavHost, the graph
|
||||
* cannot be changed.
|
||||
*
|
||||
* @param navController the navController for this host
|
||||
* @param graph the graph for this host
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of NavHost that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
@Composable
|
||||
public fun NavHost(
|
||||
navController: NavHostController,
|
||||
graph: NavGraph,
|
||||
modifier: Modifier = Modifier,
|
||||
) = NavHost(navController, graph, modifier)
|
||||
|
||||
/**
|
||||
* Provides in place in the Compose hierarchy for self contained navigation to occur.
|
||||
*
|
||||
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
|
||||
* the provided [navController].
|
||||
*
|
||||
* @param navController the navController for this host
|
||||
* @param graph the graph for this host
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
* @param contentAlignment The [Alignment] of the [AnimatedContent]
|
||||
* @param enterTransition callback to define enter transitions for destination in this host
|
||||
* @param exitTransition callback to define exit transitions for destination in this host
|
||||
* @param popEnterTransition callback to define popEnter transitions for destination in this host
|
||||
* @param popExitTransition callback to define popExit transitions for destination in this host
|
||||
*/
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Composable
|
||||
public fun NavHost(
|
||||
navController: NavHostController,
|
||||
graph: NavGraph,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.Center,
|
||||
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
|
||||
{ fadeIn(animationSpec = tween(700)) },
|
||||
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
|
||||
{ fadeOut(animationSpec = tween(700)) },
|
||||
popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
|
||||
enterTransition,
|
||||
popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
|
||||
exitTransition,
|
||||
) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
|
||||
"NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
|
||||
}
|
||||
|
||||
navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
|
||||
|
||||
// Then set the graph
|
||||
navController.graph = graph
|
||||
|
||||
// Find the ComposeNavigator, returning early if it isn't found
|
||||
// (such as is the case when using TestNavHostController)
|
||||
val composeNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
|
||||
ComposeNavigator.NAME,
|
||||
) as? ComposeNavigator ?: return
|
||||
|
||||
val currentBackStack by composeNavigator.backStack.collectAsState()
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
// Setup the navController with proper owners
|
||||
navController.setLifecycleOwner(lifecycleOwner)
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
|
||||
val allVisibleEntries by navController.visibleEntries.collectAsState()
|
||||
|
||||
// Intercept back only when there's a destination to pop
|
||||
val visibleEntries by remember {
|
||||
derivedStateOf {
|
||||
allVisibleEntries.filter { entry ->
|
||||
entry.destination.navigatorName == ComposeNavigator.NAME
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val backStackEntry: NavBackStackEntry? = visibleEntries.lastOrNull()
|
||||
|
||||
val zIndices = remember { mutableMapOf<String, Float>() }
|
||||
|
||||
if (backStackEntry != null) {
|
||||
val finalEnter: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
|
||||
val targetDestination = targetState.destination as ComposeNavigator.Destination
|
||||
|
||||
if (composeNavigator.isPop.value) {
|
||||
targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
|
||||
destination.createPopEnterTransition(this)
|
||||
} ?: popEnterTransition.invoke(this)
|
||||
} else {
|
||||
targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
|
||||
destination.createEnterTransition(this)
|
||||
} ?: enterTransition.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
val finalExit: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
|
||||
val initialDestination = initialState.destination as ComposeNavigator.Destination
|
||||
|
||||
if (composeNavigator.isPop.value) {
|
||||
initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
|
||||
destination.createPopExitTransition(this)
|
||||
} ?: popExitTransition.invoke(this)
|
||||
} else {
|
||||
initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
|
||||
destination.createExitTransition(this)
|
||||
} ?: exitTransition.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(true) {
|
||||
onDispose {
|
||||
visibleEntries.forEach { entry ->
|
||||
composeNavigator.onTransitionComplete(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val enablePredictiveBack by Settings.predictiveNavAnim.collectAsState()
|
||||
val transition = if (enablePredictiveBack) {
|
||||
val duration = 500000000L
|
||||
val transitionState = remember { MutableTransitionState(backStackEntry) }
|
||||
val transition = updateTransition(transitionState = transitionState, label = "entry")
|
||||
val animatedFraction = remember { Animatable(0f).also { it.updateBounds(lowerBound = 0f, upperBound = 1f) } }
|
||||
|
||||
fun seekToFraction(lState: NavBackStackEntry, rState: NavBackStackEntry) {
|
||||
val fraction = animatedFraction.value
|
||||
val playTimeNanos = (fraction * duration).roundToLong()
|
||||
transition.setPlaytimeAfterInitialAndTargetStateEstablished(lState, rState, playTimeNanos)
|
||||
}
|
||||
|
||||
LaunchedEffect(backStackEntry) {
|
||||
try {
|
||||
animatedFraction.animateTo(1f, animationSpec = tween(500, easing = LinearEasing)) {
|
||||
seekToFraction(transitionState.currentState, backStackEntry)
|
||||
}
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
animatedFraction.snapTo(0f)
|
||||
transition.setPlaytimeAfterInitialAndTargetStateEstablished(
|
||||
backStackEntry,
|
||||
backStackEntry,
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PredictiveBackHandler(currentBackStack.size > 1) { flow ->
|
||||
// Prev can never be null, i.e. BackHandler should not enabled when prev is null
|
||||
val prev = currentBackStack.last { it != backStackEntry }
|
||||
try {
|
||||
flow.collect {
|
||||
val interpolated = EaseOut.transform(it.progress)
|
||||
animatedFraction.snapTo(interpolated)
|
||||
seekToFraction(backStackEntry, prev)
|
||||
}
|
||||
navController.popBackStack()
|
||||
} catch (e: CancellationException) {
|
||||
withContext(NonCancellable) {
|
||||
animatedFraction.snapTo(0f)
|
||||
transition.setPlaytimeAfterInitialAndTargetStateEstablished(
|
||||
backStackEntry,
|
||||
backStackEntry,
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
transition
|
||||
} else {
|
||||
BackHandler(currentBackStack.size > 1) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
updateTransition(backStackEntry, label = "entry")
|
||||
}
|
||||
|
||||
transition.AnimatedContent(
|
||||
modifier,
|
||||
transitionSpec = {
|
||||
// If the initialState of the AnimatedContent is not in visibleEntries, we are in
|
||||
// a case where visible has cleared the old state for some reason, so instead of
|
||||
// attempting to animate away from the initialState, we skip the animation.
|
||||
if (initialState in visibleEntries) {
|
||||
val initialZIndex = zIndices[initialState.id]
|
||||
?: 0f.also { zIndices[initialState.id] = 0f }
|
||||
val targetZIndex = when {
|
||||
targetState.id == initialState.id -> initialZIndex
|
||||
composeNavigator.isPop.value -> initialZIndex - 1f
|
||||
else -> initialZIndex + 1f
|
||||
}.also { zIndices[targetState.id] = it }
|
||||
|
||||
ContentTransform(finalEnter(this), finalExit(this), targetZIndex)
|
||||
} else {
|
||||
EnterTransition.None togetherWith ExitTransition.None
|
||||
}
|
||||
},
|
||||
contentAlignment,
|
||||
contentKey = { it.id },
|
||||
) {
|
||||
// while in the scope of the composable, we provide the navBackStackEntry as the
|
||||
// ViewModelStoreOwner and LifecycleOwner
|
||||
it.LocalOwnersProvider(saveableStateHolder) {
|
||||
(it.destination as ComposeNavigator.Destination)
|
||||
.content(this, it)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(transition.currentState, transition.targetState) {
|
||||
if (transition.currentState == transition.targetState) {
|
||||
visibleEntries.forEach { entry ->
|
||||
composeNavigator.onTransitionComplete(entry)
|
||||
}
|
||||
zIndices
|
||||
.filter { it.key != transition.targetState.id }
|
||||
.forEach { zIndices.remove(it.key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dialogNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
|
||||
DialogNavigator.NAME,
|
||||
) as? DialogNavigator ?: return
|
||||
|
||||
// Show any dialog destinations
|
||||
DialogHost(dialogNavigator)
|
||||
}
|
||||
|
||||
private fun NavDestination.createEnterTransition(
|
||||
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
|
||||
): EnterTransition? = when (this) {
|
||||
is ComposeNavigator.Destination -> this.enterTransition?.invoke(scope)
|
||||
is ComposeNavGraphNavigator.ComposeNavGraph -> this.enterTransition?.invoke(scope)
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun NavDestination.createExitTransition(
|
||||
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
|
||||
): ExitTransition? = when (this) {
|
||||
is ComposeNavigator.Destination -> this.exitTransition?.invoke(scope)
|
||||
is ComposeNavGraphNavigator.ComposeNavGraph -> this.exitTransition?.invoke(scope)
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun NavDestination.createPopEnterTransition(
|
||||
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
|
||||
): EnterTransition? = when (this) {
|
||||
is ComposeNavigator.Destination -> this.popEnterTransition?.invoke(scope)
|
||||
is ComposeNavGraphNavigator.ComposeNavGraph -> this.popEnterTransition?.invoke(scope)
|
||||
else -> null
|
||||
}
|
||||
private fun NavDestination.createPopExitTransition(
|
||||
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
|
||||
): ExitTransition? = when (this) {
|
||||
is ComposeNavigator.Destination -> this.popExitTransition?.invoke(scope)
|
||||
is ComposeNavGraphNavigator.ComposeNavGraph -> this.popExitTransition?.invoke(scope)
|
||||
else -> null
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.Navigator
|
||||
|
||||
/**
|
||||
* Gets the current navigation back stack entry as a [MutableState]. When the given navController
|
||||
* changes the back stack due to a [NavController.navigate] or [NavController.popBackStack] this
|
||||
* will trigger a recompose and return the top entry on the back stack.
|
||||
*
|
||||
* @return a mutable state of the current back stack entry
|
||||
*/
|
||||
@Composable
|
||||
public fun NavController.currentBackStackEntryAsState(): State<NavBackStackEntry?> {
|
||||
return currentBackStackEntryFlow.collectAsState(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a NavHostController that handles the adding of the [ComposeNavigator] and
|
||||
* [DialogNavigator]. Additional [Navigator] instances can be passed through [navigators] to
|
||||
* be applied to the returned NavController. Note that each [Navigator] must be separately
|
||||
* remembered before being passed in here: any changes to those inputs will cause the
|
||||
* NavController to be recreated.
|
||||
*
|
||||
* @see NavHost
|
||||
*/
|
||||
@Composable
|
||||
public fun rememberNavController(
|
||||
vararg navigators: Navigator<out NavDestination>,
|
||||
): NavHostController {
|
||||
val context = LocalContext.current
|
||||
return rememberSaveable(inputs = navigators, saver = NavControllerSaver(context)) {
|
||||
createNavController(context)
|
||||
}.apply {
|
||||
for (navigator in navigators) {
|
||||
navigatorProvider.addNavigator(navigator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNavController(context: Context) =
|
||||
NavHostController(context).apply {
|
||||
navigatorProvider.addNavigator(ComposeNavGraphNavigator(navigatorProvider))
|
||||
navigatorProvider.addNavigator(ComposeNavigator())
|
||||
navigatorProvider.addNavigator(DialogNavigator())
|
||||
}
|
||||
|
||||
/**
|
||||
* Saver to save and restore the NavController across config change and process death.
|
||||
*/
|
||||
private fun NavControllerSaver(
|
||||
context: Context,
|
||||
): Saver<NavHostController, *> = Saver<NavHostController, Bundle>(
|
||||
save = { it.saveState() },
|
||||
restore = { createNavController(context).apply { restoreState(it) } },
|
||||
)
|
||||
+10
-27
@@ -145,7 +145,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
|
||||
// Min scale allowed (prevent infinite zoom)
|
||||
private var minScale = minScale()
|
||||
private val sRegion = Rect(0, 0, 0, 0)
|
||||
private var srcRect = Rect(0, 0, 0, 0)
|
||||
|
||||
// Is two-finger zooming in progress
|
||||
private var isZooming = false
|
||||
@@ -230,8 +230,9 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
*
|
||||
* @param bitmap Image source.
|
||||
*/
|
||||
fun setImage(bitmap: Bitmap) {
|
||||
fun setImage(bitmap: Bitmap, rect: Rect) {
|
||||
reset(true)
|
||||
srcRect = Rect(rect)
|
||||
onImageLoaded(bitmap)
|
||||
}
|
||||
|
||||
@@ -267,7 +268,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
}
|
||||
sWidth = 0
|
||||
sHeight = 0
|
||||
sRegion.setEmpty()
|
||||
srcRect.setEmpty()
|
||||
isReady = false
|
||||
isImageLoaded = false
|
||||
bitmap = null
|
||||
@@ -760,6 +761,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
val xScale = scale
|
||||
val yScale = scale
|
||||
mtrx.reset()
|
||||
mtrx.postTranslate((-srcRect.left).toFloat(), (-srcRect.top).toFloat())
|
||||
mtrx.postScale(xScale, yScale)
|
||||
mtrx.postTranslate(vTranslate!!.x, vTranslate!!.y)
|
||||
canvas.drawBitmap(bitmap!!, mtrx, bitmapPaint)
|
||||
@@ -908,7 +910,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
@Synchronized
|
||||
private fun onImageLoaded(bitmap: Bitmap) {
|
||||
// If actual dimensions don't match the declared size, reset everything.
|
||||
if (sWidth > 0 && sHeight > 0 && (sWidth != bitmap.width || sHeight != bitmap.height)) {
|
||||
if (sWidth > 0 && sHeight > 0 && (sWidth != srcRect.width() || sHeight != srcRect.height())) {
|
||||
reset(false)
|
||||
}
|
||||
if (!this.bitmapIsCached) {
|
||||
@@ -916,8 +918,8 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
}
|
||||
this.bitmapIsCached = true
|
||||
this.bitmap = bitmap
|
||||
sWidth = bitmap.width
|
||||
sHeight = bitmap.height
|
||||
sWidth = srcRect.width()
|
||||
sHeight = srcRect.height()
|
||||
val ready = checkReady()
|
||||
val imageLoaded = checkImageLoaded()
|
||||
if (ready || imageLoaded) {
|
||||
@@ -1334,8 +1336,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener allowing notification of load and error events. Extend [DefaultOnImageEventListener]
|
||||
* to simplify implementation.
|
||||
* Add a listener allowing notification of load and error events.
|
||||
*
|
||||
* @param onImageEventListener an [OnImageEventListener] instance.
|
||||
*/
|
||||
@@ -1389,7 +1390,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
/**
|
||||
* An event listener, allowing subclasses and activities to be notified of significant events.
|
||||
*/
|
||||
interface OnImageEventListener {
|
||||
fun interface OnImageEventListener {
|
||||
/**
|
||||
* Called when the dimensions of the image and view are known, and either a preview image,
|
||||
* the full size image, or base layer tiles are loaded. This indicates the scale and translate
|
||||
@@ -1397,16 +1398,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* graphic, or inform a subclass that it is safe to draw overlays.
|
||||
*/
|
||||
fun onReady()
|
||||
|
||||
/**
|
||||
* Indicates an error initiliasing the decoder when using a tiling, or when loading the full
|
||||
* size bitmap when tiling is disabled. This method cannot be relied upon; certain encoding
|
||||
* types of supported image formats can result in corrupt or blank images being loaded and
|
||||
* displayed with no detectable error.
|
||||
*
|
||||
* @param e The exception thrown. This error is also logged by the view.
|
||||
*/
|
||||
fun onImageLoadError(e: Exception)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1465,14 +1456,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
|
||||
private data class ScaleAndTranslate(var scale: Float, val vTranslate: PointF)
|
||||
|
||||
/**
|
||||
* Default implementation of [OnImageEventListener] for extension. This does nothing in any method.
|
||||
*/
|
||||
open class DefaultOnImageEventListener : OnImageEventListener {
|
||||
override fun onReady() {}
|
||||
override fun onImageLoadError(e: Exception) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class used to set additional options for a scale animation. Create an instance using [.animateScale],
|
||||
* then set your options and call [.start].
|
||||
|
||||
@@ -17,25 +17,32 @@
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.Context
|
||||
import android.os.StrictMode
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.snapshots.Snapshot
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import coil3.EventListener
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.asCoilImage
|
||||
import coil3.decode.ImageDecoderDecoder
|
||||
import coil3.gif.AnimatedImageDecoder
|
||||
import coil3.gif.GifDecoder
|
||||
import coil3.network.ktor.KtorNetworkFetcherFactory
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import coil3.serviceLoaderEnabled
|
||||
import coil3.util.DebugLogger
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.coil.CropBorderInterceptor
|
||||
import com.hippo.ehviewer.coil.DownloadThumbInterceptor
|
||||
import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
|
||||
import com.hippo.ehviewer.coil.MergeInterceptor
|
||||
import com.hippo.ehviewer.coil.installKtorHttpUriFetcher
|
||||
import com.hippo.ehviewer.dailycheck.checkDawn
|
||||
import com.hippo.ehviewer.dao.SearchDatabase
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
@@ -49,24 +56,18 @@ import com.hippo.ehviewer.util.AppConfig
|
||||
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.isCronetAvailable
|
||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.cookies.HttpCookies
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okhttp3.AsyncDns
|
||||
import okhttp3.android.AndroidAsyncDns
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import logcat.asLog
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.arch.room.roomDb
|
||||
import splitties.init.appCtx
|
||||
@@ -84,6 +85,9 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
AppCompatDelegate.setDefaultNightMode(mode)
|
||||
}
|
||||
}
|
||||
if (!LogcatLogger.isInstalled && Settings.saveCrashLog) {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
}
|
||||
}
|
||||
lifecycle.addObserver(lockObserver)
|
||||
val handler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
@@ -97,51 +101,37 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
handler?.uncaughtException(t, e)
|
||||
}
|
||||
super.onCreate()
|
||||
if (isAtLeastP) {
|
||||
System.loadLibrary("ehviewer")
|
||||
}
|
||||
System.loadLibrary("ehviewer_rust")
|
||||
ReadableTime.initialize(this)
|
||||
System.loadLibrary("ehviewer")
|
||||
lifecycleScope.launchIO {
|
||||
launchIO {
|
||||
launch {
|
||||
EhTagDatabase
|
||||
}
|
||||
launchIO {
|
||||
launch {
|
||||
EhDB
|
||||
}
|
||||
launchIO {
|
||||
DownloadManager.isIdle
|
||||
launch {
|
||||
DownloadManager.readMetadataFromLocal()
|
||||
}
|
||||
launchIO {
|
||||
runSuspendCatching {
|
||||
val files = mutableListOf<File>()
|
||||
AppConfig.externalCrashDir?.listFiles()?.let { files.addAll(it) }
|
||||
AppConfig.externalParseErrorDir?.listFiles()?.let { files.addAll(it) }
|
||||
files.sortByDescending { it.lastModified() }
|
||||
files.forEachIndexed { index, file ->
|
||||
ensureActive()
|
||||
if (index > 9) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
launch {
|
||||
FileUtils.cleanupDirectory(AppConfig.externalCrashDir)
|
||||
FileUtils.cleanupDirectory(AppConfig.externalParseErrorDir)
|
||||
}
|
||||
launchIO {
|
||||
launch {
|
||||
cleanupDownload()
|
||||
}
|
||||
if (Settings.requestNews) {
|
||||
launchIO {
|
||||
launch {
|
||||
checkDawn()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
cleanObsoleteCache()
|
||||
}
|
||||
}
|
||||
cleanObsoleteCache(this)
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.enableDefaults()
|
||||
Snapshot.registerApplyObserver { anies, _ ->
|
||||
logcat(Log.VERBOSE) { anies.toString() }
|
||||
logcat(LogPriority.VERBOSE) { anies.toString() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,12 +140,12 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
runCatching {
|
||||
keepNoMediaFileStatus()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
runCatching {
|
||||
clearTempDir()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,55 +160,48 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
|
||||
galleryDetailCache.evictAll()
|
||||
}
|
||||
}
|
||||
|
||||
override fun newImageLoader() = imageLoader {
|
||||
override fun newImageLoader(context: Context) = context.imageLoader {
|
||||
components {
|
||||
installKtorHttpUriFetcher()
|
||||
if (isAtLeastP) {
|
||||
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
|
||||
}
|
||||
serviceLoaderEnabled(false)
|
||||
add(KtorNetworkFetcherFactory { ktorClient })
|
||||
add(MergeInterceptor)
|
||||
add(DownloadThumbInterceptor)
|
||||
add(CropBorderInterceptor)
|
||||
add(HardwareBitmapInterceptor)
|
||||
if (isAtLeastP) {
|
||||
add(AnimatedImageDecoder.Factory(false))
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
diskCache(imageCache)
|
||||
diskCache { imageCache }
|
||||
crossfade(300)
|
||||
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
||||
if (drawable != null) error(drawable.asCoilImage(true))
|
||||
if (BuildConfig.DEBUG) logger(DebugLogger())
|
||||
if (BuildConfig.DEBUG) {
|
||||
logger(DebugLogger())
|
||||
} else {
|
||||
eventListener(object : EventListener() {
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
logcat("ImageLoader", LogPriority.ERROR) {
|
||||
"🚨 Failed - ${request.data}\n${result.throwable.asLog()}"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ktorClient by lazy {
|
||||
if (Settings.enableCronet && isCronetAvailable) {
|
||||
HttpClient(CronetEngine) {
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HttpClient(OkHttp) {
|
||||
engine {
|
||||
config {
|
||||
if (isAtLeastQ) {
|
||||
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
|
||||
}
|
||||
addInterceptor(UncaughtExceptionInterceptor())
|
||||
}
|
||||
}
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
HttpClient(CronetEngine) {
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val noRedirectKtorClient by lazy {
|
||||
HttpClient(ktorClient.engine) {
|
||||
HttpClient(CronetEngine) {
|
||||
followRedirects = false
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
|
||||
@@ -17,8 +17,6 @@ package com.hippo.ehviewer
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
import arrow.fx.coroutines.release
|
||||
import arrow.fx.coroutines.resource
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
@@ -29,14 +27,20 @@ import com.hippo.ehviewer.dao.EhDatabase
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.ehviewer.dao.HistoryInfo
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||
import com.hippo.ehviewer.dao.ProgressInfo
|
||||
import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.dao.Schema17to18
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.unifile.asUniFile
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import splitties.arch.room.roomDb
|
||||
|
||||
object EhDB {
|
||||
private val db = roomDb<EhDatabase>("eh.db")
|
||||
private const val DB_NAME = "eh.db"
|
||||
private val db = roomDb<EhDatabase>(DB_NAME) {
|
||||
addMigrations(Schema17to18())
|
||||
}
|
||||
|
||||
private suspend fun putGalleryInfo(galleryInfo: BaseGalleryInfo) {
|
||||
db.galleryDao().upsert(galleryInfo)
|
||||
@@ -46,13 +50,22 @@ object EhDB {
|
||||
runCatching { db.galleryDao().delete(galleryInfo) }
|
||||
}
|
||||
|
||||
suspend fun updateGalleryInfo(galleryInfoList: List<BaseGalleryInfo>) {
|
||||
db.galleryDao().update(galleryInfoList)
|
||||
}
|
||||
|
||||
fun getReadProgressFlow(gid: Long) = db.progressDao().getPageFlow(gid)
|
||||
suspend fun getReadProgress(gid: Long) = db.progressDao().getPage(gid)
|
||||
suspend fun putReadProgress(gid: Long, page: Int) = db.progressDao().upsert(ProgressInfo(gid, page))
|
||||
suspend fun clearProgressInfo() = db.progressDao().deleteAll()
|
||||
|
||||
suspend fun getAllDownloadInfo() = db.downloadsDao().joinList().onEach {
|
||||
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
|
||||
it.state = DownloadInfo.STATE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateDownloadInfo(downloadInfo: List<DownloadInfo>) {
|
||||
suspend fun updateDownloadInfo(downloadInfo: Collection<DownloadInfo>) {
|
||||
val dao = db.downloadsDao()
|
||||
dao.update(downloadInfo.map(DownloadInfo::downloadInfo))
|
||||
}
|
||||
@@ -65,17 +78,17 @@ object EhDB {
|
||||
suspend fun removeDownloadInfo(downloadInfo: DownloadInfo) {
|
||||
val dao = db.downloadsDao()
|
||||
dao.delete(downloadInfo.downloadInfo)
|
||||
dao.fill(downloadInfo.position)
|
||||
deleteGalleryInfo(downloadInfo.galleryInfo)
|
||||
}
|
||||
|
||||
suspend fun randomLocalFav() = db.localFavoritesDao().random()
|
||||
|
||||
suspend fun removeDownloadInfo(downloadInfo: List<DownloadInfo>) {
|
||||
val dao = db.downloadsDao()
|
||||
downloadInfo.forEach {
|
||||
dao.delete(it.downloadInfo)
|
||||
deleteGalleryInfo(it.galleryInfo)
|
||||
}
|
||||
dao.fill(downloadInfo.first().position, downloadInfo.size)
|
||||
}
|
||||
|
||||
suspend fun getDownloadDirname(gid: Long): String? {
|
||||
@@ -99,6 +112,9 @@ object EhDB {
|
||||
dao.insertOrIgnore(downloadDirnameList)
|
||||
}
|
||||
|
||||
val downloadsCount
|
||||
get() = db.downloadsDao().count()
|
||||
|
||||
suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list()
|
||||
|
||||
suspend fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
|
||||
@@ -182,13 +198,15 @@ object EhDB {
|
||||
val historyLazyList
|
||||
get() = db.historyDao().joinListLazy()
|
||||
|
||||
fun searchHistory(keyword: String) = db.historyDao().joinListLazy("*$keyword*")
|
||||
|
||||
val localFavLazyList
|
||||
get() = db.localFavoritesDao().joinListLazy()
|
||||
|
||||
val localFavCount: Flow<Int>
|
||||
get() = db.localFavoritesDao().count()
|
||||
|
||||
fun searchLocalFav(keyword: String) = db.localFavoritesDao().joinListLazy("%$keyword%")
|
||||
fun searchLocalFav(keyword: String) = db.localFavoritesDao().joinListLazy("*$keyword*")
|
||||
|
||||
suspend fun putHistoryInfo(galleryInfo: BaseGalleryInfo) {
|
||||
putGalleryInfo(galleryInfo)
|
||||
@@ -242,38 +260,20 @@ object EhDB {
|
||||
db.filterDao().update(filter)
|
||||
}
|
||||
|
||||
suspend fun exportDB(context: Context, uri: Uri) {
|
||||
val ehExportName = "eh.export.db"
|
||||
resource {
|
||||
context.deleteDatabase(ehExportName)
|
||||
roomDb<EhDatabase>(ehExportName)
|
||||
} release {
|
||||
it.close()
|
||||
context.deleteDatabase(ehExportName)
|
||||
} use { newDb ->
|
||||
db.galleryDao().list().let { newDb.galleryDao().insertOrIgnore(it) }
|
||||
db.downloadLabelDao().list().let { newDb.downloadLabelDao().insert(it) }
|
||||
db.downloadDirnameDao().list().let { newDb.downloadDirnameDao().insertOrIgnore(it) }
|
||||
db.downloadsDao().list().let { newDb.downloadsDao().insert(it) }
|
||||
db.historyDao().list().let { newDb.historyDao().insertOrIgnore(it) }
|
||||
db.quickSearchDao().list().let { newDb.quickSearchDao().insert(it) }
|
||||
db.localFavoritesDao().list().let { newDb.localFavoritesDao().insertOrIgnore(it) }
|
||||
db.filterDao().list().let { newDb.filterDao().insert(it) }
|
||||
newDb.close()
|
||||
val dbFile = context.getDatabasePath(ehExportName)
|
||||
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
|
||||
ParcelFileDescriptor.open(dbFile, MODE_READ_ONLY).use { fromFd ->
|
||||
fromFd sendTo toFd
|
||||
}
|
||||
}
|
||||
}
|
||||
fun exportDB(context: Context, uri: Uri) {
|
||||
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
|
||||
val dbFile = context.getDatabasePath(DB_NAME)
|
||||
dbFile.asUniFile() sendTo uri.asUniFile()
|
||||
}
|
||||
|
||||
suspend fun importDB(context: Context, uri: Uri) {
|
||||
val tempDBName = "tmp.db"
|
||||
resource {
|
||||
context.deleteDatabase(tempDBName)
|
||||
roomDb<EhDatabase>(tempDBName) { createFromInputStream { context.contentResolver.openInputStream(uri) } }
|
||||
roomDb<EhDatabase>(tempDBName) {
|
||||
createFromInputStream { context.contentResolver.openInputStream(uri) }
|
||||
addMigrations(Schema17to18())
|
||||
}
|
||||
} release {
|
||||
it.close()
|
||||
context.deleteDatabase(tempDBName)
|
||||
@@ -281,6 +281,9 @@ object EhDB {
|
||||
runCatching {
|
||||
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
|
||||
}
|
||||
runCatching {
|
||||
db.progressDao().insertOrIgnore(oldDB.progressDao().list())
|
||||
}
|
||||
runCatching {
|
||||
val downloadLabelList = oldDB.downloadLabelDao().list()
|
||||
DownloadManager.addDownloadLabel(downloadLabelList)
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 com.hippo.ehviewer.download.SortMode
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import java.util.Locale
|
||||
@@ -108,11 +109,15 @@ object Settings : DataStorePreferences(null) {
|
||||
val thumbColumns = intPref("thumb_columns", 3)
|
||||
val listThumbSize = intPref("list_tile_size", 40)
|
||||
val languageFilter = intPref("language_filter", -1)
|
||||
val downloadSortMode = intPref("download_sort_mode", SortMode.Default.flag)
|
||||
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
|
||||
val predictiveNavAnim = boolPref("predictive_nav_anim", true)
|
||||
val blackDarkTheme = boolPref("black_dark_theme", false)
|
||||
val gridView = boolPref("grid_view", false)
|
||||
val showGalleryPages = boolPref("show_gallery_pages", true)
|
||||
val qSSaveProgress = boolPref("qs_save_progress", true)
|
||||
val security = boolPref("require_unlock", false)
|
||||
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
|
||||
|
||||
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)
|
||||
@@ -134,39 +139,35 @@ object Settings : DataStorePreferences(null) {
|
||||
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 showTagTranslations by boolPref(KEY_SHOW_TAG_TRANSLATIONS, false).observed { updateWhenTagTranslationChanges() }
|
||||
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
|
||||
var enabledSecurity by boolPref("enable_secure", false)
|
||||
var useCIUpdateChannel by boolPref("ci_update_channel", false)
|
||||
var mediaScan by boolPref("media_scan", false).observed { updateWhenKeepMediaStatusChanges() }
|
||||
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
|
||||
var qSSaveProgress by boolPref("qs_save_progress", true)
|
||||
var saveParseErrorBody by boolPref("save_parse_error_body", true)
|
||||
var saveCrashLog by boolPref("save_crash_log", true)
|
||||
var security by boolPref("require_unlock", false)
|
||||
var removeImageFiles by boolPref("include_pic", true)
|
||||
var needSignIn by boolPref("need_sign_in", true).also { needSignInFlow = it.valueFlow() }
|
||||
var needSignIn by boolPref("need_sign_in", true)
|
||||
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 saveAsCbz by boolPref("save_as_cbz", false)
|
||||
var archiveMetadata by boolPref("archive_metadata", true)
|
||||
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
|
||||
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
||||
var securityDelay by intPref("require_unlock_delay", 0)
|
||||
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
|
||||
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 requestNewsTime by intPref("request_news_time", 0).observed { updateWhenRequestNewsChanges() }
|
||||
var updateIntervalDays by intPref("update_interval_days", 7)
|
||||
var lastDawnDays by intPref("last_dawn_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)
|
||||
var avatar by stringOrNullPref("avatar", null)
|
||||
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
|
||||
var lastDawnTime by longPref("last_dawn_time", 0)
|
||||
var lastUpdateTime by longPref("last_update_time", 0)
|
||||
var favDialogTheta by floatPref("fav_select_dialog_delta", 0F)
|
||||
|
||||
@@ -174,6 +175,9 @@ object Settings : DataStorePreferences(null) {
|
||||
var touchSlopFactor by intPref("touch_slop", 3)
|
||||
|
||||
// Tachiyomi Reader
|
||||
var newReader by boolPref("new_compose_reader", false)
|
||||
|
||||
val cropBorder = boolPref("crop_borders", false)
|
||||
val colorFilter = boolPref("pref_color_filter_key", false)
|
||||
val colorFilterValue = intPref("color_filter_value", 0)
|
||||
val colorFilterMode = intPref("color_filter_mode", 0)
|
||||
@@ -182,6 +186,27 @@ object Settings : DataStorePreferences(null) {
|
||||
val readingMode = intPref("pref_default_reading_mode_key", ReadingModeType.LEFT_TO_RIGHT.flagValue)
|
||||
val orientationMode = intPref("pref_default_orientation_type_key", OrientationType.FREE.flagValue)
|
||||
val showReaderSeekbar = boolPref("pref_show_reader_seekbar", true)
|
||||
val showPageNumber = boolPref("pref_show_page_number_key", true)
|
||||
val readerTheme = intPref("pref_reader_theme_key", 1)
|
||||
val doubleTapToZoom = boolPref("pref_double_tap_to_zoom", true)
|
||||
val fullscreen = boolPref("fullscreen", true)
|
||||
val cutoutShort = boolPref("cutout_short", true)
|
||||
val keepScreenOn = boolPref("pref_keep_screen_on_key", true)
|
||||
val readerLongTapAction = boolPref("reader_long_tap", true)
|
||||
val pageTransitions = boolPref("pref_enable_transitions_key", true)
|
||||
val readWithVolumeKeys = boolPref("reader_volume_keys", false)
|
||||
val readWithVolumeKeysInterval = intPref("reader_volume_keys_interval", 0)
|
||||
val readWithVolumeKeysInverted = boolPref("reader_volume_keys_inverted", false)
|
||||
val grayScale = boolPref("pref_grayscale", false)
|
||||
val invertedColors = boolPref("pref_inverted_colors", false)
|
||||
val readerWebtoonNav = intPref("reader_navigation_mode_webtoon", 0)
|
||||
val readerPagerNav = intPref("reader_navigation_mode_pager", 0)
|
||||
val readerWebtoonNavInverted = intPref("reader_tapping_inverted_webtoon", 0)
|
||||
val webtoonSidePadding = intPref("webtoon_side_padding", 0)
|
||||
val navigateToPan = boolPref("navigate_pan", true)
|
||||
val imageScaleType = intPref("pref_image_scale_type_key", 1)
|
||||
val landscapeZoom = boolPref("landscape_zoom", false)
|
||||
val zoomStart = intPref("pref_zoom_start_key", 1)
|
||||
|
||||
init {
|
||||
if ("CN" == Locale.getDefault().country) {
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -68,7 +69,7 @@ fun updateWhenGallerySiteChanges() {
|
||||
runCatching {
|
||||
EhEngine.getUConfig()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +80,7 @@ fun updateWhenTagTranslationChanges() {
|
||||
runCatching {
|
||||
EhTagDatabase.update()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ package com.hippo.ehviewer.client
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastJoinToString
|
||||
import io.ktor.client.plugins.cookies.CookiesStorage
|
||||
import io.ktor.http.Cookie
|
||||
import io.ktor.http.Url
|
||||
import io.ktor.http.parseClientCookiesHeader
|
||||
import io.ktor.http.renderCookieHeader
|
||||
import io.ktor.http.renderSetCookieHeader
|
||||
|
||||
object EhCookieStore : CookiesStorage {
|
||||
@@ -23,7 +23,6 @@ object EhCookieStore : CookiesStorage {
|
||||
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
||||
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
||||
const val KEY_IGNEOUS = "igneous"
|
||||
private const val KEY_STAR = "star"
|
||||
private const val KEY_CONTENT_WARNING = "nw"
|
||||
private const val CONTENT_WARNING_NOT_SHOW = "1"
|
||||
private const val KEY_UTMP_NAME = "__utmp"
|
||||
@@ -32,26 +31,31 @@ object EhCookieStore : CookiesStorage {
|
||||
value = CONTENT_WARNING_NOT_SHOW,
|
||||
)
|
||||
|
||||
fun copyNecessaryCookies() {
|
||||
val cookies = load(Url(EhUrl.HOST_E))
|
||||
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())
|
||||
}
|
||||
}
|
||||
fun clearIgneous() {
|
||||
manager.setCookie(
|
||||
EhUrl.HOST_EX,
|
||||
renderSetCookieHeader(KEY_IGNEOUS, "", maxAge = 0, domain = EhUrl.DOMAIN_EX, path = "/"),
|
||||
)
|
||||
}
|
||||
|
||||
fun addCookie(k: String, v: String, domain: String) {
|
||||
val cookie = Cookie(name = k, value = v, domain = domain, maxAge = Int.MAX_VALUE)
|
||||
val url = if (EhUrl.DOMAIN_E == cookie.domain) EhUrl.HOST_E else EhUrl.HOST_EX
|
||||
manager.setCookie(url, renderSetCookieHeader(cookie))
|
||||
fun getIdentityCookies(): List<Pair<String, String?>> {
|
||||
val eCookies = getCookies(EhUrl.HOST_E)
|
||||
val exCookies = getCookies(EhUrl.HOST_EX)
|
||||
val ipbMemberId = eCookies?.get(KEY_IPB_MEMBER_ID)
|
||||
val ipbPassHash = eCookies?.get(KEY_IPB_PASS_HASH)
|
||||
val igneous = exCookies?.get(KEY_IGNEOUS)
|
||||
return listOf(
|
||||
KEY_IPB_MEMBER_ID to ipbMemberId,
|
||||
KEY_IPB_PASS_HASH to ipbPassHash,
|
||||
KEY_IGNEOUS to igneous,
|
||||
)
|
||||
}
|
||||
|
||||
fun flush() = manager.flush()
|
||||
|
||||
fun getCookieHeader(url: String): String {
|
||||
return load(Url(url)).fastJoinToString("; ") {
|
||||
"${it.name}=${it.value}"
|
||||
renderCookieHeader(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,18 +68,16 @@ object EhCookieStore : CookiesStorage {
|
||||
|
||||
override fun close() = Unit
|
||||
|
||||
private fun getCookies(url: String) = manager.getCookie(url)?.let { parseClientCookiesHeader(it) }
|
||||
|
||||
fun load(url: Url): List<Cookie> {
|
||||
val cookies = manager.getCookie(url.toString())
|
||||
val checkTips = EhUrl.DOMAIN_E in url.host
|
||||
return cookies?.let {
|
||||
parseClientCookiesHeader(cookies)
|
||||
.map { Cookie(it.key, it.value, maxAge = Int.MAX_VALUE) }
|
||||
.filterTo(mutableListOf()) { it.name != KEY_UTMP_NAME }.apply {
|
||||
if (checkTips) {
|
||||
removeAll { it.name == KEY_CONTENT_WARNING }
|
||||
add(sTipsCookie)
|
||||
}
|
||||
}
|
||||
return getCookies(url.toString())?.mapTo(mutableListOf()) {
|
||||
Cookie(it.key, it.value)
|
||||
}?.apply {
|
||||
if (checkTips) {
|
||||
add(sTipsCookie)
|
||||
}
|
||||
} ?: if (checkTips) listOf(sTipsCookie) else emptyList()
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.fx.coroutines.parMap
|
||||
import arrow.fx.coroutines.parZip
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
@@ -31,49 +32,46 @@ import com.hippo.ehviewer.client.exception.NotLoggedInException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.parser.ArchiveParser
|
||||
import com.hippo.ehviewer.client.parser.EventPaneParser
|
||||
import com.hippo.ehviewer.client.parser.FavParserResult
|
||||
import com.hippo.ehviewer.client.parser.FavoritesParser
|
||||
import com.hippo.ehviewer.client.parser.ForumsParser
|
||||
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.GalleryListResult
|
||||
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.client.parser.ProfileParser
|
||||
import com.hippo.ehviewer.client.parser.RateGalleryResult
|
||||
import com.hippo.ehviewer.client.parser.SignInParser
|
||||
import com.hippo.ehviewer.client.parser.TorrentParser
|
||||
import com.hippo.ehviewer.client.parser.TorrentResult
|
||||
import com.hippo.ehviewer.client.parser.VoteCommentResult
|
||||
import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||
import com.hippo.ehviewer.dailycheck.showEventNotification
|
||||
import com.hippo.ehviewer.dailycheck.today
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.StatusCodeException
|
||||
import io.ktor.client.request.forms.append
|
||||
import com.hippo.ehviewer.util.bodyAsUtf8Text
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.statement.HttpStatement
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.utils.io.core.writeFully
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import io.ktor.utils.io.pool.useInstance
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import kotlin.math.ceil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
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 okio.buffer
|
||||
import okio.sink
|
||||
import org.jsoup.Jsoup
|
||||
import splitties.init.appCtx
|
||||
|
||||
@@ -87,11 +85,11 @@ private const val U_CONFIG_TEXT = "Selected Profile"
|
||||
fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
|
||||
val dir = AppConfig.externalParseErrorDir ?: return
|
||||
val file = File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt")
|
||||
RandomAccessFile(file, "rw").use { randomAccessFile ->
|
||||
with(randomAccessFile) {
|
||||
writeUTF(e.message + "\n")
|
||||
onLeft { writeUTF(it) }
|
||||
onRight { channel.write(it) }
|
||||
file.sink().buffer().use { sink ->
|
||||
with(sink) {
|
||||
writeUtf8(e.message + "\n")
|
||||
onLeft { writeUtf8(it) }
|
||||
onRight { write(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +98,7 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
// Don't translate coroutine cancellation
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
// Check sad panda(without panda)
|
||||
// Check sad panda (without panda)
|
||||
val empty = body.fold(
|
||||
{ it.isEmpty() },
|
||||
{ !it.hasRemaining() },
|
||||
@@ -130,6 +128,12 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
|
||||
if (e is ParseException || e is SerializationException) {
|
||||
body.onLeft { if ("<" !in it) throw EhException(it) }
|
||||
when (val message = e.cause?.message) {
|
||||
"No hits found!" -> throw EhException(R.string.gallery_list_empty_hit)
|
||||
"No watched tags!" -> throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
"Not logged in!" -> throw NotLoggedInException()
|
||||
is String -> if (message.startsWith("Your IP address")) throw EhException(message)
|
||||
}
|
||||
if (Settings.saveParseErrorBody) body.saveParseError(e)
|
||||
throw EhException(appCtx.getString(R.string.error_parse_error), e)
|
||||
}
|
||||
@@ -140,8 +144,8 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
|
||||
val httpContentPool = DirectByteBufferPool(8, 0x80000)
|
||||
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = execute { response ->
|
||||
val body = response.bodyAsText()
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = executeSafely { response ->
|
||||
val body = response.bodyAsUtf8Text()
|
||||
runSuspendCatching {
|
||||
block(body)
|
||||
}.onFailure {
|
||||
@@ -149,30 +153,31 @@ suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = execute { response ->
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = executeSafely { response ->
|
||||
httpContentPool.useInstance { buffer ->
|
||||
with(response.bodyAsChannel()) { while (!isClosedForRead) readAvailable(buffer) }
|
||||
buffer.flip()
|
||||
runSuspendCatching {
|
||||
block(buffer)
|
||||
}.onFailure {
|
||||
buffer.rewind()
|
||||
rethrowExactly(response.status.value, buffer.right(), it)
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
object EhEngine {
|
||||
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).execute { response ->
|
||||
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
|
||||
val location = response.headers["Location"] ?: throw InsufficientFundsException()
|
||||
location.takeIf { "bounce_login" !in it } ?: throw NotLoggedInException()
|
||||
}
|
||||
|
||||
suspend fun getTorrentList(url: String, gid: Long, token: String?): TorrentResult {
|
||||
suspend fun getTorrentList(url: String, gid: Long, token: String): TorrentResult {
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
return ehRequest(url, referer).fetchUsingAsByteBuffer(TorrentParser::parse)
|
||||
}
|
||||
|
||||
suspend fun getArchiveList(url: String, gid: Long, token: String?) = ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
|
||||
suspend fun getArchiveList(url: String, gid: Long, token: String) = ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
|
||||
.fetchUsingAsText(ArchiveParser::parse)
|
||||
.apply { funds = funds ?: ehRequest(EhUrl.URL_FUNDS).fetchUsingAsText(HomeParser::parseFunds) }
|
||||
|
||||
@@ -196,7 +201,7 @@ object EhEngine {
|
||||
}.onFailure { throwable ->
|
||||
// It may get redirected when accessing ex for the first time
|
||||
if (url == EhUrl.URL_UCONFIG_EX) {
|
||||
throwable.printStackTrace()
|
||||
logcat(throwable)
|
||||
ehRequest(url).fetchUsingAsText { check(U_CONFIG_TEXT in this) { "Unable to load config from $url!" } }
|
||||
} else {
|
||||
throw throwable
|
||||
@@ -204,7 +209,7 @@ object EhEngine {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGalleryPage(url: String, gid: Long, token: String?): GalleryPageParser.Result {
|
||||
suspend fun getGalleryPage(url: String, gid: Long, token: String): GalleryPageParser.Result {
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
return ehRequest(url, referer).fetchUsingAsText(GalleryPageParser::parse)
|
||||
}
|
||||
@@ -216,35 +221,19 @@ object EhEngine {
|
||||
suspend fun getGalleryDetail(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsText {
|
||||
val eventPane = EventPaneParser.parse(this)
|
||||
if (eventPane != null) {
|
||||
Settings.lastDawnTime = Clock.System.now().epochSeconds
|
||||
Settings.lastDawnDays = today
|
||||
showEventNotification(eventPane)
|
||||
}
|
||||
GalleryDetailParser.parse(this)
|
||||
}
|
||||
|
||||
suspend fun getPreviewList(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsText {
|
||||
GalleryDetailParser.parsePreviewList(this) to GalleryDetailParser.parsePreviewPages(this)
|
||||
GalleryDetailParser.parsePreviewList(this)
|
||||
}
|
||||
|
||||
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsByteBuffer(FavoritesParser::parse)
|
||||
.apply { galleryInfoList.fillInfo(url) }
|
||||
|
||||
suspend fun signIn(username: String, password: String): String {
|
||||
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
|
||||
val url = EhUrl.API_SIGN_IN
|
||||
val origin = "https://forums.e-hentai.org"
|
||||
return ehRequest(url, referer, origin) {
|
||||
formBody {
|
||||
append("referer", referer)
|
||||
append("b", "")
|
||||
append("bt", "")
|
||||
append("UserName", username)
|
||||
append("PassWord", password)
|
||||
append("CookieDate", "1")
|
||||
}
|
||||
}.fetchUsingAsText(SignInParser::parse)
|
||||
}
|
||||
|
||||
suspend fun commentGallery(url: String, comment: String, id: Long = -1) = ehRequest(url, url, EhUrl.origin) {
|
||||
formBody {
|
||||
if (id == -1L) {
|
||||
@@ -254,16 +243,21 @@ object EhEngine {
|
||||
append("edit_comment", id.toString())
|
||||
}
|
||||
}
|
||||
}.fetchUsingAsText {
|
||||
val document = Jsoup.parse(this)
|
||||
val elements = document.select("#chd + p")
|
||||
if (elements.size > 0) {
|
||||
throw EhException(elements[0].text())
|
||||
}.executeSafely { response ->
|
||||
// Ktor does not handle POST redirect, we need to do it manually
|
||||
// https://youtrack.jetbrains.com/issue/KTOR-478
|
||||
val location = response.headers["Location"] ?: url
|
||||
ehRequest(location, url).fetchUsingAsText {
|
||||
val document = Jsoup.parse(this)
|
||||
val elements = document.select("#chd + p")
|
||||
if (elements.size > 0) {
|
||||
throw EhException(elements[0].text())
|
||||
}
|
||||
GalleryDetailParser.parseComments(document)
|
||||
}
|
||||
GalleryDetailParser.parseComments(document)
|
||||
}
|
||||
|
||||
suspend fun modifyFavorites(gid: Long, token: String?, dstCat: Int = -1, note: String = "") {
|
||||
suspend fun modifyFavorites(gid: Long, token: String, dstCat: Int = -1, note: String = "") {
|
||||
val catStr: String = when (dstCat) {
|
||||
-1 -> "favdel"
|
||||
in 0..9 -> dstCat.toString()
|
||||
@@ -278,12 +272,10 @@ object EhEngine {
|
||||
append("apply", "Apply Changes")
|
||||
append("update", "1")
|
||||
}
|
||||
}.execute { }
|
||||
}.executeSafely { }
|
||||
}
|
||||
|
||||
suspend fun downloadArchive(gid: Long, token: String?, or: String?, res: String?, isHAtH: Boolean): String? {
|
||||
if (or.isNullOrEmpty()) throw EhException("Invalid form param or: $or")
|
||||
if (res.isNullOrEmpty()) throw EhException("Invalid res: $res")
|
||||
suspend fun downloadArchive(gid: Long, token: String, or: String, res: String, isHAtH: Boolean): String? {
|
||||
val url = EhUrl.getDownloadArchive(gid, token, or)
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
val request = ehRequest(url, referer, EhUrl.origin) {
|
||||
@@ -319,7 +311,7 @@ object EhEngine {
|
||||
}
|
||||
}.fetchUsingAsText(HomeParser::parseResetLimits)
|
||||
|
||||
suspend fun modifyFavorites(gidArray: LongArray, srcCat: Int, dstCat: Int): FavoritesParser.Result {
|
||||
suspend fun modifyFavorites(gidArray: LongArray, srcCat: Int, dstCat: Int): FavParserResult {
|
||||
val url = ehUrl(EhUrl.FAV_PATH) {
|
||||
if (FavListUrlBuilder.isValidFavCat(srcCat)) addQueryParameter("favcat", srcCat.toString())
|
||||
}.buildString()
|
||||
@@ -349,18 +341,18 @@ object EhEngine {
|
||||
}.fetchUsingAsText { GalleryPageParser.parse(filterNot { it == '\\' }) }
|
||||
}
|
||||
|
||||
suspend fun rateGallery(apiUid: Long, apiKey: String?, gid: Long, token: String?, rating: Float): RateGalleryResult = ehRequest(EhUrl.apiUrl, EhUrl.getGalleryDetailUrl(gid, token), EhUrl.origin) {
|
||||
suspend fun rateGallery(apiUid: Long, apiKey: String?, gid: Long, token: String, rating: Float): RateGalleryResult = ehRequest(EhUrl.apiUrl, EhUrl.getGalleryDetailUrl(gid, token), EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "rategallery")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", apiKey)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("rating", ceil((rating * 2).toDouble()).toInt())
|
||||
}
|
||||
}.fetchUsingAsText(String::parseAs)
|
||||
|
||||
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String) =
|
||||
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String? = null) =
|
||||
galleryInfoList.chunked(MAX_REQUEST_SIZE).chunked(MAX_SEQUENTIAL_REQUESTS).forEachIndexed { index, chunk ->
|
||||
if (index != 0) {
|
||||
delay(REQUEST_INTERVAL)
|
||||
@@ -383,12 +375,12 @@ object EhEngine {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun voteComment(apiUid: Long, apiKey: String?, gid: Long, token: String?, commentId: Long, commentVote: Int): VoteCommentResult =
|
||||
suspend fun voteComment(apiUid: Long, apiKey: String?, gid: Long, token: String, commentId: Long, commentVote: Int): VoteCommentResult =
|
||||
ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "votecomment")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", apiKey)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("comment_id", commentId)
|
||||
@@ -396,12 +388,12 @@ object EhEngine {
|
||||
}
|
||||
}.fetchUsingAsText(String::parseAs)
|
||||
|
||||
suspend fun voteTag(apiUid: Long, apiKey: String?, gid: Long, token: String?, tags: String?, vote: Int) =
|
||||
suspend fun voteTag(apiUid: Long, apiKey: String?, gid: Long, token: String, tags: String, vote: Int) =
|
||||
ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "taggallery")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", apiKey)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("tags", tags)
|
||||
@@ -409,7 +401,7 @@ object EhEngine {
|
||||
}
|
||||
}.fetchUsingAsText(VoteTagParser::parse)
|
||||
|
||||
suspend fun getGalleryToken(gid: Long, gtoken: String?, page: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
suspend fun getGalleryToken(gid: Long, gtoken: String, page: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "gtoken")
|
||||
array("pagelist") {
|
||||
@@ -422,32 +414,20 @@ object EhEngine {
|
||||
}
|
||||
}.fetchUsingAsText(GalleryTokenApiParser::parse)
|
||||
|
||||
suspend fun imageSearch(jpeg: File, uss: Boolean, osc: Boolean): GalleryListResult {
|
||||
val location = noRedirectEhRequest(EhUrl.imageSearchUrl, EhUrl.referer, EhUrl.origin) {
|
||||
multipartBody {
|
||||
append("sfile", "a.jpg", ContentType.Image.JPEG, jpeg.length()) {
|
||||
RandomAccessFile(jpeg, "r").use {
|
||||
writeFully(it.channel.map(FileChannel.MapMode.READ_ONLY, 0, it.length()))
|
||||
}
|
||||
}
|
||||
if (uss) append("fs_similar", "on")
|
||||
if (osc) append("fs_covers", "on")
|
||||
append("f_sfile", "File Search")
|
||||
}
|
||||
}.execute { it.headers["Location"] ?: error("Failed to search image!!!") }
|
||||
return ehRequest(location).fetchUsingAsByteBuffer(GalleryListParser::parse).apply {
|
||||
galleryInfoList.fillInfo(EhUrl.imageSearchUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<BaseGalleryInfo>.fillInfo(url: String, filter: Boolean = false) = with(EhFilter) {
|
||||
if (filter) removeAllSuspend { filterTitle(it) || filterUploader(it) }
|
||||
val hasTags = any { !it.simpleTags.isNullOrEmpty() }
|
||||
val hasPages = any { it.pages != 0 }
|
||||
val hasRated = any { it.rated }
|
||||
val needApi = filter && needTags() && !hasTags || Settings.showGalleryPages && !hasPages || hasRated
|
||||
val needApi = filter && needTags() && !hasTags || Settings.showGalleryPages.value && !hasPages || hasRated
|
||||
if (needApi) fillGalleryListByApi(this@fillInfo, url)
|
||||
if (filter) removeAllSuspend { filterUploader(it) || filterTag(it) || filterTagNamespace(it) }
|
||||
forEach {
|
||||
if (it.favoriteSlot == GalleryInfo.NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
|
||||
it.favoriteSlot = GalleryInfo.LOCAL_FAVORITED
|
||||
}
|
||||
if (!needApi) it.generateSLang()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addFavorites(galleryList: List<Pair<Long, String>>, dstCat: Int) {
|
||||
|
||||
@@ -18,23 +18,22 @@ package com.hippo.ehviewer.client
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.util.bodyAsUtf8Text
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.forms.FormBuilder
|
||||
import io.ktor.client.request.forms.FormDataContent
|
||||
import io.ktor.client.request.forms.MultiPartFormDataContent
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.prepareRequest
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.HttpStatement
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.ParametersBuilder
|
||||
import io.ktor.http.content.TextContent
|
||||
import io.ktor.http.userAgent
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArrayBuilder
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
@@ -45,7 +44,7 @@ import okio.BufferedSource
|
||||
|
||||
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
|
||||
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = execute { it.bodyAsText().parseAs<T>() }
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = executeSafely { it.bodyAsUtf8Text().parseAs<T>() }
|
||||
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
|
||||
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
|
||||
|
||||
@@ -85,6 +84,16 @@ suspend inline fun noRedirectEhRequest(
|
||||
apply(builder)
|
||||
}.also { logcat("EhRequest") { url } }
|
||||
|
||||
suspend inline fun <T> HttpStatement.executeSafely(
|
||||
crossinline block: suspend (response: HttpResponse) -> T,
|
||||
) = execute { resp ->
|
||||
try {
|
||||
block(resp)
|
||||
} finally {
|
||||
resp.coroutineContext.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun HttpRequestBuilder.formBody(builder: ParametersBuilder.() -> Unit) {
|
||||
method = HttpMethod.Post
|
||||
val parameters = ParametersBuilder().apply(builder).build()
|
||||
@@ -96,8 +105,3 @@ inline fun HttpRequestBuilder.jsonBody(builder: JsonObjectBuilder.() -> Unit) {
|
||||
val json = buildJsonObject(builder).toString()
|
||||
setBody(TextContent(text = json, contentType = ContentType.Application.Json))
|
||||
}
|
||||
|
||||
fun HttpRequestBuilder.multipartBody(builder: FormBuilder.() -> Unit) {
|
||||
method = HttpMethod.Post
|
||||
setBody(MultiPartFormDataContent(formData(builder)))
|
||||
}
|
||||
@@ -21,10 +21,14 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.TagNamespace
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.StatusCodeException
|
||||
import com.hippo.ehviewer.util.copyTo
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.sha1
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.prepareGet
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
@@ -41,8 +45,6 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.BufferedSource
|
||||
import okio.HashingSink.Companion.sha1
|
||||
import okio.blackholeSink
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import splitties.init.appCtx
|
||||
@@ -62,7 +64,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
var initialized by mutableStateOf(false)
|
||||
|
||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
|
||||
return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
|
||||
return tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
|
||||
}
|
||||
|
||||
private fun internalSuggestFlow(
|
||||
@@ -105,7 +107,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
val prefix = keyword.dropLast(mKeyword.length)
|
||||
val namespace = mKeyword.substringBefore(':')
|
||||
val tagKeyword = mKeyword.drop(namespace.length + 1)
|
||||
val namespacePrefix = namespaceToPrefix(namespace) ?: namespace
|
||||
val namespacePrefix = TagNamespace(namespace).toPrefix() ?: namespace
|
||||
val tags = tagGroups[namespacePrefix.takeIf { tagKeyword.isNotEmpty() && it != NAMESPACE_PREFIX }]
|
||||
tags?.let {
|
||||
internalSuggestFlow(it, tagKeyword, translate, exactly).collect { (hint, tag) ->
|
||||
@@ -129,24 +131,6 @@ object EhTagDatabase : CoroutineScope {
|
||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
||||
removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||
|
||||
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
|
||||
it["artist"] = "a"
|
||||
it["cosplayer"] = "cos"
|
||||
it["character"] = "c"
|
||||
it["female"] = "f"
|
||||
it["group"] = "g"
|
||||
it["language"] = "l"
|
||||
it["male"] = "m"
|
||||
it["mixed"] = "x"
|
||||
it["other"] = "o"
|
||||
it["parody"] = "p"
|
||||
it["reclass"] = "r"
|
||||
}
|
||||
|
||||
fun namespaceToPrefix(namespace: String): String? {
|
||||
return NAMESPACE_TO_PREFIX[namespace]
|
||||
}
|
||||
|
||||
private fun getMetadata(context: Context): Array<String>? {
|
||||
return context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
.takeIf { it.size == 4 }
|
||||
@@ -162,24 +146,13 @@ object EhTagDatabase : CoroutineScope {
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun getFileSha1(file: File): String? {
|
||||
return runCatching {
|
||||
file.source().buffer().use { source ->
|
||||
sha1(blackholeSink()).use {
|
||||
source.readAll(it)
|
||||
it.hash.hex()
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun checkData(sha1: String?, data: File): Boolean {
|
||||
return sha1 != null && sha1 == getFileSha1(data)
|
||||
return sha1 != null && sha1 == data.asUniFile().sha1()
|
||||
}
|
||||
|
||||
private suspend fun save(client: HttpClient, url: String, file: File) {
|
||||
runCatching {
|
||||
client.prepareGet(url).execute {
|
||||
client.prepareGet(url).executeSafely {
|
||||
if (it.status.isSuccess()) {
|
||||
it.bodyAsChannel().copyTo(file)
|
||||
} else {
|
||||
@@ -213,7 +186,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
runSuspendCatching {
|
||||
issueUpdateInMemoryData()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
updateInternal()
|
||||
}
|
||||
@@ -272,7 +245,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
// Read new EhTagDatabase
|
||||
issueUpdateInMemoryData(dataFile)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ object EhUrl {
|
||||
const val API_E = "https://api.e-hentai.org/api.php"
|
||||
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
|
||||
const val URL_MY_TAGS_E = HOST_E + "mytags"
|
||||
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
|
||||
const val URL_POPULAR_E = "https://e-hentai.org/popular"
|
||||
const val URL_POPULAR_EX = "https://exhentai.org/popular"
|
||||
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
|
||||
@@ -52,18 +51,11 @@ object EhUrl {
|
||||
const val REFERER_E = "https://$DOMAIN_E"
|
||||
const val ORIGIN_E = REFERER_E
|
||||
|
||||
val host: String
|
||||
get() = when (Settings.gallerySite) {
|
||||
SITE_E -> HOST_E
|
||||
SITE_EX -> HOST_EX
|
||||
else -> HOST_E
|
||||
}
|
||||
|
||||
val domain: String
|
||||
get() = when (Settings.gallerySite) {
|
||||
SITE_E -> DOMAIN_E
|
||||
SITE_EX -> DOMAIN_EX
|
||||
else -> HOST_E
|
||||
else -> DOMAIN_E
|
||||
}
|
||||
|
||||
val apiUrl: String
|
||||
@@ -119,39 +111,48 @@ object EhUrl {
|
||||
else -> URL_IMAGE_SEARCH_E
|
||||
}
|
||||
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?): String {
|
||||
return getGalleryDetailUrl(gid, token, 0, false)
|
||||
}
|
||||
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) =
|
||||
ehUrl(listOf("g", "$gid", token)) {
|
||||
if (index != 0) addQueryParameter("p", "$index")
|
||||
if (allComment) addQueryParameter("hc", "1")
|
||||
}.buildString()
|
||||
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean) = ehUrl(listOf("g", gid.toString(), token.orEmpty())) {
|
||||
if (index != 0) addQueryParameter("p", "$index")
|
||||
if (allComment) addQueryParameter("hc", "1")
|
||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", "$gid", token)).buildString()
|
||||
|
||||
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(listOf("s", pToken, "$gid-${index + 1}")) {
|
||||
addQueryParameterIfNotBlank("nl", nl)
|
||||
}.buildString()
|
||||
|
||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", gid.toString(), token)).buildString()
|
||||
fun getAddFavorites(gid: Long, token: String) = ehUrl("gallerypopups.php") {
|
||||
addQueryParameter("gid", "$gid")
|
||||
addQueryParameter("t", token)
|
||||
addQueryParameter("act", "addfav")
|
||||
}.buildString()
|
||||
|
||||
fun getPageUrl(gid: Long, index: Int, pToken: String) = host + "s/" + pToken + '/' + gid + '-' + (index + 1)
|
||||
|
||||
fun getAddFavorites(gid: Long, token: String?) = host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
|
||||
|
||||
fun getDownloadArchive(gid: Long, token: String?, or: String) = host + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or
|
||||
fun getDownloadArchive(gid: Long, token: String, or: String) = ehUrl("archiver.php") {
|
||||
addQueryParameter("gid", "$gid")
|
||||
addQueryParameter("token", token)
|
||||
addQueryParameter("or", or)
|
||||
}.buildString()
|
||||
|
||||
fun getTagDefinitionUrl(tag: String) = "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
|
||||
}
|
||||
|
||||
inline fun ehUrl(path: String?, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||
path = if (path.isNullOrBlank()) emptyList() else listOf(path),
|
||||
host = host,
|
||||
parameters = Parameters.build(builder),
|
||||
)
|
||||
|
||||
inline fun ehUrl(path: List<String>, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||
inline fun ehUrl(path: List<String>, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||
path = path,
|
||||
host = host,
|
||||
parameters = Parameters.build(builder),
|
||||
)
|
||||
|
||||
fun ehUrl(path: List<String>, parameters: Parameters = Parameters.Empty) = URLBuilder(
|
||||
fun ehUrl(path: List<String>, host: String = EhUrl.domain, parameters: Parameters = Parameters.Empty) = URLBuilder(
|
||||
protocol = URLProtocol.HTTPS,
|
||||
host = EhUrl.domain,
|
||||
host = host,
|
||||
pathSegments = path,
|
||||
parameters = parameters,
|
||||
)
|
||||
|
||||
@@ -15,19 +15,17 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import arrow.core.memoize
|
||||
import com.google.android.material.color.utilities.Hct
|
||||
import com.google.android.material.color.utilities.MathUtils
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.abs
|
||||
|
||||
object EhUtils {
|
||||
const val NONE = -1 // Use it for homepage
|
||||
@@ -59,14 +57,14 @@ object EhUtils {
|
||||
private const val BG_COLOR_UNKNOWN = 0xff000000u
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||
private val PATTERN_TITLE_PREFIX = Pattern.compile(
|
||||
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*",
|
||||
private val PATTERN_TITLE_PREFIX = Regex(
|
||||
"""^(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*""",
|
||||
)
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
|
||||
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
private val PATTERN_TITLE_SUFFIX = Regex(
|
||||
"""(?:\s+ch.[\s\d-]+)?(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*$""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
private val CATEGORY_VALUES = hashMapOf(
|
||||
@@ -102,14 +100,27 @@ object EhUtils {
|
||||
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun differenceDegrees(a: Float, b: Float): Float {
|
||||
return 180.0f - abs(abs(a - b) - 180.0f)
|
||||
}
|
||||
|
||||
private fun sanitizeDegreesDouble(degrees: Float): Float {
|
||||
val deg = degrees % 360.0f
|
||||
return if (deg < 0) deg + 360.0f else deg
|
||||
}
|
||||
|
||||
private fun rotationDirection(from: Float, to: Float): Float {
|
||||
val increasingDifference = sanitizeDegreesDouble(to - from)
|
||||
return if (increasingDifference <= 180.0) 1.0f else -1.0f
|
||||
}
|
||||
|
||||
val harmonizeWithRole = { primaryContainer: Int, src: Int ->
|
||||
val fromHct = Hct.fromInt(src)
|
||||
val toHct = Hct.fromInt(primaryContainer)
|
||||
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()
|
||||
val fromHct = FloatArray(3).apply { ColorUtils.colorToM3HCT(src, this) }
|
||||
val toHct = FloatArray(3).apply { ColorUtils.colorToM3HCT(primaryContainer, this) }
|
||||
val differenceDegrees = differenceDegrees(fromHct[0], toHct[0])
|
||||
val rotationDegrees = minOf(differenceDegrees * 0.5f, 15.0f)
|
||||
val outputHue = sanitizeDegreesDouble(fromHct[0] + rotationDegrees * rotationDirection(fromHct[0], toHct[0]))
|
||||
ColorUtils.M3HCTToColor(outputHue, toHct[1], toHct[2])
|
||||
}.memoize()
|
||||
|
||||
@Stable
|
||||
@@ -158,16 +169,12 @@ object EhUtils {
|
||||
|
||||
fun extractTitle(fullTitle: String?): String? {
|
||||
var title: String = fullTitle ?: return null
|
||||
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("")
|
||||
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("")
|
||||
title = PATTERN_TITLE_PREFIX.replaceFirst(title, "")
|
||||
title = PATTERN_TITLE_SUFFIX.replaceFirst(title, "")
|
||||
// Sometimes title is combined by romaji and english translation.
|
||||
// Only need romaji.
|
||||
// TODO But not sure every '|' means that
|
||||
val index = title.indexOf('|')
|
||||
if (index >= 0) {
|
||||
title = title.substring(0, index)
|
||||
}
|
||||
return title.ifEmpty { null }
|
||||
return title.substringBeforeLast('|').trim().ifEmpty { null }
|
||||
}
|
||||
|
||||
fun handleThumbUrlResolution(url: String?): String? {
|
||||
|
||||
@@ -5,7 +5,6 @@ interface AbstractDownloadInfo {
|
||||
var legacy: Int
|
||||
var time: Long
|
||||
var label: String?
|
||||
var position: Int
|
||||
var speed: Long
|
||||
var remaining: Long
|
||||
var finished: Int
|
||||
|
||||
@@ -16,23 +16,27 @@
|
||||
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
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
@Entity(tableName = "GALLERIES")
|
||||
data class BaseGalleryInfo @Keep constructor(
|
||||
@TypeConverters(SimpleTagsConverter::class)
|
||||
data class BaseGalleryInfo(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "GID")
|
||||
override var gid: Long = 0,
|
||||
override var gid: Long,
|
||||
|
||||
@ColumnInfo(name = "TOKEN")
|
||||
override var token: String? = null,
|
||||
override var token: String,
|
||||
|
||||
@ColumnInfo(name = "TITLE")
|
||||
override var title: String? = null,
|
||||
@@ -61,10 +65,10 @@ data class BaseGalleryInfo @Keep constructor(
|
||||
@Ignore
|
||||
override var rated: Boolean = false,
|
||||
|
||||
@Ignore
|
||||
override var simpleTags: ArrayList<String>? = null,
|
||||
@ColumnInfo(name = "SIMPLE_TAGS")
|
||||
override var simpleTags: List<String>? = null,
|
||||
|
||||
@Ignore
|
||||
@ColumnInfo(name = "PAGES", defaultValue = "0")
|
||||
override var pages: Int = 0,
|
||||
|
||||
@Ignore
|
||||
@@ -85,5 +89,13 @@ data class BaseGalleryInfo @Keep constructor(
|
||||
@Ignore
|
||||
override var favoriteNote: String? = null,
|
||||
) : GalleryInfo, Parcelable {
|
||||
constructor() : this(0)
|
||||
constructor() : this(0, "")
|
||||
}
|
||||
|
||||
class SimpleTagsConverter {
|
||||
@TypeConverter
|
||||
fun fromString(value: String) = value.split(", ")
|
||||
|
||||
@TypeConverter
|
||||
fun toString(value: List<String>) = value.joinToString()
|
||||
}
|
||||
@@ -45,7 +45,7 @@ data class GalleryDetail(
|
||||
var torrentUrl: String? = null,
|
||||
var archiveUrl: String? = null,
|
||||
var parent: String? = null,
|
||||
var newerVersions: ArrayList<BaseGalleryInfo> = ArrayList(),
|
||||
var newerVersions: List<BaseGalleryInfo> = emptyList(),
|
||||
var visible: String? = null,
|
||||
var language: String? = null,
|
||||
var size: String? = null,
|
||||
@@ -56,8 +56,9 @@ data class GalleryDetail(
|
||||
val previewPages: Int,
|
||||
val previewList: List<GalleryPreview>,
|
||||
) : GalleryInfo by galleryInfo, Parcelable {
|
||||
override fun generateSLang() {
|
||||
fun fillInfo() {
|
||||
val index = LANGUAGES.indexOf(language)
|
||||
if (index != -1) simpleLanguage = S_LANGS[index]
|
||||
simpleTags = tags.flatten()
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,11 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import java.util.regex.Pattern
|
||||
import moe.tarsin.kt.unreachable
|
||||
|
||||
interface GalleryInfo {
|
||||
var gid: Long
|
||||
var token: String?
|
||||
var token: String
|
||||
var title: String?
|
||||
var titleJpn: String?
|
||||
var thumbKey: String?
|
||||
@@ -30,7 +29,7 @@ interface GalleryInfo {
|
||||
var disowned: Boolean
|
||||
var rating: Float
|
||||
var rated: Boolean
|
||||
var simpleTags: ArrayList<String>?
|
||||
var simpleTags: List<String>?
|
||||
var pages: Int
|
||||
var thumbWidth: Int
|
||||
var thumbHeight: Int
|
||||
@@ -62,36 +61,36 @@ interface GalleryInfo {
|
||||
"NL",
|
||||
)
|
||||
private val S_LANG_PATTERNS = arrayOf(
|
||||
Pattern.compile(
|
||||
Regex(
|
||||
"[(\\[]eng(?:lish)?[)\\]]|英訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
// [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
|
||||
Pattern.compile(
|
||||
Regex(
|
||||
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile(
|
||||
Regex(
|
||||
"[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]|スペイン翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile("[(\\[]korean?[)\\]]|韓国翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]portuguese|ポルトガル翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile(
|
||||
Regex("[(\\[]korean?[)\\]]|韓国翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]portuguese|ポルトガル翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex(
|
||||
"[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย|タイ翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile("[(\\[]german[)\\]]|ドイツ翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]italiano?[)\\]]|イタリア翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile(
|
||||
Regex("[(\\[]german[)\\]]|ドイツ翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]italiano?[)\\]]|イタリア翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex(
|
||||
"[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]|ベトナム翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile("[(\\[]polish[)\\]]|ポーランド翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Regex("[(\\[]polish[)\\]]|ポーランド翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]dutch[)\\]]|オランダ翻訳", RegexOption.IGNORE_CASE),
|
||||
)
|
||||
val S_LANG_TAGS = arrayOf(
|
||||
"language:english",
|
||||
@@ -123,7 +122,7 @@ interface GalleryInfo {
|
||||
|
||||
private fun generateSLangFromTitle(title: String): String? {
|
||||
for (i in S_LANGS.indices) {
|
||||
if (S_LANG_PATTERNS[i].matcher(title).find()) {
|
||||
if (S_LANG_PATTERNS[i].containsMatchIn(title)) {
|
||||
return S_LANGS[i]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
|
||||
import com.hippo.ehviewer.client.ehUrl
|
||||
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 java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
@@ -39,6 +38,7 @@ data class ListUrlBuilder(
|
||||
var mNext: String? = null,
|
||||
// Reset to null after initial loading
|
||||
var mJumpTo: String? = null,
|
||||
private var mRange: Int = 0,
|
||||
var category: Int = EhUtils.NONE,
|
||||
private var mKeyword: String? = null,
|
||||
var hash: String? = null,
|
||||
@@ -47,19 +47,25 @@ data class ListUrlBuilder(
|
||||
var pageFrom: Int = -1,
|
||||
var pageTo: Int = -1,
|
||||
var imagePath: String? = null,
|
||||
var isUseSimilarityScan: Boolean = false,
|
||||
var isOnlySearchCovers: Boolean = false,
|
||||
) : Parcelable {
|
||||
|
||||
fun setIndex(index: String?, isNext: Boolean = true) {
|
||||
mNext = index?.takeIf { isNext }
|
||||
mPrev = index?.takeUnless { isNext }
|
||||
fun setIndex(index: String, isNext: Boolean = true) {
|
||||
mNext = index.takeIf { isNext }
|
||||
mPrev = index.takeUnless { isNext }
|
||||
mRange = 0
|
||||
}
|
||||
|
||||
fun setJumpTo(jumpTo: Int) {
|
||||
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
|
||||
}
|
||||
|
||||
fun setRange(range: Int) {
|
||||
mRange = range
|
||||
mPrev = null
|
||||
mNext = null
|
||||
mJumpTo = null
|
||||
}
|
||||
|
||||
var keyword: String?
|
||||
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
|
||||
set(keyword) {
|
||||
@@ -74,7 +80,7 @@ data class ListUrlBuilder(
|
||||
minRating = q.minRating
|
||||
pageFrom = q.pageFrom
|
||||
pageTo = q.pageTo
|
||||
mNext = q.name.substringAfterLast('@', "").takeIf { it.isNotEmpty() }
|
||||
mNext = q.name.substringAfterLast('@', "").ifEmpty { null }
|
||||
}
|
||||
|
||||
fun toQuickSearch(name: String): QuickSearch {
|
||||
@@ -266,10 +272,10 @@ data class ListUrlBuilder(
|
||||
} ?: it
|
||||
}
|
||||
addQueryParameterIfNotBlank("f_search", query)
|
||||
addQueryParameterIfNotBlank("f_shash", hash)
|
||||
addQueryParameterIfNotBlank("prev", mPrev)
|
||||
addQueryParameterIfNotBlank("next", mNext)
|
||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||
// Advance search
|
||||
if (advanceSearch > 0 || minRating > 0 || pageFrom > 0 || pageTo > 0) {
|
||||
addQueryParameter("advsearch", "1")
|
||||
@@ -306,55 +312,27 @@ data class ListUrlBuilder(
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
}.buildString()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
MODE_UPLOADER, MODE_TAG -> {
|
||||
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
||||
ehUrl(listOf(path, requireNotNull(mKeyword))) {
|
||||
addQueryParameterIfNotBlank("prev", mPrev)
|
||||
addQueryParameterIfNotBlank("next", mNext)
|
||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||
}.buildString()
|
||||
}
|
||||
|
||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
|
||||
MODE_IMAGE_SEARCH -> ehUrl {
|
||||
addQueryParameter("f_shash", requireNotNull(hash))
|
||||
}.buildString()
|
||||
MODE_TOPLIST -> {
|
||||
buildString {
|
||||
append(EhUrl.HOST_E)
|
||||
append("toplist.php?tl=")
|
||||
append(mKeyword!!)
|
||||
mJumpTo?.let {
|
||||
append("&p=").append(it)
|
||||
}
|
||||
}
|
||||
ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
||||
addQueryParameter("tl", requireNotNull(mKeyword))
|
||||
addQueryParameterIfNotBlank("p", mJumpTo)
|
||||
}.buildString()
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
@JvmInline
|
||||
value class TagNamespace(val value: String) {
|
||||
fun toPrefix(): String? = prefixMap[this]
|
||||
|
||||
companion object {
|
||||
val Artist = TagNamespace("artist")
|
||||
val Cosplayer = TagNamespace("cosplayer")
|
||||
val Character = TagNamespace("character")
|
||||
val Female = TagNamespace("female")
|
||||
val Group = TagNamespace("group")
|
||||
val Language = TagNamespace("language")
|
||||
val Male = TagNamespace("male")
|
||||
val Mixed = TagNamespace("mixed")
|
||||
val Other = TagNamespace("other")
|
||||
val Parody = TagNamespace("parody")
|
||||
val Reclass = TagNamespace("reclass")
|
||||
private val prefixMap = mapOf(
|
||||
Artist to "a",
|
||||
Cosplayer to "cos",
|
||||
Character to "c",
|
||||
Female to "f",
|
||||
Group to "g",
|
||||
Language to "l",
|
||||
Male to "m",
|
||||
Mixed to "x",
|
||||
Other to "o",
|
||||
Parody to "p",
|
||||
Reclass to "r",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object ArchiveParser {
|
||||
@@ -48,7 +49,7 @@ object ArchiveParser {
|
||||
}.onSuccess {
|
||||
archiveList.add(it)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,5 +88,5 @@ object ArchiveParser {
|
||||
val isHAtH: Boolean,
|
||||
)
|
||||
|
||||
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: HomeParser.Funds?)
|
||||
class Result(val paramOr: String, val archiveList: List<Archive>, var funds: HomeParser.Funds?)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import io.ktor.util.moveToByteArray
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
|
||||
inline fun <reified T> unmarshalParsingAs(body: ByteBuffer, parser: (ByteBuffer, Int) -> Int): T {
|
||||
val cborBytes = parser(body, body.limit())
|
||||
body.limit(cborBytes)
|
||||
val array = body.moveToByteArray()
|
||||
return Cbor.decodeFromByteArray<T>(array)
|
||||
}
|
||||
@@ -1,32 +1,29 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.NotLoggedInException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.Serializable
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
@Serializable
|
||||
data class FavParserResult(
|
||||
val catArray: List<String>,
|
||||
val countArray: List<Int>,
|
||||
val galleryListResult: GalleryListResult,
|
||||
) {
|
||||
val prev = galleryListResult.prev
|
||||
val next = galleryListResult.next
|
||||
val galleryInfoList = galleryListResult.galleryInfoList
|
||||
}
|
||||
|
||||
object FavoritesParser {
|
||||
suspend fun parse(body: ByteBuffer): Result {
|
||||
val catArray = arrayOfNulls<String>(10)
|
||||
val countArray = runSuspendCatching {
|
||||
parseFav(body, favCat = catArray).also { check(it.isNotEmpty()) }
|
||||
fun parse(body: ByteBuffer): FavParserResult {
|
||||
return runSuspendCatching {
|
||||
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
|
||||
}.getOrElse {
|
||||
if (it is RuntimeException && it.message == "Not logged in!") throw NotLoggedInException()
|
||||
throw ParseException("Parse favorites error", it)
|
||||
}
|
||||
val result = GalleryListParser.parse(body)
|
||||
return Result(catArray.requireNoNulls(), countArray, result)
|
||||
}
|
||||
|
||||
class Result(
|
||||
val catArray: Array<String>,
|
||||
val countArray: IntArray,
|
||||
galleryListResult: GalleryListResult,
|
||||
) {
|
||||
val prev = galleryListResult.prev
|
||||
val next = galleryListResult.next
|
||||
val galleryInfoList = galleryListResult.galleryInfoList
|
||||
}
|
||||
}
|
||||
|
||||
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit(), favCat: Array<String?>): IntArray
|
||||
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit()): Int
|
||||
@@ -60,6 +60,6 @@ object GalleryApiParser {
|
||||
@SerialName("filecount")
|
||||
val pages: Int,
|
||||
val rating: Float,
|
||||
val tags: ArrayList<String>,
|
||||
val tags: List<String>,
|
||||
)
|
||||
}
|
||||
@@ -38,14 +38,15 @@ import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.exception.PiningException
|
||||
import com.hippo.ehviewer.client.getThumbKey
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.toEpochMillis
|
||||
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 eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.format.MonthNames
|
||||
import kotlinx.datetime.format.char
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@@ -77,8 +78,19 @@ object GalleryDetailParser {
|
||||
private val PATTERN_FAVORITE_SLOT =
|
||||
Regex("/fav.png\\); background-position:0px -(\\d+)px")
|
||||
private val EMPTY_GALLERY_COMMENT_LIST = GalleryCommentList(emptyList(), false)
|
||||
private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
|
||||
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC)
|
||||
|
||||
// dd MMMM yyyy, HH:mm
|
||||
private val WEB_COMMENT_DATE_FORMAT = LocalDateTime.Format {
|
||||
dayOfMonth()
|
||||
char(' ')
|
||||
monthName(MonthNames.ENGLISH_FULL)
|
||||
char(' ')
|
||||
year()
|
||||
chars(", ")
|
||||
hour()
|
||||
char(':')
|
||||
minute()
|
||||
}
|
||||
private const val OFFENSIVE_STRING =
|
||||
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>"
|
||||
private const val PINING_STRING = "<p>This gallery is pining for the fjords.</p>"
|
||||
@@ -93,8 +105,7 @@ object GalleryDetailParser {
|
||||
|
||||
// Error info
|
||||
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
|
||||
// Temporary workaround, see https://github.com/jhy/jsoup/issues/1850
|
||||
val document = Jsoup.parse(body.replace("del>", "s>"))
|
||||
val document = Jsoup.parse(body)
|
||||
val galleryDetail = GalleryDetail(
|
||||
tags = parseTagGroups(document),
|
||||
comments = parseComments(document),
|
||||
@@ -103,8 +114,8 @@ object GalleryDetailParser {
|
||||
)
|
||||
parseDetail(galleryDetail, document, body)
|
||||
|
||||
// Generate simpleLanguage for local favorites
|
||||
galleryDetail.generateSLang()
|
||||
// Fill info for database
|
||||
galleryDetail.fillInfo()
|
||||
return galleryDetail
|
||||
}
|
||||
|
||||
@@ -220,15 +231,10 @@ object GalleryDetailParser {
|
||||
// newer version
|
||||
d.getElementById("gnd")?.run {
|
||||
val dates = PATTERN_NEWER_DATE.findAll(body).map { it.groupValues[1] }.toList()
|
||||
select("a").forEachIndexed { index, element ->
|
||||
val gi = BaseGalleryInfo()
|
||||
gd.newerVersions = select("a").mapIndexedNotNull { index, element ->
|
||||
val result = GalleryDetailUrlParser.parse(element.attr("href"))
|
||||
if (result != null) {
|
||||
gi.gid = result.gid
|
||||
gi.token = result.token
|
||||
gi.title = element.text().trim()
|
||||
gi.posted = dates[index]
|
||||
gd.newerVersions.add(gi)
|
||||
result?.run {
|
||||
BaseGalleryInfo(gid, token, element.text().trim(), posted = dates[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,7 +305,7 @@ object GalleryDetailParser {
|
||||
if (group.size > 0) group else null
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -314,7 +320,7 @@ object GalleryDetailParser {
|
||||
parseTagGroups(tagGroups)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
@@ -324,7 +330,7 @@ object GalleryDetailParser {
|
||||
trs.mapNotNull { parseTagGroup(it) }
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
@@ -377,7 +383,7 @@ object GalleryDetailParser {
|
||||
} else {
|
||||
temp.substring("Posted on ".length) to null
|
||||
}
|
||||
val time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(timeStr)).toEpochMilli()
|
||||
val time = WEB_COMMENT_DATE_FORMAT.parse(timeStr).toEpochMillis()
|
||||
// comment
|
||||
val c6 = element.getElementsByClass("c6").first()
|
||||
// fix underline support
|
||||
@@ -396,14 +402,14 @@ object GalleryDetailParser {
|
||||
}
|
||||
// last edited
|
||||
val c8 = element.getElementsByClass("c8").first()
|
||||
val lastEdited = c8?.children()?.first()?.run { Instant.from(WEB_COMMENT_DATE_FORMAT.parse(text())).toEpochMilli() } ?: 0
|
||||
val lastEdited = c8?.children()?.first()?.run { WEB_COMMENT_DATE_FORMAT.parse(text()).toEpochMillis() } ?: 0
|
||||
GalleryComment(
|
||||
id, score, editable, voteUpAble, voteUpEd, voteDownAble,
|
||||
voteDownEd, uploader, voteState, time, user, commentHtml, lastEdited,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -412,6 +418,8 @@ object GalleryDetailParser {
|
||||
* Parse comments with html parser
|
||||
*/
|
||||
suspend fun parseComments(document: Document): GalleryCommentList {
|
||||
// Disable pretty print to get comments in raw html
|
||||
document.outputSettings().prettyPrint(false)
|
||||
return try {
|
||||
val cdiv = document.getElementById("cdiv")!!
|
||||
val c1s = cdiv.getElementsByClass("c1")
|
||||
@@ -433,7 +441,7 @@ object GalleryDetailParser {
|
||||
GalleryCommentList(list, hasMore)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
EMPTY_GALLERY_COMMENT_LIST
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,29 +16,23 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.util.toLongOrDefault
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Like http://exhentai.org/g/1234567/a1b2c3d4e5<br></br>
|
||||
*/
|
||||
object GalleryDetailUrlParser {
|
||||
private val URL_STRICT_PATTERN = Pattern.compile(
|
||||
private val URL_STRICT_PATTERN = Regex(
|
||||
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/(?:g|mpv)/(\\d+)/([0-9a-f]{10})",
|
||||
)
|
||||
private val URL_PATTERN = Pattern.compile("(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)")
|
||||
private val URL_PATTERN = Regex("(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)")
|
||||
|
||||
fun parse(url: String?, strict: Boolean = true): Result? {
|
||||
url ?: return null
|
||||
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
||||
val m = pattern.matcher(url)
|
||||
return if (m.find()) {
|
||||
val gid = m.group(1)!!.toLongOrDefault(0).takeIf { it > 0 }
|
||||
gid?.let { Result(it, m.group(2)!!) }
|
||||
} else {
|
||||
null
|
||||
return pattern.find(url)?.destructured?.let { (gid, token) ->
|
||||
Result(gid.toLong(), token)
|
||||
}
|
||||
}
|
||||
|
||||
class Result(val gid: Long, val token: String)
|
||||
data class Result(val gid: Long, val token: String)
|
||||
}
|
||||
@@ -1,48 +1,26 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import androidx.annotation.Keep
|
||||
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
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.Serializable
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
object GalleryListParser {
|
||||
suspend fun parse(body: ByteBuffer) = runSuspendCatching {
|
||||
parseGalleryInfoList(body)
|
||||
}.onFailure {
|
||||
if (it is RuntimeException) {
|
||||
if (it.message?.startsWith("Your IP address") == true) {
|
||||
throw EhException(it.message)
|
||||
}
|
||||
if (it.message == "No watched tags!") {
|
||||
throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
}
|
||||
if (it.message == "No hits found!") {
|
||||
throw EhException(R.string.gallery_list_empty_hit)
|
||||
}
|
||||
}
|
||||
fun parse(body: ByteBuffer) = runSuspendCatching {
|
||||
unmarshalParsingAs<GalleryListResult>(body, ::parseGalleryInfoList)
|
||||
}.getOrElse {
|
||||
throw ParseException("Can't parse gallery list", it)
|
||||
}.getOrThrow().apply {
|
||||
galleryInfoList.onEach {
|
||||
if (it.favoriteSlot == NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
|
||||
it.favoriteSlot = LOCAL_FAVORITED
|
||||
}
|
||||
it.generateSLang()
|
||||
}
|
||||
}
|
||||
|
||||
val emptyResult = GalleryListResult(null, null, arrayListOf())
|
||||
}
|
||||
|
||||
class GalleryListResult @Keep constructor(
|
||||
@Serializable
|
||||
data class GalleryListResult(
|
||||
val prev: String?,
|
||||
val next: String?,
|
||||
val galleryInfoList: ArrayList<BaseGalleryInfo>,
|
||||
)
|
||||
|
||||
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): GalleryListResult
|
||||
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
|
||||
@@ -19,6 +19,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
@@ -77,7 +78,7 @@ object GalleryListUrlParser {
|
||||
try {
|
||||
URLDecoder.decode(uploader, StandardCharsets.UTF_8.displayName())
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -106,7 +107,7 @@ object GalleryListUrlParser {
|
||||
try {
|
||||
URLDecoder.decode(tag, StandardCharsets.UTF_8.displayName())
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object GalleryNotAvailableParser {
|
||||
@@ -27,7 +28,7 @@ object GalleryNotAvailableParser {
|
||||
e!!.child(0).html().replace("<br>", "\n")
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import com.hippo.ehviewer.util.unescapeXml
|
||||
|
||||
object GalleryPageParser {
|
||||
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
|
||||
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([\\d-]+)'\\)")
|
||||
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
|
||||
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+/fullimg/[^\"]+)\">")
|
||||
|
||||
// TODO Not sure about the size of show keys
|
||||
|
||||
@@ -16,34 +16,21 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import com.hippo.ehviewer.util.toLongOrDefault
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Like http://exhentai.org/s/91ea4b6d89/901103-12
|
||||
*/
|
||||
object GalleryPageUrlParser {
|
||||
private val URL_STRICT_PATTERN = Pattern.compile(
|
||||
private val URL_STRICT_PATTERN = Regex(
|
||||
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/s/([0-9a-f]{10})/(\\d+)-(\\d+)",
|
||||
)
|
||||
private val URL_PATTERN = Pattern.compile("([0-9a-f]{10})/(\\d+)-(\\d+)")
|
||||
private val URL_PATTERN = Regex("([0-9a-f]{10})/(\\d+)-(\\d+)")
|
||||
|
||||
fun parse(url: String?, strict: Boolean = true): Result? {
|
||||
url ?: return null
|
||||
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
||||
val m = pattern.matcher(url)
|
||||
return if (m.find()) {
|
||||
val gid = m.group(2)!!.toLongOrDefault(-1L)
|
||||
val pToken = m.group(1)!!
|
||||
val page = m.group(3)!!.toIntOrDefault(0) - 1
|
||||
if (gid < 0 || page < 0) {
|
||||
null
|
||||
} else {
|
||||
Result(gid, pToken, page)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
return pattern.find(url)?.destructured?.let { (pToken, gid, page) ->
|
||||
Result(gid.toLong(), pToken, page.toInt() - 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
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 kotlinx.serialization.Serializable
|
||||
|
||||
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)
|
||||
unmarshalParsingAs<Limits>(body, ::parseLimit)
|
||||
}.getOrElse { throw ParseException("Parse image limits error", it) }
|
||||
|
||||
fun parseResetLimits(body: String) {
|
||||
@@ -32,10 +32,13 @@ object HomeParser {
|
||||
|
||||
@Parcelize
|
||||
data class Funds(val fundsGP: Int, val fundsC: Int) : Parcelable
|
||||
class Result(val limits: Limits, val funds: Funds)
|
||||
|
||||
@Parcelize
|
||||
data class Result(val limits: Limits, val funds: Funds) : Parcelable
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Limits @Keep constructor(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
||||
@Serializable
|
||||
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
||||
|
||||
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Limits
|
||||
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Int
|
||||
@@ -16,20 +16,29 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import com.hippo.ehviewer.util.toLocalDateTime
|
||||
import com.hippo.ehviewer.util.toLongOrDefault
|
||||
import com.hippo.ehviewer.util.unescapeXml
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.format.char
|
||||
|
||||
object ParserUtils {
|
||||
private val formatter =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm", Locale.US).withZone(ZoneOffset.UTC)
|
||||
// yyyy-MM-dd HH:mm
|
||||
private val formatter = LocalDateTime.Format {
|
||||
year()
|
||||
char('-')
|
||||
monthNumber()
|
||||
char('-')
|
||||
dayOfMonth()
|
||||
char(' ')
|
||||
hour()
|
||||
char(':')
|
||||
minute()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun formatDate(time: Long): String {
|
||||
return formatter.format(Instant.ofEpochMilli(time))
|
||||
return formatter.format(time.toLocalDateTime())
|
||||
}
|
||||
|
||||
fun trim(str: String?): String {
|
||||
|
||||
@@ -15,14 +15,13 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import android.util.Log
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object ProfileParser {
|
||||
private val TAG = ProfileParser::class.java.simpleName
|
||||
fun parse(body: String): Result {
|
||||
return runCatching {
|
||||
val d = Jsoup.parse(body)
|
||||
@@ -31,7 +30,7 @@ object ProfileParser {
|
||||
val avatar = runCatching {
|
||||
val avatar =
|
||||
profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr("src")
|
||||
if (avatar.isNullOrEmpty()) {
|
||||
if (avatar.isEmpty()) {
|
||||
null
|
||||
} else if (!avatar.startsWith("http")) {
|
||||
EhUrl.URL_FORUMS + avatar
|
||||
@@ -40,7 +39,7 @@ object ProfileParser {
|
||||
}
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
Log.i(TAG, "No avatar")
|
||||
logcat { "No avatar" }
|
||||
null
|
||||
}
|
||||
Result(displayName, avatar)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object SignInParser {
|
||||
private val NAME_PATTERN = Pattern.compile("<p>You are now logged in as: (.+?)<")
|
||||
private val ERROR_PATTERN = Pattern.compile(
|
||||
"<h4>The error returned was:</h4>\\s*<p>(.+?)</p>" +
|
||||
"|<span class=\"postcolor\">(.+?)</span>",
|
||||
)
|
||||
|
||||
fun parse(body: String): String {
|
||||
var m = NAME_PATTERN.matcher(body)
|
||||
return if (m.find()) {
|
||||
m.group(1)!!
|
||||
} else {
|
||||
m = ERROR_PATTERN.matcher(body)
|
||||
if (m.find()) {
|
||||
throw EhException(m.group(1) ?: m.group(2))
|
||||
} else {
|
||||
throw ParseException("Can't parse sign in")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object TorrentParser {
|
||||
fun parse(body: ByteBuffer) = runCatching {
|
||||
parseTorrent(body)
|
||||
}.getOrElse { throw ParseException("Can't parse torrent list", it) }
|
||||
unmarshalParsingAs<TorrentResult>(body, ::parseTorrent)
|
||||
}.getOrElse {
|
||||
throw ParseException("Can't parse torrent list", it)
|
||||
}
|
||||
}
|
||||
|
||||
class Torrent @Keep constructor(
|
||||
@Serializable
|
||||
data class Torrent(
|
||||
val posted: String,
|
||||
val size: String,
|
||||
val seeds: Int,
|
||||
@@ -24,4 +27,4 @@ fun Torrent.format() = "[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloa
|
||||
|
||||
typealias TorrentResult = List<Torrent>
|
||||
|
||||
private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): ArrayList<Torrent>
|
||||
private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): Int
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import coil3.BitmapImage
|
||||
import coil3.Extras
|
||||
import coil3.Image
|
||||
import coil3.asCoilImage
|
||||
import coil3.getExtra
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.intercept.Interceptor.Chain
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.allowHardware
|
||||
import com.hippo.ehviewer.image.detectBorder
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
|
||||
private const val CROP_THRESHOLD = 0.75f
|
||||
private const val RATIO_THRESHOLD = 2
|
||||
|
||||
private val maybeCropBorderKey = Extras.Key(default = false)
|
||||
|
||||
fun ImageRequest.Builder.maybeCropBorder(enable: Boolean) = apply {
|
||||
extras[maybeCropBorderKey] = enable
|
||||
}
|
||||
|
||||
val ImageRequest.maybeCropBorder: Boolean
|
||||
get() = getExtra(maybeCropBorderKey)
|
||||
|
||||
class BitmapImageWithRect(
|
||||
val rect: IntRect,
|
||||
val image: Image,
|
||||
) : Image by image
|
||||
|
||||
object CropBorderInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Chain): ImageResult {
|
||||
val req = chain.request
|
||||
if (req.maybeCropBorder) {
|
||||
val hw = req.allowHardware && isAtLeastO
|
||||
val result = if (hw) {
|
||||
val nonHw = req.newBuilder().allowHardware(false).build()
|
||||
chain.withRequest(nonHw).proceed()
|
||||
} else {
|
||||
chain.proceed()
|
||||
}
|
||||
if (result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImage) {
|
||||
val bitmap = image.bitmap
|
||||
val ratio = if (image.height > image.width) {
|
||||
image.height / image.width
|
||||
} else {
|
||||
image.width / image.height
|
||||
}
|
||||
val rect = if (ratio < RATIO_THRESHOLD) {
|
||||
val array = detectBorder(bitmap)
|
||||
val minWidth = image.width * CROP_THRESHOLD
|
||||
val minHeight = image.height * CROP_THRESHOLD
|
||||
IntRect(array[0], array[1], array[2], array[3]).takeIf {
|
||||
it.width > minWidth && it.height > minHeight
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val maybeHwImage = if (hw) {
|
||||
// Upload to Graphical Buffer to accelerate render
|
||||
// May fail if the image is too long
|
||||
bitmap.copy(Bitmap.Config.HARDWARE, false)?.run {
|
||||
bitmap.recycle()
|
||||
asCoilImage()
|
||||
} ?: image
|
||||
} else {
|
||||
image
|
||||
}
|
||||
val new = if (rect != null) {
|
||||
BitmapImageWithRect(rect, maybeHwImage)
|
||||
} else {
|
||||
maybeHwImage
|
||||
}
|
||||
return result.copy(image = new, request = result.request, dataSource = result.dataSource)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return chain.proceed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
||||
import com.hippo.ehviewer.download.downloadLocation
|
||||
import com.hippo.ehviewer.spider.DownloadInfoMagics.decodeMagicRequestOrUrl
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.unifile.asUniFile
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
|
||||
object DownloadThumbInterceptor : Interceptor {
|
||||
// TODO: Remove this after a few releases
|
||||
private const val LEGACY_THUMB_FILE = ".thumb"
|
||||
private const val THUMB_FILE = "thumb.jpg"
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val magicOrUrl = chain.request.data as? String
|
||||
if (magicOrUrl != null) {
|
||||
val (url, location) = decodeMagicRequestOrUrl(magicOrUrl)
|
||||
if (location != null) {
|
||||
val thumb = withIOContext {
|
||||
val legacyThumb = downloadLocation / location / LEGACY_THUMB_FILE
|
||||
if (legacyThumb.isFile) {
|
||||
legacyThumb.apply { renameTo(THUMB_FILE) }
|
||||
} else {
|
||||
downloadLocation / location / THUMB_FILE
|
||||
}
|
||||
}
|
||||
if (withIOContext { thumb.isFile }) {
|
||||
val new = chain.request.newBuilder().data(thumb.uri.toString()).build()
|
||||
val result = chain.withRequest(new).proceed()
|
||||
if (result is SuccessResult) return result
|
||||
}
|
||||
val new = chain.request.newBuilder().data(url).build()
|
||||
val result = chain.withRequest(new).proceed()
|
||||
if (result is SuccessResult) {
|
||||
withIOContext {
|
||||
// Accessing the recreated file immediately after deleting it throws
|
||||
// FileNotFoundException, so we just overwrite the existing file.
|
||||
if (thumb.ensureFile()) {
|
||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||
imageCache.read(key) {
|
||||
data.asUniFile() sendTo thumb
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
return chain.proceed()
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,33 @@ import android.graphics.drawable.ColorDrawable
|
||||
import coil3.asCoilImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.Decoder
|
||||
import coil3.network.NetworkHeaders
|
||||
import coil3.network.httpHeaders
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.size.Size
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
||||
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryPreview
|
||||
import com.hippo.ehviewer.client.data.NormalGalleryPreview
|
||||
import com.hippo.ehviewer.client.thumbUrl
|
||||
import com.hippo.ehviewer.client.url
|
||||
import com.hippo.ehviewer.spider.DownloadInfoMagics.encodeMagicRequestOrUrl
|
||||
import io.ktor.http.HttpHeaders
|
||||
|
||||
private val header = NetworkHeaders.Builder().apply {
|
||||
add(HttpHeaders.UserAgent, Settings.userAgent)
|
||||
add(HttpHeaders.Accept, CHROME_ACCEPT)
|
||||
add(HttpHeaders.AcceptLanguage, CHROME_ACCEPT_LANGUAGE)
|
||||
}.build()
|
||||
|
||||
fun ImageRequest.Builder.ehUrl(info: GalleryInfo) = apply {
|
||||
val key = info.thumbKey!!
|
||||
data(info.thumbUrl)
|
||||
data(encodeMagicRequestOrUrl(info))
|
||||
memoryCacheKey(key)
|
||||
diskCacheKey(key)
|
||||
httpHeaders(header)
|
||||
}
|
||||
|
||||
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
|
||||
@@ -26,6 +39,7 @@ fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
|
||||
memoryCacheKey(preview.imageKey)
|
||||
diskCacheKey(preview.imageKey)
|
||||
if (preview is NormalGalleryPreview) size(Size.ORIGINAL)
|
||||
httpHeaders(header)
|
||||
}
|
||||
|
||||
private val stubImage = ColorDrawable(Color.BLACK).asCoilImage(true)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.intercept.Interceptor.Chain
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.allowHardware
|
||||
|
||||
/**
|
||||
* Allocating hardware bitmaps may fail on extremely long images.
|
||||
*
|
||||
* Retry with [allowHardware] disabled.
|
||||
*/
|
||||
object HardwareBitmapInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Chain): ImageResult {
|
||||
val result = chain.proceed()
|
||||
return if (chain.request.allowHardware && result is ErrorResult && result.throwable is OutOfMemoryError) {
|
||||
val req = chain.request.newBuilder().allowHardware(false).build()
|
||||
chain.withRequest(req).proceed()
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
@file:Suppress("BlockingMethodInNonBlockingContext")
|
||||
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import coil3.ComponentRegistry
|
||||
import coil3.ImageLoader
|
||||
import coil3.Uri
|
||||
import coil3.decode.DataSource
|
||||
import coil3.decode.ImageSource
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.SourceFetchResult
|
||||
import coil3.request.Options
|
||||
import com.hippo.ehviewer.client.ehRequest
|
||||
import com.hippo.ehviewer.util.copyTo
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
|
||||
/**
|
||||
* A HttpUriFetcher impl use Ktor (Will do jvmheapless IO when backing engine is Cronet)
|
||||
* cacheHeader is never respected | recorded since thumb is immutable
|
||||
*/
|
||||
class KtorHttpUriFetcher(private val data: String, private val options: Options, private val imageLoader: ImageLoader) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult {
|
||||
val diskCacheKey = options.diskCacheKey ?: data
|
||||
val diskCache = requireNotNull(imageLoader.diskCache)
|
||||
val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run {
|
||||
val success = ehRequest(data, verbose = false).execute {
|
||||
diskCache.suspendEdit(diskCacheKey) {
|
||||
it.bodyAsChannel().copyTo(data.toFile())
|
||||
}
|
||||
}
|
||||
check(success)
|
||||
// diskcache snapshot MUST exist here
|
||||
requireNotNull(diskCache.openSnapshot(diskCacheKey))
|
||||
}
|
||||
return SourceFetchResult(
|
||||
source = ImageSource(snapshot.data, diskCache.fileSystem, diskCacheKey, snapshot),
|
||||
mimeType = getMimeType(data),
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun ComponentRegistry.Builder.installKtorHttpUriFetcher() = add { data: Uri, options, loader ->
|
||||
if (data.scheme != "http" && data.scheme != "https") return@add null
|
||||
KtorHttpUriFetcher(data.toString(), options, loader)
|
||||
}
|
||||
|
||||
private fun getMimeType(url: String) = MimeTypeMap.getSingleton().getMimeTypeFromUrl(url)
|
||||
|
||||
private fun MimeTypeMap.getMimeTypeFromUrl(url: String?): String? {
|
||||
if (url.isNullOrBlank()) return null
|
||||
val extension = url
|
||||
.substringBeforeLast('#') // Strip the fragment.
|
||||
.substringBeforeLast('?') // Strip the query.
|
||||
.substringAfterLast('/') // Get the last path segment.
|
||||
.substringAfterLast('.', missingDelimiterValue = "") // Get the file extension.
|
||||
return getMimeTypeFromExtension(extension)
|
||||
}
|
||||
File renamed without changes.
@@ -24,11 +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 eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.LocalTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
@@ -48,18 +48,12 @@ class DailyCheckWork(context: Context, workerParams: WorkerParameters) :
|
||||
}
|
||||
}
|
||||
|
||||
val schedHour
|
||||
get() = Settings.requestNewsTimerHour.takeUnless { it == -1 } ?: 0
|
||||
|
||||
val schedMinute
|
||||
get() = Settings.requestNewsTimerMinute.takeUnless { it == -1 } ?: 0
|
||||
|
||||
private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
|
||||
val now = Clock.System.now()
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
val whenToWork = LocalDateTime(
|
||||
now.toLocalDateTime(timeZone).date,
|
||||
LocalTime(schedHour, schedMinute),
|
||||
LocalTime.fromSecondOfDay(Settings.requestNewsTime),
|
||||
).toInstant(timeZone)
|
||||
val initialDelay = (whenToWork - now).run { if (isNegative()) plus(1.days) else this }
|
||||
val constraints = Constraints.Builder()
|
||||
@@ -77,7 +71,7 @@ fun updateDailyCheckWork(context: Context) {
|
||||
if (Settings.requestNews) {
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
|
||||
getDailyCheckWorkRequest(),
|
||||
)
|
||||
} else {
|
||||
@@ -85,17 +79,18 @@ fun updateDailyCheckWork(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
val today
|
||||
get() = (Clock.System.now().epochSeconds / 86400).toInt()
|
||||
|
||||
suspend fun checkDawn() = runCatching {
|
||||
val now = Clock.System.now()
|
||||
val last = Instant.fromEpochSeconds(Settings.lastDawnTime)
|
||||
if (EhCookieStore.hasSignedIn() && now > last + 1.days) {
|
||||
if (EhCookieStore.hasSignedIn() && Settings.lastDawnDays != today) {
|
||||
EhEngine.getNews(true)?.let {
|
||||
Settings.lastDawnTime = now.epochSeconds
|
||||
Settings.lastDawnDays = today
|
||||
showEventNotification(it)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(WORK_NAME, it)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@@ -124,6 +119,6 @@ fun showEventNotification(html: String) {
|
||||
runCatching {
|
||||
notificationManager.notify(1, msg.build())
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(WORK_NAME, it)
|
||||
}
|
||||
}
|
||||
@@ -2,171 +2,36 @@ package com.hippo.ehviewer.dao
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.DeleteTable
|
||||
import androidx.room.RenameColumn
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
|
||||
@DeleteTable(tableName = "BOOKMARKS")
|
||||
class DropBookMarkDao : AutoMigrationSpec
|
||||
|
||||
@RenameColumn(tableName = "HISTORY", fromColumnName = "MODE", toColumnName = "FAVORITE_SLOT")
|
||||
class HistoryMigration : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("UPDATE HISTORY SET FAVORITE_SLOT = FAVORITE_SLOT - 2")
|
||||
}
|
||||
}
|
||||
|
||||
class AddPositionColumn : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
val needMigrationTables = arrayOf(
|
||||
"QUICK_SEARCH",
|
||||
"DOWNLOAD_LABELS",
|
||||
"DOWNLOADS",
|
||||
)
|
||||
needMigrationTables.forEach { table ->
|
||||
// TODO: Rewrite this with row_number() when min sdk is 30 (SQLite 3.28.0)
|
||||
db.execSQL("UPDATE $table SET POSITION = (SELECT COUNT(*) FROM $table T WHERE T.TIME < $table.TIME)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteColumn(tableName = "QUICK_SEARCH", columnName = "TIME")
|
||||
@DeleteColumn(tableName = "DOWNLOAD_LABELS", columnName = "TIME")
|
||||
class DropTimeColumn : AutoMigrationSpec
|
||||
|
||||
class AddGalleryTable : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
val needMigrationTables = arrayOf(
|
||||
"HISTORY",
|
||||
"DOWNLOADS",
|
||||
"LOCAL_FAVORITES",
|
||||
)
|
||||
needMigrationTables.forEachIndexed { index, table ->
|
||||
db.execSQL(
|
||||
"INSERT OR IGNORE INTO GALLERIES " +
|
||||
"SELECT GID, TOKEN, TITLE, TITLE_JPN, THUMB, CATEGORY, POSTED, UPLOADER, RATING, SIMPLE_LANGUAGE, " +
|
||||
"${if (index == 0) "FAVORITE_SLOT" else NOT_FAVORITED} FROM $table",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "TOKEN")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "TITLE")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "TITLE_JPN")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "THUMB")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "CATEGORY")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "POSTED")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "UPLOADER")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "RATING")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "SIMPLE_LANGUAGE")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "FAVORITE_SLOT")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TOKEN")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE_JPN")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "THUMB")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "CATEGORY")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "POSTED")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "UPLOADER")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "RATING")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "SIMPLE_LANGUAGE")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "TOKEN")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE_JPN")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "THUMB")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "CATEGORY")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "POSTED")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "UPLOADER")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "RATING")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "SIMPLE_LANGUAGE")
|
||||
class GalleryTableMigration : AutoMigrationSpec
|
||||
|
||||
class ThumbKeyMigration : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
val needMigrationTables = arrayOf(
|
||||
"DOWNLOADS",
|
||||
"HISTORY",
|
||||
"LOCAL_FAVORITES",
|
||||
)
|
||||
val prefixToRemove = arrayOf(
|
||||
"https://ehgt.org/",
|
||||
"https://s.exhentai.org/t/",
|
||||
"https://exhentai.org/t/",
|
||||
)
|
||||
needMigrationTables.forEach { table ->
|
||||
prefixToRemove.forEach { prefix ->
|
||||
db.execSQL("UPDATE $table SET thumb = SUBSTR(thumb ,LENGTH('$prefix') + 1) WHERE thumb LIKE '$prefix%'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Database(
|
||||
entities = [BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class, Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, QuickSearch::class],
|
||||
version = 16,
|
||||
entities = [
|
||||
BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class,
|
||||
Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, ProgressInfo::class, QuickSearch::class,
|
||||
GalleryEntityFts::class,
|
||||
],
|
||||
version = 22,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(
|
||||
from = 4,
|
||||
to = 5,
|
||||
spec = ThumbKeyMigration::class,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 5,
|
||||
to = 6,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 6,
|
||||
to = 7,
|
||||
spec = DropBookMarkDao::class,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 7,
|
||||
to = 8,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 8,
|
||||
to = 9,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 9,
|
||||
to = 10,
|
||||
spec = HistoryMigration::class,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 10,
|
||||
to = 11,
|
||||
spec = AddPositionColumn::class,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 11,
|
||||
to = 12,
|
||||
spec = DropTimeColumn::class,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 12,
|
||||
to = 13,
|
||||
spec = AddGalleryTable::class,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 13,
|
||||
to = 14,
|
||||
spec = GalleryTableMigration::class,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 14,
|
||||
to = 15,
|
||||
),
|
||||
AutoMigration(
|
||||
from = 15,
|
||||
to = 16,
|
||||
),
|
||||
AutoMigration(from = 4, to = 5, spec = Schema4to5::class),
|
||||
AutoMigration(from = 5, to = 6),
|
||||
AutoMigration(from = 6, to = 7, spec = Schema6to7::class),
|
||||
AutoMigration(from = 7, to = 8),
|
||||
AutoMigration(from = 8, to = 9),
|
||||
AutoMigration(from = 9, to = 10, spec = Schema9to10::class),
|
||||
AutoMigration(from = 10, to = 11, spec = Schema10to11::class),
|
||||
AutoMigration(from = 11, to = 12, spec = Schema11to12::class),
|
||||
AutoMigration(from = 12, to = 13, spec = Schema12to13::class),
|
||||
AutoMigration(from = 13, to = 14, spec = Schema13to14::class),
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17),
|
||||
AutoMigration(from = 18, to = 19, spec = Schema18to19::class),
|
||||
AutoMigration(from = 19, to = 20),
|
||||
AutoMigration(from = 20, to = 21, spec = Schema20to21::class),
|
||||
AutoMigration(from = 21, to = 22, spec = Schema21to22::class),
|
||||
],
|
||||
)
|
||||
@TypeConverters(FilterModeConverter::class)
|
||||
@@ -178,6 +43,7 @@ abstract class EhDatabase : RoomDatabase() {
|
||||
abstract fun filterDao(): FilterDao
|
||||
abstract fun historyDao(): HistoryDao
|
||||
abstract fun localFavoritesDao(): LocalFavoritesDao
|
||||
abstract fun progressDao(): ProgressDao
|
||||
abstract fun quickSearchDao(): QuickSearchDao
|
||||
}
|
||||
|
||||
@@ -185,10 +51,7 @@ abstract class EhDatabase : RoomDatabase() {
|
||||
entities = [Search::class],
|
||||
version = 2,
|
||||
autoMigrations = [
|
||||
AutoMigration(
|
||||
from = 1,
|
||||
to = 2,
|
||||
),
|
||||
AutoMigration(from = 1, to = 2),
|
||||
],
|
||||
)
|
||||
abstract class SearchDatabase : RoomDatabase() {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.hippo.ehviewer.dao
|
||||
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.DeleteTable
|
||||
import androidx.room.RenameColumn
|
||||
import androidx.room.RenameTable
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
|
||||
class Schema4to5 : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
val needMigrationTables = arrayOf(
|
||||
"DOWNLOADS",
|
||||
"HISTORY",
|
||||
"LOCAL_FAVORITES",
|
||||
)
|
||||
val prefixToRemove = arrayOf(
|
||||
"https://ehgt.org/",
|
||||
"https://s.exhentai.org/t/",
|
||||
"https://exhentai.org/t/",
|
||||
)
|
||||
needMigrationTables.forEach { table ->
|
||||
prefixToRemove.forEach { prefix ->
|
||||
db.execSQL("UPDATE $table SET thumb = SUBSTR(thumb ,LENGTH('$prefix') + 1) WHERE thumb LIKE '$prefix%'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteTable(tableName = "BOOKMARKS")
|
||||
class Schema6to7 : AutoMigrationSpec
|
||||
|
||||
@RenameColumn(tableName = "HISTORY", fromColumnName = "MODE", toColumnName = "FAVORITE_SLOT")
|
||||
class Schema9to10 : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("UPDATE HISTORY SET FAVORITE_SLOT = FAVORITE_SLOT - 2")
|
||||
}
|
||||
}
|
||||
|
||||
class Schema10to11 : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
val needMigrationTables = arrayOf(
|
||||
"QUICK_SEARCH",
|
||||
"DOWNLOAD_LABELS",
|
||||
"DOWNLOADS",
|
||||
)
|
||||
needMigrationTables.forEach { table ->
|
||||
// TODO: Rewrite this with row_number() when min sdk is 30 (SQLite 3.28.0)
|
||||
db.execSQL("UPDATE $table SET POSITION = (SELECT COUNT(*) FROM $table T WHERE T.TIME < $table.TIME)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteColumn(tableName = "QUICK_SEARCH", columnName = "TIME")
|
||||
@DeleteColumn(tableName = "DOWNLOAD_LABELS", columnName = "TIME")
|
||||
class Schema11to12 : AutoMigrationSpec
|
||||
|
||||
class Schema12to13 : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
val needMigrationTables = arrayOf(
|
||||
"HISTORY",
|
||||
"DOWNLOADS",
|
||||
"LOCAL_FAVORITES",
|
||||
)
|
||||
needMigrationTables.forEachIndexed { index, table ->
|
||||
db.execSQL(
|
||||
"INSERT OR IGNORE INTO GALLERIES " +
|
||||
"SELECT GID, TOKEN, TITLE, TITLE_JPN, THUMB, CATEGORY, POSTED, UPLOADER, RATING, SIMPLE_LANGUAGE, " +
|
||||
"${if (index == 0) "FAVORITE_SLOT" else NOT_FAVORITED} FROM $table",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "TOKEN")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "TITLE")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "TITLE_JPN")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "THUMB")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "CATEGORY")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "POSTED")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "UPLOADER")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "RATING")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "SIMPLE_LANGUAGE")
|
||||
@DeleteColumn(tableName = "HISTORY", columnName = "FAVORITE_SLOT")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TOKEN")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE_JPN")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "THUMB")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "CATEGORY")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "POSTED")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "UPLOADER")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "RATING")
|
||||
@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "SIMPLE_LANGUAGE")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "TOKEN")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE_JPN")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "THUMB")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "CATEGORY")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "POSTED")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "UPLOADER")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "RATING")
|
||||
@DeleteColumn(tableName = "DOWNLOADS", columnName = "SIMPLE_LANGUAGE")
|
||||
class Schema13to14 : AutoMigrationSpec
|
||||
|
||||
class Schema17to18 : Migration(17, 18) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `_new_GALLERIES` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))")
|
||||
db.execSQL("INSERT INTO `_new_GALLERIES` (`GID`,`TOKEN`,`TITLE`,`TITLE_JPN`,`THUMB`,`CATEGORY`,`POSTED`,`UPLOADER`,`RATING`,`SIMPLE_LANGUAGE`,`FAVORITE_SLOT`) SELECT `GID`,`TOKEN`,`TITLE`,`TITLE_JPN`,`THUMB`,`CATEGORY`,`POSTED`,`UPLOADER`,`RATING`,`SIMPLE_LANGUAGE`,`FAVORITE_SLOT` FROM `GALLERIES` WHERE `TOKEN` IS NOT NULL")
|
||||
db.execSQL("DROP TABLE `GALLERIES`")
|
||||
db.execSQL("ALTER TABLE `_new_GALLERIES` RENAME TO `GALLERIES`")
|
||||
}
|
||||
}
|
||||
|
||||
@RenameColumn(tableName = "DOWNLOADS", fromColumnName = "POSITION", toColumnName = "TIME")
|
||||
class Schema18to19 : AutoMigrationSpec
|
||||
|
||||
class Schema20to21 : AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("INSERT INTO GalleryFts(GalleryFts) VALUES('rebuild')")
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual tables can't be altered, so we use rename to recreate it
|
||||
@RenameTable(fromTableName = "GalleryFts", toTableName = "GALLERIES_FTS")
|
||||
class Schema21to22 : AutoMigrationSpec
|
||||
@@ -42,9 +42,6 @@ data class DownloadEntity(
|
||||
@ColumnInfo(name = "LABEL", index = true)
|
||||
override var label: String? = null,
|
||||
|
||||
@ColumnInfo(name = "POSITION")
|
||||
override var position: Int = 0,
|
||||
|
||||
@Ignore
|
||||
override var speed: Long = 0,
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import androidx.room.Update
|
||||
|
||||
@Dao
|
||||
interface DownloadLabelDao {
|
||||
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION ASC")
|
||||
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION")
|
||||
suspend fun list(): List<DownloadLabel>
|
||||
|
||||
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION ASC LIMIT :limit OFFSET :offset")
|
||||
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION LIMIT :limit OFFSET :offset")
|
||||
suspend fun list(offset: Int, limit: Int): List<DownloadLabel>
|
||||
|
||||
@Query("UPDATE DOWNLOAD_LABELS SET POSITION = POSITION - 1 WHERE POSITION > :position")
|
||||
|
||||
@@ -3,23 +3,26 @@ package com.hippo.ehviewer.dao
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.MapColumn
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface DownloadsDao {
|
||||
@Query("SELECT * FROM DOWNLOADS ORDER BY POSITION DESC")
|
||||
@Suppress("ktlint:standard:annotation")
|
||||
@Query("SELECT LABEL, COUNT(*) AS COUNT FROM DOWNLOADS LEFT JOIN DOWNLOAD_LABELS USING(LABEL) GROUP BY LABEL")
|
||||
fun count(): Flow<Map<@MapColumn("LABEL") String?, @MapColumn("COUNT") Int>>
|
||||
|
||||
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME")
|
||||
suspend fun list(): List<DownloadEntity>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM DOWNLOADS LEFT JOIN DOWNLOAD_DIRNAME USING(GID) ORDER BY POSITION DESC")
|
||||
@Query("SELECT * FROM DOWNLOADS LEFT JOIN DOWNLOAD_DIRNAME USING(GID) ORDER BY TIME DESC")
|
||||
suspend fun joinList(): List<DownloadInfo>
|
||||
|
||||
@Query("UPDATE DOWNLOADS SET POSITION = POSITION - :offset WHERE POSITION > :position")
|
||||
suspend fun fill(position: Int, offset: Int = 1)
|
||||
|
||||
@Update
|
||||
suspend fun update(downloadInfo: List<DownloadEntity>)
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ interface GalleryDao {
|
||||
@Update
|
||||
suspend fun update(galleryInfo: BaseGalleryInfo)
|
||||
|
||||
@Update
|
||||
suspend fun update(galleryInfoList: List<BaseGalleryInfo>)
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(galleryInfo: BaseGalleryInfo)
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.hippo.ehviewer.dao
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Fts4
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
|
||||
@Fts4(contentEntity = BaseGalleryInfo::class)
|
||||
@Entity(tableName = "GALLERIES_FTS")
|
||||
class GalleryEntityFts(
|
||||
@ColumnInfo(name = "TITLE")
|
||||
val title: String,
|
||||
|
||||
@ColumnInfo(name = "TITLE_JPN")
|
||||
val titleJpn: String,
|
||||
|
||||
@ColumnInfo(name = "SIMPLE_TAGS")
|
||||
val simpleTags: String?,
|
||||
)
|
||||
@@ -5,19 +5,23 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||
import androidx.room.Upsert
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
|
||||
@Dao
|
||||
interface HistoryDao {
|
||||
@Query("SELECT * FROM HISTORY ORDER BY TIME DESC")
|
||||
@Query("SELECT * FROM HISTORY ORDER BY TIME")
|
||||
suspend fun list(): List<HistoryInfo>
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Query("SELECT * FROM HISTORY JOIN GALLERIES USING(GID) ORDER BY TIME DESC")
|
||||
@Query("SELECT GALLERIES.* FROM HISTORY JOIN GALLERIES USING(GID) ORDER BY TIME DESC")
|
||||
fun joinListLazy(): PagingSource<Int, BaseGalleryInfo>
|
||||
|
||||
@Query(
|
||||
"""SELECT GALLERIES.* FROM HISTORY JOIN GALLERIES USING(GID)
|
||||
JOIN GALLERIES_FTS ON GALLERIES.rowid = docid WHERE GALLERIES_FTS MATCH :title ORDER BY TIME DESC""",
|
||||
)
|
||||
fun joinListLazy(title: String): PagingSource<Int, BaseGalleryInfo>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertOrIgnore(historyInfoList: List<HistoryInfo>)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||
import androidx.room.Upsert
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -15,17 +14,21 @@ interface LocalFavoritesDao {
|
||||
@Query("SELECT COUNT(*) FROM LOCAL_FAVORITES")
|
||||
fun count(): Flow<Int>
|
||||
|
||||
@Query("SELECT * FROM LOCAL_FAVORITES ORDER BY TIME DESC")
|
||||
@Query("SELECT * FROM LOCAL_FAVORITES ORDER BY TIME")
|
||||
suspend fun list(): List<LocalFavoriteInfo>
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Query("SELECT * FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) ORDER BY TIME DESC")
|
||||
@Query("SELECT GALLERIES.* FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) ORDER BY TIME DESC")
|
||||
fun joinListLazy(): PagingSource<Int, BaseGalleryInfo>
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Query("SELECT * FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) WHERE TITLE LIKE :title ORDER BY TIME DESC")
|
||||
@Query(
|
||||
"""SELECT GALLERIES.* FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID)
|
||||
JOIN GALLERIES_FTS ON GALLERIES.rowid = docid WHERE GALLERIES_FTS MATCH :title ORDER BY TIME DESC""",
|
||||
)
|
||||
fun joinListLazy(title: String): PagingSource<Int, BaseGalleryInfo>
|
||||
|
||||
@Query("SELECT GALLERIES.* FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) ORDER BY RANDOM() LIMIT 1")
|
||||
suspend fun random(): BaseGalleryInfo
|
||||
|
||||
@Query("SELECT EXISTS(SELECT * FROM LOCAL_FAVORITES WHERE GID = :gid)")
|
||||
suspend fun contains(gid: Long): Boolean
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.hippo.ehviewer.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface ProgressDao {
|
||||
@Query("SELECT IFNULL(PAGE, 0) FROM PROGRESS WHERE GID = :gid")
|
||||
suspend fun getPage(gid: Long): Int
|
||||
|
||||
@Query("SELECT IFNULL(PAGE, 0) FROM PROGRESS WHERE GID = :gid")
|
||||
fun getPageFlow(gid: Long): Flow<Int>
|
||||
|
||||
@Query("SELECT * FROM PROGRESS")
|
||||
suspend fun list(): List<ProgressInfo>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertOrIgnore(progress: List<ProgressInfo>)
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(progress: ProgressInfo)
|
||||
|
||||
@Query("DELETE FROM PROGRESS")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.hippo.ehviewer.dao
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
|
||||
@Entity(
|
||||
tableName = "PROGRESS",
|
||||
foreignKeys = [
|
||||
ForeignKey(BaseGalleryInfo::class, ["GID"], ["GID"], onDelete = ForeignKey.CASCADE),
|
||||
],
|
||||
)
|
||||
class ProgressInfo(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "GID")
|
||||
val gid: Long,
|
||||
|
||||
@ColumnInfo(name = "PAGE")
|
||||
val page: Int,
|
||||
)
|
||||
@@ -8,10 +8,10 @@ import androidx.room.Update
|
||||
|
||||
@Dao
|
||||
interface QuickSearchDao {
|
||||
@Query("SELECT * FROM QUICK_SEARCH ORDER BY POSITION ASC")
|
||||
@Query("SELECT * FROM QUICK_SEARCH ORDER BY POSITION")
|
||||
suspend fun list(): List<QuickSearch>
|
||||
|
||||
@Query("SELECT * FROM QUICK_SEARCH ORDER BY POSITION ASC LIMIT :limit OFFSET :offset")
|
||||
@Query("SELECT * FROM QUICK_SEARCH ORDER BY POSITION LIMIT :limit OFFSET :offset")
|
||||
suspend fun list(offset: Int, limit: Int): List<QuickSearch>
|
||||
|
||||
@Query("UPDATE QUICK_SEARCH SET POSITION = POSITION - 1 WHERE POSITION > :position")
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -22,7 +22,6 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.core.app.ActivityCompat
|
||||
@@ -40,6 +39,7 @@ import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.SimpleHandler
|
||||
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -148,9 +148,9 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
|
||||
ACTION_CLEAR -> {
|
||||
clear()
|
||||
checkStopSelf()
|
||||
}
|
||||
}
|
||||
checkStopSelf()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = error("No bindService")
|
||||
@@ -339,7 +339,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
sItemTitleArray.valueAt(0),
|
||||
)
|
||||
} else {
|
||||
Log.d("TAG", "WTF, sItemTitleArray is null")
|
||||
logcat { "WTF, sItemTitleArray is null" }
|
||||
getString(R.string.error_unknown)
|
||||
}
|
||||
needStyle = false
|
||||
@@ -355,7 +355,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
sItemTitleArray.valueAt(0),
|
||||
)
|
||||
} else {
|
||||
Log.d("TAG", "WTF, sItemTitleArray is null")
|
||||
logcat { "WTF, sItemTitleArray is null" }
|
||||
getString(R.string.error_unknown)
|
||||
}
|
||||
needStyle = false
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.hippo.ehviewer.download
|
||||
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
|
||||
data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel: Boolean = false) {
|
||||
val flag
|
||||
get() = groupByDownloadLabel.compareTo(false) shl 4 or (field.value shl 1) or order.value
|
||||
|
||||
enum class Field(val value: Int) {
|
||||
GID(0),
|
||||
TITLE(1),
|
||||
TIME(2),
|
||||
LABEL(3),
|
||||
PAGES(4),
|
||||
}
|
||||
|
||||
enum class Order(val value: Int) {
|
||||
ASC(0),
|
||||
DESC(1),
|
||||
}
|
||||
|
||||
fun comparator(): Comparator<DownloadInfo> {
|
||||
var comparator = when (field) {
|
||||
Field.GID -> compareBy { it.gid }
|
||||
Field.TITLE, Field.LABEL -> compareBy { EhUtils.getSuitableTitle(it) }
|
||||
Field.TIME -> compareBy { it.time }
|
||||
Field.PAGES -> compareBy<DownloadInfo> { it.pages }.thenBy { EhUtils.getSuitableTitle(it) }
|
||||
}
|
||||
if (order == Order.DESC) {
|
||||
comparator = comparator.reversed()
|
||||
}
|
||||
if (groupByDownloadLabel) {
|
||||
comparator = compareBy<DownloadInfo> { it.label }.then(comparator)
|
||||
}
|
||||
return comparator
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(flag: Int): SortMode {
|
||||
val field = flag and 0xF shr 1
|
||||
val order = flag and 0x1
|
||||
val groupByDownloadLabel = flag shr 4 == 1
|
||||
require(field in Field.entries.indices && order in Order.entries.indices)
|
||||
return SortMode(Field.entries[field], Order.entries[order], groupByDownloadLabel)
|
||||
}
|
||||
val Default = SortMode(Field.TIME, Order.DESC)
|
||||
val All = listOf(
|
||||
SortMode(Field.TIME, Order.DESC),
|
||||
SortMode(Field.TIME, Order.ASC),
|
||||
SortMode(Field.GID, Order.DESC),
|
||||
SortMode(Field.GID, Order.ASC),
|
||||
SortMode(Field.TITLE, Order.ASC),
|
||||
SortMode(Field.TITLE, Order.DESC),
|
||||
SortMode(Field.PAGES, Order.ASC),
|
||||
SortMode(Field.PAGES, Order.DESC),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,34 +13,27 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:RequiresApi(Build.VERSION_CODES.P)
|
||||
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hippo.ehviewer.Settings.archivePasswds
|
||||
import com.hippo.ehviewer.image.ByteBufferSource
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.jni.closeArchive
|
||||
import com.hippo.ehviewer.jni.extractToByteBuffer
|
||||
import com.hippo.ehviewer.jni.extractToFd
|
||||
import com.hippo.ehviewer.jni.getFilename
|
||||
import com.hippo.ehviewer.jni.getExtension
|
||||
import com.hippo.ehviewer.jni.needPassword
|
||||
import com.hippo.ehviewer.jni.openArchive
|
||||
import com.hippo.ehviewer.jni.providePassword
|
||||
import com.hippo.ehviewer.jni.releaseByteBuffer
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.displayPath
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -52,17 +45,21 @@ import moe.tarsin.coroutines.withLock
|
||||
typealias PasswdInvalidator = (String) -> Boolean
|
||||
typealias PasswdProvider = suspend (PasswdInvalidator) -> String
|
||||
|
||||
class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider: PasswdProvider) : PageLoader2(), CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
class ArchivePageLoader(
|
||||
private val file: UniFile,
|
||||
gid: Long = 0,
|
||||
startPage: Int = 0,
|
||||
passwdProvider: PasswdProvider? = null,
|
||||
) : PageLoader2(gid, startPage) {
|
||||
private lateinit var pfd: ParcelFileDescriptor
|
||||
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
||||
Log.d(DEBUG_TAG, "Open archive $uri")
|
||||
pfd = context.contentResolver.openFileDescriptor(uri, "r")!!
|
||||
size = openArchive(pfd.fd, pfd.statSize)
|
||||
logcat(DEBUG_TAG) { "Open archive ${file.uri.displayPath}" }
|
||||
pfd = file.openFileDescriptor("r")
|
||||
size = openArchive(pfd.fd, pfd.statSize, gid == 0L)
|
||||
if (size == 0) {
|
||||
return@launch
|
||||
}
|
||||
if (needPassword()) {
|
||||
if (passwdProvider != null && needPassword()) {
|
||||
archivePasswds?.forEach {
|
||||
it ?: return@forEach
|
||||
if (providePassword(it)) return@launch
|
||||
@@ -76,15 +73,15 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider:
|
||||
private set
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
hostJob.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
cancel()
|
||||
super.stop()
|
||||
closeArchive()
|
||||
pfd.close()
|
||||
Log.d(DEBUG_TAG, "Close archive $uri successfully!")
|
||||
super.stop()
|
||||
logcat(DEBUG_TAG) { "Close archive ${file.uri.displayPath} successfully!" }
|
||||
}
|
||||
|
||||
private val mJobMap = hashMapOf<Int, Job>()
|
||||
@@ -107,10 +104,9 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider:
|
||||
}
|
||||
|
||||
private suspend fun doRealWork(index: Int) {
|
||||
val buffer = extractToByteBuffer(index)
|
||||
buffer ?: return
|
||||
val buffer = extractToByteBuffer(index) ?: return notifyPageFailed(index, null)
|
||||
check(buffer.isDirect)
|
||||
val src = object : Image.ByteBufferSource {
|
||||
val src = object : ByteBufferSource {
|
||||
override val source: ByteBuffer = buffer
|
||||
|
||||
override fun close() {
|
||||
@@ -139,7 +135,7 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider:
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
hostJob.join()
|
||||
return size != 0
|
||||
return super.awaitReady() && size != 0
|
||||
}
|
||||
|
||||
override val isReady: Boolean
|
||||
@@ -149,30 +145,23 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider:
|
||||
mJobMap[index]?.cancel()
|
||||
}
|
||||
|
||||
override fun getImageFilename(index: Int): String {
|
||||
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index))!!
|
||||
override val title by lazy {
|
||||
FileUtils.getNameFromFilename(file.name)!!
|
||||
}
|
||||
|
||||
override fun getImageFilenameWithExtension(index: Int): String {
|
||||
return FileUtils.sanitizeFilename(getFilename(index))
|
||||
override fun getImageExtension(index: Int): String {
|
||||
return getExtension(index)
|
||||
}
|
||||
|
||||
override fun save(index: Int, file: UniFile): Boolean {
|
||||
runCatching {
|
||||
return runCatching {
|
||||
file.openFileDescriptor("w").use {
|
||||
extractToFd(index, it.fd)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
return false
|
||||
}.getOrElse {
|
||||
logcat(it)
|
||||
false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun save(index: Int, dir: UniFile, filename: String): UniFile {
|
||||
val dst = dir.subFile(filename)
|
||||
save(index, dst!!)
|
||||
return dst
|
||||
}
|
||||
|
||||
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
|
||||
|
||||
@@ -15,19 +15,21 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||
import com.hippo.ehviewer.util.SimpleHandler
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.unifile.UniFile
|
||||
import java.util.Locale
|
||||
|
||||
class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpiderListener {
|
||||
class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
||||
PageLoader2(mGalleryInfo.gid, startPage), OnSpiderListener {
|
||||
private lateinit var mSpiderQueen: SpiderQueen
|
||||
override fun start() {
|
||||
super.start()
|
||||
if (!::mSpiderQueen.isInitialized) {
|
||||
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
|
||||
mSpiderQueen.addOnSpiderListener(this)
|
||||
@@ -37,40 +39,21 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpi
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
mSpiderQueen.removeOnSpiderListener(this)
|
||||
// Activity recreate may called, so wait 0.5s
|
||||
SimpleHandler.postDelayed(ReleaseTask(mSpiderQueen), 500)
|
||||
releaseSpiderQueen(mSpiderQueen, SpiderQueen.MODE_READ)
|
||||
}
|
||||
|
||||
override val startPage
|
||||
get() = mSpiderQueen.startPage
|
||||
|
||||
override fun getImageFilename(index: Int): String {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"%d-%s-%08d",
|
||||
mGalleryInfo.gid,
|
||||
mGalleryInfo.token,
|
||||
index + 1,
|
||||
)
|
||||
override val title by lazy {
|
||||
FileUtils.sanitizeFilename(EhUtils.getSuitableTitle(mGalleryInfo))
|
||||
}
|
||||
|
||||
override fun getImageFilenameWithExtension(index: Int): String {
|
||||
val filename = getImageFilename(index)
|
||||
return mSpiderQueen.getExtension(index)?.let { "$filename.$it" } ?: filename
|
||||
override fun getImageExtension(index: Int): String {
|
||||
return mSpiderQueen.getExtension(index)!!
|
||||
}
|
||||
|
||||
override fun save(index: Int, file: UniFile): Boolean {
|
||||
return mSpiderQueen.save(index, file)
|
||||
}
|
||||
|
||||
override fun save(index: Int, dir: UniFile, filename: String): UniFile? {
|
||||
return mSpiderQueen.save(index, dir, filename)
|
||||
}
|
||||
|
||||
override fun putStartPage(page: Int) {
|
||||
mSpiderQueen.putStartPage(page)
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = mSpiderQueen.size
|
||||
|
||||
@@ -83,7 +66,7 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpi
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
return mSpiderQueen.awaitReady()
|
||||
return super.awaitReady() && mSpiderQueen.awaitReady()
|
||||
}
|
||||
|
||||
override val isReady: Boolean
|
||||
@@ -131,11 +114,4 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpi
|
||||
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
|
||||
mSpiderQueen.preloadPages(pages, pair)
|
||||
}
|
||||
|
||||
private class ReleaseTask(private var mSpiderQueen: SpiderQueen?) : Runnable {
|
||||
override fun run() {
|
||||
mSpiderQueen?.let { releaseSpiderQueen(it, SpiderQueen.MODE_READ) }
|
||||
mSpiderQueen = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,34 +15,60 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class PageLoader2 : PageLoader() {
|
||||
open val startPage: Int
|
||||
get() = 0
|
||||
abstract class PageLoader2(private val gid: Long, var startPage: Int) : PageLoader(), CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
private val progressJob = launch(start = CoroutineStart.LAZY) {
|
||||
if (startPage == -1) {
|
||||
startPage = EhDB.getReadProgress(gid)
|
||||
}
|
||||
}
|
||||
|
||||
open fun putStartPage(page: Int) {}
|
||||
override fun start() {
|
||||
super.start()
|
||||
progressJob.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return without extension
|
||||
*/
|
||||
abstract fun getImageFilename(index: Int): String
|
||||
@CallSuper
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
progressJob.join()
|
||||
return startPage != -1
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
if (gid != 0L) {
|
||||
launch {
|
||||
EhDB.putReadProgress(gid, startPage)
|
||||
this@PageLoader2.cancel()
|
||||
}
|
||||
} else {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract val title: String
|
||||
|
||||
protected abstract fun getImageExtension(index: Int): String
|
||||
|
||||
fun getImageFilename(index: Int): String {
|
||||
return "$title - ${index + 1}.${getImageExtension(index)}"
|
||||
}
|
||||
|
||||
/**
|
||||
* @return with extension
|
||||
*/
|
||||
abstract fun getImageFilenameWithExtension(index: Int): String
|
||||
abstract fun save(index: Int, file: UniFile): Boolean
|
||||
|
||||
/**
|
||||
* @param filename with extension
|
||||
*/
|
||||
abstract fun save(index: Int, dir: UniFile, filename: String): UniFile?
|
||||
fun saveToDir(index: Int, dir: UniFile): UniFile? {
|
||||
val filename = getImageFilename(index)
|
||||
return (dir / filename).takeIf { save(index, it) }
|
||||
}
|
||||
}
|
||||
|
||||
val SUPPORT_IMAGE_EXTENSIONS = arrayOf(
|
||||
".jpg",
|
||||
".png",
|
||||
".gif",
|
||||
)
|
||||
@@ -14,6 +14,8 @@ object EhIcons {
|
||||
val Default = Filled
|
||||
}
|
||||
|
||||
object Reader
|
||||
|
||||
val Default = Filled
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.hippo.ehviewer.icons.filled
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
val EhIcons.Filled.Crop by unsafeLazy {
|
||||
materialIcon(name = "Crop") {
|
||||
materialPath {
|
||||
moveTo(17.0F, 15.0F)
|
||||
horizontalLineToRelative(2.0F)
|
||||
verticalLineTo(7.0F)
|
||||
curveToRelative(0.0F, -1.1F, -0.9F, -2.0F, -2.0F, -2.0F)
|
||||
horizontalLineTo(9.0F)
|
||||
verticalLineToRelative(2.0F)
|
||||
horizontalLineToRelative(8.0F)
|
||||
verticalLineToRelative(8.0F)
|
||||
close()
|
||||
moveTo(7.0F, 17.0F)
|
||||
verticalLineTo(1.0F)
|
||||
horizontalLineTo(5.0F)
|
||||
verticalLineToRelative(4.0F)
|
||||
horizontalLineTo(1.0F)
|
||||
verticalLineToRelative(2.0F)
|
||||
horizontalLineToRelative(4.0F)
|
||||
verticalLineToRelative(10.0F)
|
||||
curveToRelative(0.0F, 1.1F, 0.9F, 2.0F, 2.0F, 2.0F)
|
||||
horizontalLineToRelative(10.0F)
|
||||
verticalLineToRelative(4.0F)
|
||||
horizontalLineToRelative(2.0F)
|
||||
verticalLineToRelative(-4.0F)
|
||||
horizontalLineToRelative(4.0F)
|
||||
verticalLineToRelative(-2.0F)
|
||||
horizontalLineTo(7.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconCropPreview() {
|
||||
Image(imageVector = EhIcons.Default.Crop, contentDescription = null)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.hippo.ehviewer.icons.filled
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@Suppress("BooleanLiteralArgument")
|
||||
val EhIcons.Filled.CropOff by unsafeLazy {
|
||||
materialIcon(name = "CropOff") {
|
||||
materialPath {
|
||||
moveTo(7.0F, 1.0F)
|
||||
lineToRelative(-2.0F, 0.0F)
|
||||
lineToRelative(0.0F, 1.18F)
|
||||
lineToRelative(2.0F, 2.0F)
|
||||
lineToRelative(0.0F, -3.18F)
|
||||
close()
|
||||
moveTo(17.0F, 7.0F)
|
||||
verticalLineToRelative(7.18F)
|
||||
lineToRelative(0.82F, 0.82F)
|
||||
horizontalLineTo(19.0F)
|
||||
verticalLineTo(7.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, -2.0F, -2.0F)
|
||||
horizontalLineTo(9.0F)
|
||||
verticalLineTo(6.18F)
|
||||
lineTo(9.82F, 7.0F)
|
||||
close()
|
||||
moveTo(19.0001F, 19.0F)
|
||||
lineToRelative(-2.0F, -2.0F)
|
||||
horizontalLineToRelative(0.0F)
|
||||
lineTo(2.1F, 2.1F)
|
||||
lineTo(0.68F, 3.51F)
|
||||
lineTo(2.1707F, 5.0F)
|
||||
horizontalLineTo(1.0F)
|
||||
verticalLineTo(7.0F)
|
||||
horizontalLineTo(4.1718F)
|
||||
lineTo(5.0F, 7.8278F)
|
||||
verticalLineTo(17.0F)
|
||||
arcToRelative(2.0059F, 2.0059F, 0.0F, false, false, 2.0F, 2.0F)
|
||||
horizontalLineToRelative(9.1778F)
|
||||
lineTo(17.0F, 19.8218F)
|
||||
verticalLineTo(23.0F)
|
||||
horizontalLineToRelative(2.0F)
|
||||
verticalLineTo(21.8207F)
|
||||
lineTo(20.49F, 23.31F)
|
||||
lineTo(21.9F, 21.9F)
|
||||
lineToRelative(-2.9F, -2.9F)
|
||||
close()
|
||||
moveTo(7.0F, 17.0F)
|
||||
verticalLineTo(9.8268F)
|
||||
lineTo(14.1768F, 17.0F)
|
||||
close()
|
||||
moveTo(19.82F, 17.0F)
|
||||
lineToRelative(2.0F, 2.0F)
|
||||
lineToRelative(1.18F, 0.0F)
|
||||
lineToRelative(0.0F, -2.0F)
|
||||
lineToRelative(-3.18F, 0.0F)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconCropOffPreview() {
|
||||
Image(imageVector = EhIcons.Default.CropOff, contentDescription = null)
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.hippo.ehviewer.icons.filled
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.hippo.ehviewer.icons.EhIcons
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
|
||||
@@ -31,3 +34,10 @@ val EhIcons.Filled.GoTo by unsafeLazy {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun IconGoToPreview() {
|
||||
Image(imageVector = EhIcons.Default.GoTo, contentDescription = null)
|
||||
}
|
||||
Loaded 100 of 363 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user