Compare commits
229
Commits
1.11.4
...
1.11.6-RC2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70f46442c6 | ||
|
|
13cf42437f | ||
|
|
1e29f6dcb4 | ||
|
|
f25981792f | ||
|
|
934c8be3e4 | ||
|
|
b9994eacbc | ||
|
|
704ab0ee7e | ||
|
|
6d1d737f9f | ||
|
|
bc75156232 | ||
|
|
2101ea7a36 | ||
|
|
26921c8ae1 | ||
|
|
be1eecf3b5 | ||
|
|
2d4f154c9b | ||
|
|
d82cbe51ba | ||
|
|
d13ffa93ba | ||
|
|
2cce55953f | ||
|
|
07d100fcc0 | ||
|
|
cc04371181 | ||
|
|
ee50c88765 | ||
|
|
efc5b55ea7 | ||
|
|
94af66156a | ||
|
|
4861359f9e | ||
|
|
5af167dab4 | ||
|
|
db1f0bc6d7 | ||
|
|
b94a843ec5 | ||
|
|
b27d214f24 | ||
|
|
a81eb9d17b | ||
|
|
ec9b85f30f | ||
|
|
8ac2f33949 | ||
|
|
d84198ab28 | ||
|
|
34d339d9f1 | ||
|
|
bcfefc7c76 | ||
|
|
62539dc48e | ||
|
|
453ab11395 | ||
|
|
b2246b980a | ||
|
|
822a461acd | ||
|
|
25e5c0ab8e | ||
|
|
2a55db95f4 | ||
|
|
a144866182 | ||
|
|
be7a04a4cb | ||
|
|
fd1b861dab | ||
|
|
957ebd35aa | ||
|
|
29d8b11771 | ||
|
|
c87059e9d7 | ||
|
|
4babe2eec4 | ||
|
|
48fdaaca0c | ||
|
|
f0ee88b4d6 | ||
|
|
9210780b6b | ||
|
|
b479e28543 | ||
|
|
f93b98fc5e | ||
|
|
4721d1011b | ||
|
|
4da4443e67 | ||
|
|
e5343f4360 | ||
|
|
86323220dc | ||
|
|
ada8f3af75 | ||
|
|
fb940d3982 | ||
|
|
8e8729c25f | ||
|
|
ffdd063ca1 | ||
|
|
dfd5c6c70e | ||
|
|
3068aab681 | ||
|
|
e402587f8f | ||
|
|
00b1be879d | ||
|
|
e63bd49a4c | ||
|
|
8131170eb1 | ||
|
|
dc1abd3806 | ||
|
|
468fbd50da | ||
|
|
f2011f9b6f | ||
|
|
1e53a9b389 | ||
|
|
dc2504b67f | ||
|
|
2ae3e3b64c | ||
|
|
e6bdaf1172 | ||
|
|
36a66d9199 | ||
|
|
fb6fbb8df9 | ||
|
|
ef8449dc02 | ||
|
|
b59f8e9ade | ||
|
|
a49506f4a0 | ||
|
|
4ce3d25eed | ||
|
|
81b166bee1 | ||
|
|
37a587859a | ||
|
|
b7544630bb | ||
|
|
05c7a0fa39 | ||
|
|
eee7655f80 | ||
|
|
9390813c48 | ||
|
|
d032fbdd67 | ||
|
|
adcd73a590 | ||
|
|
87b405bf1f | ||
|
|
d8306e2c80 | ||
|
|
c03fe9b51e | ||
|
|
9099014d28 | ||
|
|
d7b3872486 | ||
|
|
4ae2af0bef | ||
|
|
c9bcd46b77 | ||
|
|
e531653634 | ||
|
|
a29e4c72b1 | ||
|
|
9e1f7ca051 | ||
|
|
fbbc347411 | ||
|
|
ea364536a4 | ||
|
|
575fc9e94c | ||
|
|
703c63125a | ||
|
|
b205b5e120 | ||
|
|
e27f36d2c8 | ||
|
|
d582eddc03 | ||
|
|
3b83d8744c | ||
|
|
68a5217b1c | ||
|
|
c01082b822 | ||
|
|
bb95c86320 | ||
|
|
d398926aeb | ||
|
|
7b23efb0ce | ||
|
|
7818ff197c | ||
|
|
95bfc4f8c5 | ||
|
|
c750016034 | ||
|
|
6298e501cd | ||
|
|
7c39b95025 | ||
|
|
02c1e630d1 | ||
|
|
8f3b0cc536 | ||
|
|
d0c5ef8e8a | ||
|
|
d628024637 | ||
|
|
fe1249630b | ||
|
|
cdbe3cf728 | ||
|
|
f5e36bc0cf | ||
|
|
4f9bfe13cb | ||
|
|
959da21b9d | ||
|
|
fae2b1f416 | ||
|
|
a054633d0a | ||
|
|
a4619547f1 | ||
|
|
5d53febb15 | ||
|
|
3da1587924 | ||
|
|
971d0307fe | ||
|
|
705d3afb78 | ||
|
|
bc34b4b9c1 | ||
|
|
a2ac01fcdc | ||
|
|
645ebddd4f | ||
|
|
d5d8a828ef | ||
|
|
96d4193d2d | ||
|
|
18e4afdff9 | ||
|
|
19a702ffc4 | ||
|
|
cfd44c256f | ||
|
|
4cea8d0090 | ||
|
|
30e166b29c | ||
|
|
c50f3d8c5b | ||
|
|
d9249eeedb | ||
|
|
51699cafe6 | ||
|
|
ce97976bdd | ||
|
|
7b8a12e3eb | ||
|
|
7276c37c54 | ||
|
|
4ec7de611c | ||
|
|
0295c40b02 | ||
|
|
035fa44523 | ||
|
|
60c6618db1 | ||
|
|
f35643b2b6 | ||
|
|
b25661c436 | ||
|
|
cea06f0c7e | ||
|
|
7de6a821d9 | ||
|
|
13be4e4eb1 | ||
|
|
691e7c3ba7 | ||
|
|
31aea1cdbf | ||
|
|
97d5642427 | ||
|
|
c68272c1a4 | ||
|
|
a69dc994b3 | ||
|
|
d79a71c764 | ||
|
|
35183de477 | ||
|
|
21b9fe3cb2 | ||
|
|
5762b0bfc3 | ||
|
|
3473e91747 | ||
|
|
6277d7d41d | ||
|
|
f22da2b771 | ||
|
|
3466bbd64a | ||
|
|
49fc87621c | ||
|
|
7e558f4478 | ||
|
|
307624ca9a | ||
|
|
40e66b919a | ||
|
|
cf2e0aadd5 | ||
|
|
1b523f6a78 | ||
|
|
6a9f6bb645 | ||
|
|
45602b05c6 | ||
|
|
8dc842e125 | ||
|
|
9dfc3a49fe | ||
|
|
97d4a61140 | ||
|
|
8b596e2274 | ||
|
|
123b86830b | ||
|
|
e11f5615a1 | ||
|
|
31fb2ed2b1 | ||
|
|
9c12d11c85 | ||
|
|
aea0e885c3 | ||
|
|
f34ff71145 | ||
|
|
820c16e86b | ||
|
|
a335348b1a | ||
|
|
7f134e1995 | ||
|
|
9e13c1c3db | ||
|
|
61161cef0f | ||
|
|
9ff74e789c | ||
|
|
45183df640 | ||
|
|
22f11994f5 | ||
|
|
bc7aa832b7 | ||
|
|
b32f78ea9f | ||
|
|
78002dac3a | ||
|
|
aca70354e7 | ||
|
|
d7a3471b9b | ||
|
|
e81092d42c | ||
|
|
d06df3c819 | ||
|
|
050bfea8f2 | ||
|
|
7b84c74abf | ||
|
|
009c49cd89 | ||
|
|
ca023f73f4 | ||
|
|
456dfd2aac | ||
|
|
0c0291127c | ||
|
|
fe0c74de23 | ||
|
|
df1efa7604 | ||
|
|
73122fc517 | ||
|
|
b506ba277c | ||
|
|
ea3aed8fae | ||
|
|
481bb8e7d4 | ||
|
|
0ed8c40dbf | ||
|
|
d267d7c3b0 | ||
|
|
a04206f600 | ||
|
|
8d64a6f1b2 | ||
|
|
b3f614e150 | ||
|
|
988fae56fd | ||
|
|
fa439fc62d | ||
|
|
c58b2d6156 | ||
|
|
62c56b7918 | ||
|
|
73882ab55d | ||
|
|
490ea2b521 | ||
|
|
7116871652 | ||
|
|
cca374c774 | ||
|
|
3e0dbf92d0 | ||
|
|
2747ae674c | ||
|
|
e85ec5aaba | ||
|
|
d4894e066b |
No files matched your search
+12
-15
@@ -21,7 +21,7 @@ jobs:
|
|||||||
java-version: 21
|
java-version: 21
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- 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
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -42,6 +42,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache-read-only: true
|
cache-read-only: true
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014"
|
||||||
|
|
||||||
- name: Spotless Check
|
- name: Spotless Check
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
||||||
@@ -57,8 +60,11 @@ jobs:
|
|||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 21
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014"
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- 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
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -101,12 +107,6 @@ jobs:
|
|||||||
name: default-armeabi-v7a-${{ github.sha }}
|
name: default-armeabi-v7a-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
|
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
|
||||||
|
|
||||||
- name: Upload x86
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: default-x86-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/default/release/app-default-x86-release.apk
|
|
||||||
|
|
||||||
- name: Upload mapping
|
- name: Upload mapping
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -131,8 +131,11 @@ jobs:
|
|||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 21
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014"
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- 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
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -175,12 +178,6 @@ jobs:
|
|||||||
name: marshmallow-armeabi-v7a-${{ github.sha }}
|
name: marshmallow-armeabi-v7a-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
|
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
|
||||||
|
|
||||||
- name: Upload x86
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: marshmallow-x86-${{ github.sha }}
|
|
||||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86-release.apk
|
|
||||||
|
|
||||||
- name: Upload mapping
|
- name: Upload mapping
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ jobs:
|
|||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 21
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014"
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
|
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -43,7 +46,6 @@ jobs:
|
|||||||
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-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-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_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/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/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/native-debug-symbols/defaultRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
||||||
@@ -51,7 +53,6 @@ jobs:
|
|||||||
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-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-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_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/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/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
|
mv app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
||||||
@@ -64,7 +65,6 @@ jobs:
|
|||||||
EhViewer-${{ github.ref_name }}-default-universal.apk
|
EhViewer-${{ github.ref_name }}-default-universal.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-x86_64.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-armeabi-v7a.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-mapping.txt
|
EhViewer-${{ github.ref_name }}-default-mapping.txt
|
||||||
EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
||||||
@@ -72,7 +72,6 @@ jobs:
|
|||||||
EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
|
EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
|
EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-x86_64.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-armeabi-v7a.apk
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
||||||
EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: 'Update Chrome Version'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 1 * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Update Chrome Version
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
cd chrome-for-testing
|
||||||
|
curl -OLf https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config push.autoSetupRemote true
|
||||||
|
git switch -c chore/chrome
|
||||||
|
if git commit -am "Update Chrome version for default User-Agent"; then
|
||||||
|
git push
|
||||||
|
gh pr create -f
|
||||||
|
gh pr merge chore/chrome --auto -dr
|
||||||
|
fi
|
||||||
@@ -1,5 +1,71 @@
|
|||||||
# 变更日志
|
# 变更日志
|
||||||
|
|
||||||
|
## [1.11.6-RC2] - 2024-06-06
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
* 下载图片时执行完整性校验以防止文件损坏/被篡改
|
||||||
|
|
||||||
|
|
||||||
|
## [1.11.6-RC1] - 2024-06-02
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
* 下载列表默认显示全部下载项目
|
||||||
|
* 替换搜索文本中的换行符为空格 [#1258](https://github.com/FooIbar/EhViewer/issues/1258)
|
||||||
|
|
||||||
|
### Bug 修复
|
||||||
|
|
||||||
|
* 评价画廊时未使用输入的评分
|
||||||
|
* 搜索时意外选择 Non-H 类别 [#1246](https://github.com/FooIbar/EhViewer/issues/1246)
|
||||||
|
* 侧边栏与导航抽屉重叠 [#1262](https://github.com/FooIbar/EhViewer/issues/1262)
|
||||||
|
* 快速搜索/下载标签排序失效 [#1267](https://github.com/FooIbar/EhViewer/issues/1267)
|
||||||
|
* 加载压缩包/种子列表出错时 UI 冻结
|
||||||
|
* 低版本 Android 上某些 UI 被遮挡
|
||||||
|
|
||||||
|
|
||||||
|
## [1.11.5] - 2024-05-19
|
||||||
|
|
||||||
|
### 重大变更
|
||||||
|
|
||||||
|
* 不再支持 32 位 x86
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
|
||||||
|
* 支持按作者对下载分组
|
||||||
|
* 由于 E 站不再强制要求人机验证,重新支持账号密码登录
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
* 在登录时加载收藏名称 [#1077](https://github.com/FooIbar/EhViewer/issues/1077)
|
||||||
|
* 优化归档和种子界面 UI
|
||||||
|
* 禁止在 cookie 界面截图以防止泄露
|
||||||
|
* 在缩略图模式下显示画廊页数
|
||||||
|
* 记住最低评分和页数搜索参数
|
||||||
|
* 更新 User-Agent 以缓解 IP 封禁 [#1182](https://github.com/FooIbar/EhViewer/issues/1182)
|
||||||
|
* 账号密码登录支持自动填充
|
||||||
|
* 更新中文(台灣)翻译
|
||||||
|
|
||||||
|
### Bug 修复
|
||||||
|
|
||||||
|
* 非触摸模式下无法退出搜索界面 [#1060](https://github.com/FooIbar/EhViewer/issues/1060)
|
||||||
|
* 某些设备上无法安装更新 [#1067](https://github.com/FooIbar/EhViewer/issues/1067)
|
||||||
|
* 某些设备上崩溃/卡顿 [#996](https://github.com/FooIbar/EhViewer/issues/996) [#1023](https://github.com/FooIbar/EhViewer/issues/1023)
|
||||||
|
* 搜索记录中存在重复条目时崩溃 [#1130](https://github.com/FooIbar/EhViewer/issues/1130)
|
||||||
|
* 条漫模式下缩小后点按区域偏移 [#127](https://github.com/FooIbar/EhViewer/issues/127)
|
||||||
|
* 多窗口模式下点按区域偏移
|
||||||
|
* 点击下载通知时未清除状态
|
||||||
|
* 保存未完成加载的图片时崩溃 [#1154](https://github.com/FooIbar/EhViewer/issues/1154)
|
||||||
|
* 历史记录界面无法在项目上滑动打开抽屉 [#464](https://github.com/FooIbar/EhViewer/issues/464)
|
||||||
|
* 导入数据失败时显示导入成功 [#1174](https://github.com/FooIbar/EhViewer/issues/1174)
|
||||||
|
* 某些条件下收藏界面崩溃 [#1190](https://github.com/FooIbar/EhViewer/issues/1190)
|
||||||
|
* 反转点按区域功能失效 [#1217](https://github.com/FooIbar/EhViewer/issues/1217)
|
||||||
|
|
||||||
|
### 已知问题
|
||||||
|
|
||||||
|
* 某些设备上从后台返回时某些 UI 消失,可通过关闭列表项目动画部分缓解 [#1184](https://github.com/FooIbar/EhViewer/issues/1184)
|
||||||
|
|
||||||
|
|
||||||
## [1.11.4] - 2024-04-21
|
## [1.11.4] - 2024-04-21
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -85,13 +85,13 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
|
|||||||
Here is the libraries
|
Here is the libraries
|
||||||
|
|
||||||
- [Arrow](https://arrow-kt.io/)
|
- [Arrow](https://arrow-kt.io/)
|
||||||
- [AOSP & AndroidX](http://source.android.com/)
|
- [AOSP & AndroidX](https://source.android.com/)
|
||||||
- [Kotlin & KotlinX](https://kotlinlang.org/)
|
- [Kotlin & KotlinX](https://kotlinlang.org/)
|
||||||
- [Material Icons](https://github.com/google/material-design-icons)
|
- [Material Icons](https://github.com/google/material-design-icons)
|
||||||
- [Ktor](https://ktor.io/)
|
- [Ktor](https://ktor.io/)
|
||||||
- [Coil](https://coil-kt.github.io/coil/)
|
- [Coil](https://coil-kt.github.io/coil/)
|
||||||
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
|
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
|
||||||
- [libarchive](http://www.libarchive.org/)
|
- [libarchive](https://www.libarchive.org/)
|
||||||
- [jsoup](https://jsoup.org/)
|
- [jsoup](https://jsoup.org/)
|
||||||
|
|
||||||
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
|
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
|
||||||
|
|||||||
+54
-47
@@ -1,8 +1,5 @@
|
|||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
import com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
import com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
val isRelease: Boolean
|
val isRelease: Boolean
|
||||||
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
|
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
|
||||||
@@ -19,16 +16,19 @@ plugins {
|
|||||||
alias(libs.plugins.composeCompilerReportGenerator)
|
alias(libs.plugins.composeCompilerReportGenerator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val supportedAbis = arrayOf("arm64-v8a", "x86_64", "armeabi-v7a")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
ndkVersion = "26.2.11394342"
|
ndkVersion = "27.0.11718014-beta1"
|
||||||
|
androidResources.generateLocaleConfig = true
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
isEnable = true
|
isEnable = true
|
||||||
reset()
|
reset()
|
||||||
if (isRelease) {
|
if (isRelease) {
|
||||||
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
|
include(*supportedAbis)
|
||||||
isUniversalApk = true
|
isUniversalApk = true
|
||||||
} else {
|
} else {
|
||||||
include("arm64-v8a", "x86_64")
|
include("arm64-v8a", "x86_64")
|
||||||
@@ -49,23 +49,25 @@ android {
|
|||||||
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
||||||
}.standardOutput.asText.get().trim()
|
}.standardOutput.asText.get().trim()
|
||||||
|
|
||||||
val buildTime by lazy {
|
val commitTime = providers.exec {
|
||||||
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
|
commandLine = "git log -1 --format=%ct".split(' ')
|
||||||
formatter.format(Instant.now())
|
}.standardOutput.asText.get().trim()
|
||||||
}
|
|
||||||
|
|
||||||
val repoName = providers.exec {
|
val repoName = providers.exec {
|
||||||
commandLine = "git remote get-url origin".split(' ')
|
commandLine = "git remote get-url origin".split(' ')
|
||||||
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
|
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
|
||||||
.removeSuffix(".git")
|
.removeSuffix(".git")
|
||||||
|
|
||||||
|
val chromeVersion = rootProject.layout.projectDirectory.file("chrome-for-testing/LATEST_RELEASE_STABLE").asFile
|
||||||
|
.readText().substringBefore('.')
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "moe.tarsin.ehviewer"
|
applicationId = "moe.tarsin.ehviewer"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 180055
|
versionCode = 180056
|
||||||
versionName = "1.11.4"
|
versionName = "1.11.6"
|
||||||
// versionNameSuffix = "-SNAPSHOT"
|
versionNameSuffix = "-RC2"
|
||||||
resourceConfigurations.addAll(
|
resourceConfigurations.addAll(
|
||||||
listOf(
|
listOf(
|
||||||
"zh",
|
"zh",
|
||||||
@@ -84,10 +86,20 @@ android {
|
|||||||
)
|
)
|
||||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
|
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||||
|
buildConfigField("long", "COMMIT_TIME", commitTime)
|
||||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||||
|
buildConfigField("String", "CHROME_VERSION", "\"$chromeVersion\"")
|
||||||
ndk {
|
ndk {
|
||||||
|
if (isRelease) {
|
||||||
|
abiFilters.addAll(supportedAbis)
|
||||||
|
}
|
||||||
debugSymbolLevel = "FULL"
|
debugSymbolLevel = "FULL"
|
||||||
}
|
}
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments += "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions += "api"
|
flavorDimensions += "api"
|
||||||
@@ -111,31 +123,6 @@ android {
|
|||||||
isCoreLibraryDesugaringEnabled = true
|
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",
|
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
|
||||||
"-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi",
|
|
||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
|
||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
|
||||||
"-opt-in=androidx.paging.ExperimentalPagingApi",
|
|
||||||
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
|
||||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
|
||||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
|
||||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
|
||||||
"-opt-in=splitties.experimental.ExperimentalSplittiesApi",
|
|
||||||
"-opt-in=splitties.preferences.DataStorePreferencesPreview",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
disable += setOf("MissingTranslation", "MissingQuantity")
|
disable += setOf("MissingTranslation", "MissingQuantity")
|
||||||
fatal += setOf("NewApi", "InlinedApi")
|
fatal += setOf("NewApi", "InlinedApi")
|
||||||
@@ -155,11 +142,12 @@ android {
|
|||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
proguardFiles("proguard-rules.pro")
|
proguardFiles("proguard-rules.pro")
|
||||||
signingConfig = signConfig
|
signingConfig = signConfig
|
||||||
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
|
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
buildConfigField("String", "BUILD_TIME", "\"\"")
|
lint {
|
||||||
|
abortOnError = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +161,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composeCompiler {
|
composeCompiler {
|
||||||
// https://youtrack.jetbrains.com/issue/KT-67216
|
|
||||||
suppressKotlinVersionCompatibilityCheck = libs.versions.kotlin.get()
|
|
||||||
enableIntrinsicRemember = true
|
|
||||||
enableNonSkippingGroupOptimization = true
|
enableNonSkippingGroupOptimization = true
|
||||||
enableExperimentalStrongSkippingMode = true
|
enableStrongSkippingMode = true
|
||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
@@ -209,13 +194,13 @@ dependencies {
|
|||||||
implementation(libs.androidx.core.splashscreen)
|
implementation(libs.androidx.core.splashscreen)
|
||||||
|
|
||||||
implementation(libs.androidx.constraintlayout.compose)
|
implementation(libs.androidx.constraintlayout.compose)
|
||||||
|
implementation(libs.androidx.datastore)
|
||||||
|
implementation(libs.androidx.graphics.path)
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||||
implementation(libs.androidx.lifecycle.process)
|
implementation(libs.androidx.lifecycle.process)
|
||||||
implementation(libs.androidx.lifecycle.compose)
|
implementation(libs.androidx.lifecycle.compose)
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
|
||||||
implementation(libs.androidx.navigation.compose)
|
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||||
implementation(libs.androidx.paging.compose)
|
implementation(libs.androidx.paging.compose)
|
||||||
|
|
||||||
@@ -273,11 +258,33 @@ dependencies {
|
|||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(21)
|
jvmToolchain(21)
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs = listOf(
|
||||||
|
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||||
|
"-progressive",
|
||||||
|
"-Xjvm-default=all",
|
||||||
|
"-Xcontext-receivers",
|
||||||
|
|
||||||
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
"-opt-in=androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi",
|
||||||
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||||
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
|
"-opt-in=androidx.paging.ExperimentalPagingApi",
|
||||||
|
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||||
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
"-opt-in=splitties.experimental.ExperimentalSplittiesApi",
|
||||||
|
"-opt-in=splitties.preferences.DataStorePreferencesPreview",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
arg("room.generateKotlin", "true")
|
|
||||||
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
|
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+4
@@ -16,6 +16,10 @@
|
|||||||
|
|
||||||
-dontwarn org.conscrypt.Conscrypt
|
-dontwarn org.conscrypt.Conscrypt
|
||||||
|
|
||||||
|
# https://issuetracker.google.com/222232895
|
||||||
|
-dontwarn androidx.window.extensions.**
|
||||||
|
-dontwarn androidx.window.sidecar.Sidecar*
|
||||||
|
|
||||||
-keepattributes LineNumberTable
|
-keepattributes LineNumberTable
|
||||||
|
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
|||||||
@@ -0,0 +1,564 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 23,
|
||||||
|
"identityHash": "7d1cfc4e9a9d506a73eb0520ae414d25",
|
||||||
|
"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": "DOWNLOAD_ARTISTS",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `ARTIST` TEXT NOT NULL, PRIMARY KEY(`GID`, `ARTIST`), FOREIGN KEY(`GID`) REFERENCES `DOWNLOADS`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "gid",
|
||||||
|
"columnName": "GID",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artist",
|
||||||
|
"columnName": "ARTIST",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"GID",
|
||||||
|
"ARTIST"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "DOWNLOADS",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"GID"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"GID"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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, '7d1cfc4e9a9d506a73eb0520ae414d25')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="@bool/large_heap"
|
android:largeHeap="@bool/large_heap"
|
||||||
android:localeConfig="@xml/locale_config"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
@@ -101,23 +100,24 @@
|
|||||||
<data android:scheme="content" />
|
<data android:scheme="content" />
|
||||||
<data android:host="*" />
|
<data android:host="*" />
|
||||||
<data android:mimeType="application/octet-stream" />
|
<data android:mimeType="application/octet-stream" />
|
||||||
<data android:mimeType="application/7z" />
|
|
||||||
<data android:mimeType="application/rar" />
|
|
||||||
<data android:mimeType="application/zip" />
|
<data android:mimeType="application/zip" />
|
||||||
<data android:mimeType="application/x-7z-compressed" />
|
<data android:mimeType="application/x-7z-compressed" />
|
||||||
<data android:mimeType="application/x-rar-compressed" />
|
|
||||||
<data android:mimeType="application/x-zip-compressed" />
|
|
||||||
<data android:mimeType="application/x-tar" />
|
<data android:mimeType="application/x-tar" />
|
||||||
<data android:mimeType="application/x-xz" />
|
<data android:mimeType="application/x-xz" />
|
||||||
<data android:mimeType="application/gzip" />
|
<data android:mimeType="application/gzip" />
|
||||||
|
<data android:mimeType="application/vnd.rar" />
|
||||||
|
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||||
|
<data android:mimeType="application/vnd.comicbook-rar" />
|
||||||
|
|
||||||
|
<!-- unofficial -->
|
||||||
|
<data android:mimeType="application/7z" />
|
||||||
|
<data android:mimeType="application/rar" />
|
||||||
<data android:mimeType="application/x-cbz" />
|
<data android:mimeType="application/x-cbz" />
|
||||||
<data android:mimeType="application/x-cbr" />
|
<data android:mimeType="application/x-cbr" />
|
||||||
<data android:mimeType="application/x-cbt" />
|
<data android:mimeType="application/x-cbt" />
|
||||||
<data android:mimeType="application/x-cb7" />
|
<data android:mimeType="application/x-cb7" />
|
||||||
|
<data android:mimeType="application/x-zip-compressed" />
|
||||||
<!-- untested -->
|
<data android:mimeType="application/x-rar-compressed" />
|
||||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
|
||||||
<data android:mimeType="application/vnd.comicbook-rar" />
|
|
||||||
<data android:mimeType="application/x-compressed-tar" />
|
<data android:mimeType="application/x-compressed-tar" />
|
||||||
<data android:mimeType="application/x-bzip-compressed-tar" />
|
<data android:mimeType="application/x-bzip-compressed-tar" />
|
||||||
<data android:mimeType="application/x-lzma-compressed-tar" />
|
<data android:mimeType="application/x-lzma-compressed-tar" />
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
From 2fa97f75b64c05da506968fa9a1711716f2d45a0 Mon Sep 17 00:00:00 2001
|
||||||
|
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||||
|
Date: Tue, 26 Sep 2023 00:08:03 +0800
|
||||||
|
Subject: [PATCH 1/2] Hide symbols
|
||||||
|
|
||||||
|
---
|
||||||
|
CMakeLists.txt | 4 ++--
|
||||||
|
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||||
|
index ec97e4c7..4d859341 100644
|
||||||
|
--- a/CMakeLists.txt
|
||||||
|
+++ b/CMakeLists.txt
|
||||||
|
@@ -2129,10 +2129,10 @@ SET(OLD_CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS}")
|
||||||
|
SET(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -fvisibility=hidden -Werror")
|
||||||
|
CHECK_C_SOURCE_COMPILES("void __attribute__((visibility(\"default\"))) foo(void);
|
||||||
|
int main() { return 0; }" HAVE_VISIBILITY_ATTR)
|
||||||
|
-IF (HAVE_VISIBILITY_ATTR)
|
||||||
|
+IF (0)
|
||||||
|
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
|
||||||
|
ADD_DEFINITIONS(-D__LIBARCHIVE_ENABLE_VISIBILITY)
|
||||||
|
-ENDIF(HAVE_VISIBILITY_ATTR)
|
||||||
|
+ENDIF(0)
|
||||||
|
SET(CMAKE_REQUIRED_FLAGS "${OLD_CMAKE_REQUIRED_FLAGS}")
|
||||||
|
|
||||||
|
# Generate "config.h" from "build/cmake/config.h.in"
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
From 2dcc8be937499022c37678eb6bf4bd026b6fcbb0 Mon Sep 17 00:00:00 2001
|
||||||
|
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||||
|
Date: Tue, 26 Sep 2023 00:09:25 +0800
|
||||||
|
Subject: [PATCH 2/2] Insert link libs
|
||||||
|
|
||||||
|
---
|
||||||
|
CMakeLists.txt | 2 +-
|
||||||
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||||
|
index 4d859341..1cd1cd47 100644
|
||||||
|
--- a/CMakeLists.txt
|
||||||
|
+++ b/CMakeLists.txt
|
||||||
|
@@ -438,7 +438,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
|
||||||
|
# # endif
|
||||||
|
ENDIF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
|
||||||
|
|
||||||
|
-SET(ADDITIONAL_LIBS "")
|
||||||
|
+SET(ADDITIONAL_LIBS ${LIBARCHIVE_CUSTOM_LIBS})
|
||||||
|
#
|
||||||
|
# Find ZLIB
|
||||||
|
#
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ option(BUILD_TESTING OFF)
|
|||||||
|
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
liblzma
|
liblzma
|
||||||
GIT_REPOSITORY https://git.tukaani.org/xz.git
|
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
|
||||||
GIT_TAG v5.6.1
|
GIT_TAG v5.6.2
|
||||||
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch || cd .
|
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch || cd .
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,15 +63,15 @@ option(ENABLE_CPIO OFF)
|
|||||||
option(ENABLE_CAT OFF)
|
option(ENABLE_CAT OFF)
|
||||||
option(ENABLE_UNZIP OFF)
|
option(ENABLE_UNZIP OFF)
|
||||||
option(ENABLE_TEST OFF)
|
option(ENABLE_TEST OFF)
|
||||||
option(ENABLE_WERROR OFF)
|
|
||||||
|
|
||||||
# Configure libarchive link's static lib
|
# Configure libarchive link's static lib
|
||||||
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
|
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
|
||||||
|
|
||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
libarchive
|
libarchive
|
||||||
GIT_REPOSITORY https://github.com/FooIbar/libarchive.git
|
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
||||||
GIT_TAG v3.7.3
|
GIT_TAG v3.7.4
|
||||||
|
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Hide-symbols.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || cd .
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(libarchive)
|
FetchContent_MakeAvailable(libarchive)
|
||||||
@@ -80,11 +80,12 @@ include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
Corrosion
|
Corrosion
|
||||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
||||||
GIT_TAG 0a3bdf452995803c334d79dc9a2affbfad51b720
|
GIT_TAG v0.5
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(Corrosion)
|
FetchContent_MakeAvailable(Corrosion)
|
||||||
corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml)
|
corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml)
|
||||||
|
corrosion_add_target_local_rustflags(ehviewer_rust "-Clink-args=-Wl,-z,max-page-size=16384")
|
||||||
|
|
||||||
# Build and link our app's native lib
|
# Build and link our app's native lib
|
||||||
add_library(${PROJECT_NAME} SHARED archive.c border.c gifutils.c natsort/strnatcmp.c)
|
add_library(${PROJECT_NAME} SHARED archive.c border.c gifutils.c natsort/strnatcmp.c)
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ static archive_ctx **ctx_pool;
|
|||||||
|
|
||||||
static void *mempool = NULL;
|
static void *mempool = NULL;
|
||||||
static size_t *mempoolofs = NULL;
|
static size_t *mempoolofs = NULL;
|
||||||
|
static int page_size = 0;
|
||||||
|
|
||||||
#define PAGE_ALIGN(x) ((x + ~PAGE_MASK) & PAGE_MASK)
|
#define PAGE_ALIGN(x) ((x + page_size - 1) & ~(page_size - 1))
|
||||||
|
|
||||||
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
|
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
|
||||||
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
|
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
|
||||||
@@ -282,6 +283,7 @@ JNIEXPORT jint JNICALL
|
|||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd, jlong size, jboolean sort_entries) {
|
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd, jlong size, jboolean sort_entries) {
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
|
page_size = getpagesize();
|
||||||
archive_ctx *ctx = NULL;
|
archive_ctx *ctx = NULL;
|
||||||
archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||||
if (archiveAddr == MAP_FAILED) {
|
if (archiveAddr == MAP_FAILED) {
|
||||||
|
|||||||
+278
@@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.compose.foundation.gestures
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragDelta
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import androidx.compose.ui.input.pointer.positionChange
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.MeasureResult
|
||||||
|
import androidx.compose.ui.layout.MeasureScope
|
||||||
|
import androidx.compose.ui.node.LayoutModifierNode
|
||||||
|
import androidx.compose.ui.node.ModifierNodeElement
|
||||||
|
import androidx.compose.ui.node.requireLayoutDirection
|
||||||
|
import androidx.compose.ui.platform.InspectorInfo
|
||||||
|
import androidx.compose.ui.platform.debugInspectorInfo
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
fun <T> Modifier.anchoredHorizontalDraggable(
|
||||||
|
state: AnchoredDraggableState<T>,
|
||||||
|
enableDragFromStartToEnd: Boolean = true,
|
||||||
|
enableDragFromEndToStart: Boolean = true,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
startDragImmediately: Boolean = state.isAnimationRunning,
|
||||||
|
): Modifier = this then AnchoredHorizontalDraggableElement(
|
||||||
|
state = state,
|
||||||
|
canDrag = { (x, y) ->
|
||||||
|
if (x > 0) {
|
||||||
|
enableDragFromStartToEnd
|
||||||
|
} else {
|
||||||
|
enableDragFromEndToStart
|
||||||
|
} && x.absoluteValue > y.absoluteValue * 2f
|
||||||
|
},
|
||||||
|
enabled = enabled,
|
||||||
|
startDragImmediately = startDragImmediately,
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
private class AnchoredHorizontalDraggableElement<T>(
|
||||||
|
private val state: AnchoredDraggableState<T>,
|
||||||
|
private val canDrag: (Offset) -> Boolean,
|
||||||
|
private val enabled: Boolean,
|
||||||
|
private val startDragImmediately: Boolean,
|
||||||
|
) : ModifierNodeElement<AnchoredHorizontalDraggableNode<T>>() {
|
||||||
|
override fun create() = AnchoredHorizontalDraggableNode(
|
||||||
|
state,
|
||||||
|
canDrag,
|
||||||
|
enabled,
|
||||||
|
startDragImmediately,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun update(node: AnchoredHorizontalDraggableNode<T>) {
|
||||||
|
node.update(
|
||||||
|
state,
|
||||||
|
canDrag,
|
||||||
|
enabled,
|
||||||
|
startDragImmediately,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = state.hashCode()
|
||||||
|
result = 31 * result + canDrag.hashCode()
|
||||||
|
result = 31 * result + enabled.hashCode()
|
||||||
|
result = 31 * result + startDragImmediately.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
|
||||||
|
if (other !is AnchoredHorizontalDraggableElement<*>) return false
|
||||||
|
|
||||||
|
if (state != other.state) return false
|
||||||
|
if (canDrag != other.canDrag) return false
|
||||||
|
if (enabled != other.enabled) return false
|
||||||
|
if (startDragImmediately != other.startDragImmediately) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun InspectorInfo.inspectableProperties() {
|
||||||
|
name = "anchoredDraggable"
|
||||||
|
properties["state"] = state
|
||||||
|
properties["canDrag"] = canDrag
|
||||||
|
properties["enabled"] = enabled
|
||||||
|
properties["startDragImmediately"] = startDragImmediately
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
private class AnchoredHorizontalDraggableNode<T>(
|
||||||
|
private var state: AnchoredDraggableState<T>,
|
||||||
|
private var canDrag: (Offset) -> Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
private var startDragImmediately: Boolean,
|
||||||
|
) : HorizontalDragGestureNode(
|
||||||
|
enabled = enabled,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val isReverseDirection: Boolean
|
||||||
|
get() = requireLayoutDirection() == LayoutDirection.Rtl
|
||||||
|
|
||||||
|
override fun canDrag(change: PointerInputChange): Boolean {
|
||||||
|
return canDrag(change.positionChange().reverseIfNeeded())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
|
||||||
|
state.anchoredDrag {
|
||||||
|
forEachDelta { dragDelta ->
|
||||||
|
dragTo(state.newOffsetForDelta(dragDelta.delta.reverseIfNeeded().x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDragStarted(startedPosition: Offset) { }
|
||||||
|
|
||||||
|
override fun onDragStopped(velocity: Velocity) {
|
||||||
|
if (!isAttached) return
|
||||||
|
coroutineScope.launch {
|
||||||
|
state.settle(velocity.reverseIfNeeded().x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startDragImmediately(): Boolean = startDragImmediately
|
||||||
|
|
||||||
|
fun update(
|
||||||
|
state: AnchoredDraggableState<T>,
|
||||||
|
canDrag: (Offset) -> Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
startDragImmediately: Boolean,
|
||||||
|
) {
|
||||||
|
var resetPointerInputHandling = false
|
||||||
|
|
||||||
|
if (this.state != state) {
|
||||||
|
this.state = state
|
||||||
|
resetPointerInputHandling = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canDrag = canDrag
|
||||||
|
this.startDragImmediately = startDragImmediately
|
||||||
|
|
||||||
|
update(
|
||||||
|
enabled = enabled,
|
||||||
|
shouldResetPointerInputHandling = resetPointerInputHandling,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Velocity.reverseIfNeeded() = if (isReverseDirection) this * -1f else this * 1f
|
||||||
|
private fun Offset.reverseIfNeeded() = if (isReverseDirection) this * -1f else this * 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun <T> AnchoredDraggableState<T>.newOffsetForDelta(delta: Float) =
|
||||||
|
((if (offset.isNaN()) 0f else offset) + delta)
|
||||||
|
.coerceIn(anchors.minAnchor(), anchors.maxAnchor())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Modifier allows configuring an [AnchoredDraggableState]'s anchors based on this layout
|
||||||
|
* node's size and offsetting it.
|
||||||
|
* It considers lookahead and reports the appropriate size and measurement for the appropriate
|
||||||
|
* phase.
|
||||||
|
*
|
||||||
|
* @param state The state the anchors should be attached to
|
||||||
|
* @param orientation The orientation the component should be offset in
|
||||||
|
* @param anchors Lambda to calculate the anchors based on this layout's size and the incoming
|
||||||
|
* constraints. These can be useful to avoid subcomposition.
|
||||||
|
*/
|
||||||
|
internal fun <T> Modifier.draggableAnchors(
|
||||||
|
state: AnchoredDraggableState<T>,
|
||||||
|
orientation: Orientation,
|
||||||
|
anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||||
|
) = this then DraggableAnchorsElement(state, anchors, orientation)
|
||||||
|
|
||||||
|
private class DraggableAnchorsElement<T>(
|
||||||
|
private val state: AnchoredDraggableState<T>,
|
||||||
|
private val anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||||
|
private val orientation: Orientation,
|
||||||
|
) : ModifierNodeElement<DraggableAnchorsNode<T>>() {
|
||||||
|
|
||||||
|
override fun create() = DraggableAnchorsNode(state, anchors, orientation)
|
||||||
|
|
||||||
|
override fun update(node: DraggableAnchorsNode<T>) {
|
||||||
|
node.state = state
|
||||||
|
node.anchors = anchors
|
||||||
|
node.orientation = orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
|
||||||
|
if (other !is DraggableAnchorsElement<*>) return false
|
||||||
|
|
||||||
|
if (state != other.state) return false
|
||||||
|
if (anchors !== other.anchors) return false
|
||||||
|
if (orientation != other.orientation) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = state.hashCode()
|
||||||
|
result = 31 * result + anchors.hashCode()
|
||||||
|
result = 31 * result + orientation.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun InspectorInfo.inspectableProperties() {
|
||||||
|
debugInspectorInfo {
|
||||||
|
properties["state"] = state
|
||||||
|
properties["anchors"] = anchors
|
||||||
|
properties["orientation"] = orientation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DraggableAnchorsNode<T>(
|
||||||
|
var state: AnchoredDraggableState<T>,
|
||||||
|
var anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||||
|
var orientation: Orientation,
|
||||||
|
) : Modifier.Node(), LayoutModifierNode {
|
||||||
|
private var didLookahead: Boolean = false
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
didLookahead = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun MeasureScope.measure(
|
||||||
|
measurable: Measurable,
|
||||||
|
constraints: Constraints,
|
||||||
|
): MeasureResult {
|
||||||
|
val placeable = measurable.measure(constraints)
|
||||||
|
// If we are in a lookahead pass, we only want to update the anchors here and not in
|
||||||
|
// post-lookahead. If there is no lookahead happening (!isLookingAhead && !didLookahead),
|
||||||
|
// update the anchors in the main pass.
|
||||||
|
if (!isLookingAhead || !didLookahead) {
|
||||||
|
val size = IntSize(placeable.width, placeable.height)
|
||||||
|
val newAnchorResult = anchors(size, constraints)
|
||||||
|
state.updateAnchors(newAnchorResult.first, newAnchorResult.second)
|
||||||
|
}
|
||||||
|
didLookahead = isLookingAhead || didLookahead
|
||||||
|
return layout(placeable.width, placeable.height) {
|
||||||
|
// In a lookahead pass, we use the position of the current target as this is where any
|
||||||
|
// ongoing animations would move. If the component is in a settled state, lookahead
|
||||||
|
// and post-lookahead will converge.
|
||||||
|
val offset = if (isLookingAhead) {
|
||||||
|
state.anchors.positionOf(state.targetValue)
|
||||||
|
} else {
|
||||||
|
state.requireOffset()
|
||||||
|
}
|
||||||
|
val xOffset = if (orientation == Orientation.Horizontal) offset else 0f
|
||||||
|
val yOffset = if (orientation == Orientation.Vertical) offset else 0f
|
||||||
|
placeable.placeRelative(xOffset.roundToInt(), yOffset.roundToInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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.compose.foundation.gestures
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||||
|
import androidx.compose.ui.input.pointer.positionChange
|
||||||
|
import androidx.compose.ui.platform.ViewConfiguration
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Gesture detector that waits for pointer down and touch slop in the horizontal direction and
|
||||||
|
* then calls [onDrag] for each drag event.
|
||||||
|
* It follows the touch slop detection of [awaitHorizontalTouchSlopOrCancellation] but will consume
|
||||||
|
* the position change automatically once the touch slop has been crossed, the amount of drag over
|
||||||
|
* the touch slop is reported as the first drag event [onDrag] after the slop is crossed.
|
||||||
|
* If [shouldAwaitTouchSlop] returns true the touch slop recognition phase will be ignored
|
||||||
|
* and the drag gesture will be recognized immediately.The first [onDrag] in this case will report
|
||||||
|
* an [Offset.Zero].
|
||||||
|
*
|
||||||
|
* [onDragStart] is called when the touch slop has been passed and includes an [Offset] representing
|
||||||
|
* the last known pointer position relative to the containing element as well as the initial
|
||||||
|
* down event that triggered this gesture detection cycle. The [Offset] can be outside
|
||||||
|
* the actual bounds of the element itself meaning the numbers can be negative or larger than the
|
||||||
|
* element bounds if the touch target is smaller than the
|
||||||
|
* [ViewConfiguration.minimumTouchTargetSize].
|
||||||
|
*
|
||||||
|
* [onDragEnd] is called after all pointers are up with the event change of the up event
|
||||||
|
* and [onDragCancel] is called if another gesture has consumed pointer input,
|
||||||
|
* canceling this gesture.
|
||||||
|
*
|
||||||
|
* @param onDragStart A lambda to be called when the drag gesture starts, it contains information
|
||||||
|
* about the last known [PointerInputChange] relative to the containing element and the post slop
|
||||||
|
* delta.
|
||||||
|
* @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
|
||||||
|
* up [PointerInputChange] that finished the gesture.
|
||||||
|
* @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
|
||||||
|
* when it was consumed.
|
||||||
|
* @param shouldAwaitTouchSlop Indicates if touch slop detection should be skipped.
|
||||||
|
* @param onDrag A lambda to be called for each delta event in the gesture. It contains information
|
||||||
|
* about the [PointerInputChange] and the movement offset.
|
||||||
|
*/
|
||||||
|
internal suspend fun PointerInputScope.detectHorizontalDragGestures(
|
||||||
|
canDrag: (PointerInputChange) -> Boolean,
|
||||||
|
onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
|
||||||
|
onDragEnd: () -> Unit,
|
||||||
|
onDragCancel: () -> Unit,
|
||||||
|
shouldAwaitTouchSlop: () -> Boolean,
|
||||||
|
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
|
||||||
|
) {
|
||||||
|
awaitEachGesture {
|
||||||
|
val initialDown =
|
||||||
|
awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
|
||||||
|
val awaitTouchSlop = shouldAwaitTouchSlop()
|
||||||
|
|
||||||
|
if (!awaitTouchSlop) {
|
||||||
|
initialDown.consume()
|
||||||
|
}
|
||||||
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
var drag: PointerInputChange?
|
||||||
|
var overSlop = Offset.Zero
|
||||||
|
|
||||||
|
if (awaitTouchSlop) {
|
||||||
|
do {
|
||||||
|
drag = awaitHorizontalTouchSlopOrCancellation(
|
||||||
|
down.id,
|
||||||
|
) { change, over ->
|
||||||
|
if (canDrag.invoke(change)) {
|
||||||
|
change.consume()
|
||||||
|
overSlop = Offset(over, 0f)
|
||||||
|
} else {
|
||||||
|
throw CancellationException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (drag != null && !drag.isConsumed)
|
||||||
|
} else {
|
||||||
|
drag = initialDown
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drag != null) {
|
||||||
|
onDragStart.invoke(drag, overSlop)
|
||||||
|
onDrag(drag, overSlop)
|
||||||
|
val dragEnd = horizontalDrag(
|
||||||
|
pointerId = drag.id,
|
||||||
|
onDrag = {
|
||||||
|
onDrag(it, it.positionChange())
|
||||||
|
it.consume()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (dragEnd) {
|
||||||
|
onDragEnd()
|
||||||
|
} else {
|
||||||
|
onDragCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.compose.foundation.gestures
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragCancelled
|
||||||
|
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragDelta
|
||||||
|
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragStarted
|
||||||
|
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragStopped
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEvent
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
|
||||||
|
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||||
|
import androidx.compose.ui.input.pointer.util.addPointerInputChange
|
||||||
|
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
|
||||||
|
import androidx.compose.ui.node.DelegatingNode
|
||||||
|
import androidx.compose.ui.node.PointerInputModifierNode
|
||||||
|
import androidx.compose.ui.node.currentValueOf
|
||||||
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
import kotlin.math.sign
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A node that performs drag gesture recognition and event propagation.
|
||||||
|
*/
|
||||||
|
internal abstract class HorizontalDragGestureNode(
|
||||||
|
enabled: Boolean,
|
||||||
|
) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
|
||||||
|
|
||||||
|
protected var enabled = enabled
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var channel: Channel<HorizontalDragEvent>? = null
|
||||||
|
private var isListeningForEvents = false
|
||||||
|
|
||||||
|
abstract fun canDrag(change: PointerInputChange): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for the dragging behavior between the start and the end of the drag. It
|
||||||
|
* continually invokes `forEachDelta` to process incoming events. In return, `forEachDelta`
|
||||||
|
* calls `dragBy` method to process each individual delta.
|
||||||
|
*/
|
||||||
|
abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passes the action needed when a drag starts. This gives the ability to pass the desired
|
||||||
|
* behavior from other nodes implementing AbstractDraggableNode
|
||||||
|
*/
|
||||||
|
abstract fun onDragStarted(startedPosition: Offset)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passes the action needed when a drag stops. This gives the ability to pass the desired
|
||||||
|
* behavior from other nodes implementing AbstractDraggableNode
|
||||||
|
*/
|
||||||
|
abstract fun onDragStopped(velocity: Velocity)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If touch slop recognition should be skipped. If this is true, this node will start
|
||||||
|
* recognizing drag events immediately without waiting for touch slop.
|
||||||
|
*/
|
||||||
|
abstract fun startDragImmediately(): Boolean
|
||||||
|
|
||||||
|
private fun startListeningForEvents() {
|
||||||
|
isListeningForEvents = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To preserve the original behavior we had (before the Modifier.Node migration) we need to
|
||||||
|
* scope the DragStopped and DragCancel methods to the node's coroutine scope instead of using
|
||||||
|
* the one provided by the pointer input modifier, this is to ensure that even when the pointer
|
||||||
|
* input scope is reset we will continue any coroutine scope scope that we started from these
|
||||||
|
* methods while the pointer input scope was active.
|
||||||
|
*/
|
||||||
|
coroutineScope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
var event = channel?.receive()
|
||||||
|
if (event !is DragStarted) continue
|
||||||
|
onDragStarted(event.startPoint)
|
||||||
|
try {
|
||||||
|
drag { processDelta ->
|
||||||
|
while (event !is DragStopped && event !is DragCancelled) {
|
||||||
|
(event as? DragDelta)?.let(processDelta)
|
||||||
|
event = channel?.receive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event is DragStopped) {
|
||||||
|
onDragStopped((event as DragStopped).velocity)
|
||||||
|
} else if (event is DragCancelled) {
|
||||||
|
onDragStopped(Velocity.Zero)
|
||||||
|
}
|
||||||
|
} catch (c: CancellationException) {
|
||||||
|
onDragStopped(Velocity.Zero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pointerInputNode: SuspendingPointerInputModifierNode? = null
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
isListeningForEvents = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPointerEvent(
|
||||||
|
pointerEvent: PointerEvent,
|
||||||
|
pass: PointerEventPass,
|
||||||
|
bounds: IntSize,
|
||||||
|
) {
|
||||||
|
if (enabled && pointerInputNode == null) {
|
||||||
|
pointerInputNode = delegate(initializePointerInputNode())
|
||||||
|
}
|
||||||
|
pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializePointerInputNode(): SuspendingPointerInputModifierNode {
|
||||||
|
return SuspendingPointerInputModifierNode {
|
||||||
|
// re-create tracker when pointer input block restarts. This lazily creates the tracker
|
||||||
|
// only when it is need.
|
||||||
|
val velocityTracker = VelocityTracker()
|
||||||
|
val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
|
||||||
|
{ startEvent, initialDelta ->
|
||||||
|
if (!isListeningForEvents) {
|
||||||
|
if (channel == null) {
|
||||||
|
channel = Channel(capacity = Channel.UNLIMITED)
|
||||||
|
}
|
||||||
|
startListeningForEvents()
|
||||||
|
}
|
||||||
|
val xSign = sign(startEvent.position.x)
|
||||||
|
val ySign = sign(startEvent.position.y)
|
||||||
|
val adjustedStart = startEvent.position -
|
||||||
|
Offset(initialDelta.x * xSign, initialDelta.y * ySign)
|
||||||
|
|
||||||
|
channel?.trySend(DragStarted(adjustedStart))
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDragEnd: () -> Unit = {
|
||||||
|
val maximumVelocity = currentValueOf(LocalViewConfiguration)
|
||||||
|
.maximumFlingVelocity
|
||||||
|
val velocity = velocityTracker.calculateVelocity(
|
||||||
|
Velocity(maximumVelocity, maximumVelocity),
|
||||||
|
)
|
||||||
|
velocityTracker.resetTracking()
|
||||||
|
channel?.trySend(DragStopped(velocity))
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDragCancel: () -> Unit = {
|
||||||
|
channel?.trySend(DragCancelled)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shouldAwaitTouchSlop: () -> Boolean = {
|
||||||
|
!startDragImmediately()
|
||||||
|
}
|
||||||
|
|
||||||
|
val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
|
||||||
|
{ change, delta ->
|
||||||
|
velocityTracker.addPointerInputChange(change)
|
||||||
|
channel?.trySend(DragDelta(delta))
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineScope {
|
||||||
|
try {
|
||||||
|
detectHorizontalDragGestures(
|
||||||
|
canDrag = ::canDrag,
|
||||||
|
onDragStart = onDragStart,
|
||||||
|
onDragEnd = onDragEnd,
|
||||||
|
onDragCancel = onDragCancel,
|
||||||
|
shouldAwaitTouchSlop = shouldAwaitTouchSlop,
|
||||||
|
onDrag = onDrag,
|
||||||
|
)
|
||||||
|
} catch (cancellation: CancellationException) {
|
||||||
|
channel?.trySend(DragCancelled)
|
||||||
|
if (!isActive) throw cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancelPointerInput() {
|
||||||
|
pointerInputNode?.onCancelPointerInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(
|
||||||
|
enabled: Boolean = this.enabled,
|
||||||
|
shouldResetPointerInputHandling: Boolean = false,
|
||||||
|
) {
|
||||||
|
var resetPointerInputHandling = shouldResetPointerInputHandling
|
||||||
|
|
||||||
|
if (this.enabled != enabled) {
|
||||||
|
this.enabled = enabled
|
||||||
|
if (!enabled) {
|
||||||
|
pointerInputNode?.let { undelegate(it) }
|
||||||
|
pointerInputNode = null
|
||||||
|
}
|
||||||
|
resetPointerInputHandling = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetPointerInputHandling) {
|
||||||
|
pointerInputNode?.resetPointerInputHandler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class HorizontalDragEvent {
|
||||||
|
class DragStarted(val startPoint: Offset) : HorizontalDragEvent()
|
||||||
|
class DragStopped(val velocity: Velocity) : HorizontalDragEvent()
|
||||||
|
object DragCancelled : HorizontalDragEvent()
|
||||||
|
class DragDelta(val delta: Offset) : HorizontalDragEvent()
|
||||||
|
}
|
||||||
@@ -10,11 +10,8 @@ import androidx.compose.foundation.Canvas
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
import androidx.compose.foundation.gestures.anchoredHorizontalDraggable
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
|
||||||
import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.gestures.horizontalDrag
|
|
||||||
import androidx.compose.foundation.gestures.snapTo
|
import androidx.compose.foundation.gestures.snapTo
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -42,17 +39,12 @@ import androidx.compose.ui.graphics.RectangleShape
|
|||||||
import androidx.compose.ui.graphics.TileMode
|
import androidx.compose.ui.graphics.TileMode
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.input.pointer.positionChange
|
|
||||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
|
||||||
import androidx.compose.ui.input.pointer.util.addPointerInputChange
|
|
||||||
import androidx.compose.ui.layout.layout
|
import androidx.compose.ui.layout.layout
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
import com.hippo.ehviewer.ui.tools.animateFloatMergeOneWayPredictiveBackAsState
|
import com.hippo.ehviewer.ui.tools.animateFloatMergeOneWayPredictiveBackAsState
|
||||||
@@ -63,7 +55,6 @@ import kotlinx.coroutines.launch
|
|||||||
private val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
|
private val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
|
||||||
private const val DrawerPositionalThreshold = 0.5f
|
private const val DrawerPositionalThreshold = 0.5f
|
||||||
private val DrawerVelocityThreshold = 400.dp
|
private val DrawerVelocityThreshold = 400.dp
|
||||||
private val ContainerWidth = 360.0.dp
|
|
||||||
|
|
||||||
@Suppress("NotCloseable")
|
@Suppress("NotCloseable")
|
||||||
@Stable
|
@Stable
|
||||||
@@ -256,60 +247,30 @@ fun ModalSideDrawer(
|
|||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
var minValue by remember { mutableFloatStateOf(with(density) { ContainerWidth.toPx() }) }
|
var maxValue by remember { mutableFloatStateOf(with(density) { DrawerDefaults.MaximumDrawerWidth.toPx() }) }
|
||||||
val maxValue = 0f
|
val minValue = 0f
|
||||||
|
|
||||||
SideEffect {
|
SideEffect {
|
||||||
drawerState.density = density
|
drawerState.density = density
|
||||||
drawerState.anchoredDraggableState.updateAnchors(
|
drawerState.anchoredDraggableState.updateAnchors(
|
||||||
DraggableAnchors {
|
DraggableAnchors {
|
||||||
DrawerValue.Closed at minValue
|
DrawerValue.Closed at maxValue
|
||||||
DrawerValue.Open at maxValue
|
DrawerValue.Open at minValue
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
Box(
|
||||||
val velocityTracker = remember { VelocityTracker() }
|
modifier.fillMaxSize().anchoredHorizontalDraggable(
|
||||||
val dragModifier = if (gesturesEnabled) {
|
state = drawerState.anchoredDraggableState,
|
||||||
Modifier.pointerInput(isRtl) {
|
enableDragFromStartToEnd = drawerState.isOpen,
|
||||||
val multiplier = if (isRtl) -1 else 1
|
enableDragFromEndToStart = drawerState.isClosed,
|
||||||
awaitEachGesture {
|
enabled = gesturesEnabled,
|
||||||
fun canConsume(slop: Float) = (slop > 0 && drawerState.isOpen) || (slop < 0 && drawerState.isClosed)
|
),
|
||||||
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)
|
|
||||||
drag.consume()
|
|
||||||
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 {
|
val radius by remember {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
val step = calculateFraction(minValue, maxValue, drawerState.currentOffset)
|
val step = calculateFraction(maxValue, minValue, drawerState.currentOffset)
|
||||||
lerp(0, 10, step).dp
|
lerp(0, 10, step).dp
|
||||||
}
|
}
|
||||||
}.collectAsState(0.dp)
|
}.collectAsState(0.dp)
|
||||||
@@ -331,7 +292,7 @@ fun ModalSideDrawer(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
fraction = {
|
fraction = {
|
||||||
calculateFraction(minValue, maxValue, drawerState.requireOffset())
|
calculateFraction(maxValue, minValue, drawerState.requireOffset())
|
||||||
},
|
},
|
||||||
color = scrimColor,
|
color = scrimColor,
|
||||||
)
|
)
|
||||||
@@ -349,7 +310,7 @@ fun ModalSideDrawer(
|
|||||||
val scaledHeight = (multiplierY * placeable.height).roundToInt()
|
val scaledHeight = (multiplierY * placeable.height).roundToInt()
|
||||||
val reMeasured = measurable.measure(Constraints.fixed(scaledWidth, scaledHeight))
|
val reMeasured = measurable.measure(Constraints.fixed(scaledWidth, scaledHeight))
|
||||||
layout(scaledWidth, scaledHeight) {
|
layout(scaledWidth, scaledHeight) {
|
||||||
reMeasured.placeRelative(-(scaledWidth - placeable.width), 0)
|
reMeasured.placeRelative(placeable.width - scaledWidth, 0)
|
||||||
}
|
}
|
||||||
}.background(
|
}.background(
|
||||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
|
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
|
||||||
@@ -361,14 +322,20 @@ fun ModalSideDrawer(
|
|||||||
scaleX = scale
|
scaleX = scale
|
||||||
scaleY = scale
|
scaleY = scale
|
||||||
}.offset {
|
}.offset {
|
||||||
IntOffset(lerp(0f, minValue * 0.05f, predictiveState).roundToInt(), 0)
|
IntOffset(lerp(0f, maxValue * 0.05f, predictiveState).roundToInt(), 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Modifier.onGloballyPositioned {
|
Modifier.onSizeChanged {
|
||||||
val w = it.size.width
|
val width = it.width
|
||||||
if (w != 0) {
|
if (width != 0) {
|
||||||
minValue = it.size.width.toFloat()
|
maxValue = width.toFloat()
|
||||||
|
drawerState.anchoredDraggableState.updateAnchors(
|
||||||
|
DraggableAnchors {
|
||||||
|
DrawerValue.Closed at maxValue
|
||||||
|
DrawerValue.Open at minValue
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.compose.material3.fork
|
||||||
|
|
||||||
|
import androidx.annotation.FloatRange
|
||||||
|
import androidx.compose.animation.core.SpringSpec
|
||||||
|
import androidx.compose.animation.core.exponentialDecay
|
||||||
|
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||||
|
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.anchoredHorizontalDraggable
|
||||||
|
import androidx.compose.foundation.gestures.animateTo
|
||||||
|
import androidx.compose.foundation.gestures.draggableAnchors
|
||||||
|
import androidx.compose.foundation.gestures.snapTo
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
||||||
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
||||||
|
private val SnapAnimationSpec = SpringSpec<Float>()
|
||||||
|
|
||||||
|
// TODO: Update to M3 spec once it's available
|
||||||
|
private val DecayAnimationSpec = exponentialDecay<Float>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of the [SwipeToDismissBox] composable.
|
||||||
|
*
|
||||||
|
* @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 positionalThreshold The positional threshold to be used when calculating the target state
|
||||||
|
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
|
||||||
|
* the start of a transition. It will be, depending on the direction of the interaction, added or
|
||||||
|
* subtracted from/to the origin offset. It should always be a positive value.
|
||||||
|
*/
|
||||||
|
class SwipeToDismissBoxState(
|
||||||
|
initialValue: SwipeToDismissBoxValue,
|
||||||
|
internal val density: Density,
|
||||||
|
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
|
||||||
|
positionalThreshold: (totalDistance: Float) -> Float,
|
||||||
|
) {
|
||||||
|
internal val anchoredDraggableState =
|
||||||
|
AnchoredDraggableState(
|
||||||
|
initialValue = initialValue,
|
||||||
|
snapAnimationSpec = SnapAnimationSpec,
|
||||||
|
decayAnimationSpec = DecayAnimationSpec,
|
||||||
|
confirmValueChange = confirmValueChange,
|
||||||
|
positionalThreshold = positionalThreshold,
|
||||||
|
velocityThreshold = { with(density) { DismissVelocityThreshold.toPx() } },
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val offset: Float get() = anchoredDraggableState.offset
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require the current offset.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException If the offset has not been initialized yet
|
||||||
|
*/
|
||||||
|
fun requireOffset(): Float = anchoredDraggableState.requireOffset()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current state value of the [SwipeToDismissBoxState].
|
||||||
|
*/
|
||||||
|
val currentValue: SwipeToDismissBoxValue get() = anchoredDraggableState.currentValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target state. This is the closest state to the current offset (taking into account
|
||||||
|
* positional thresholds). If no interactions like animations or drags are in progress, this
|
||||||
|
* will be the current state.
|
||||||
|
*/
|
||||||
|
val targetValue: SwipeToDismissBoxValue get() = anchoredDraggableState.targetValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fraction of the progress going from currentValue to targetValue, within [0f..1f] bounds.
|
||||||
|
*/
|
||||||
|
@get:FloatRange(from = 0.0, to = 1.0)
|
||||||
|
val progress: Float get() = anchoredDraggableState.progress(anchoredDraggableState.settledValue, anchoredDraggableState.targetValue)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The direction (if any) in which the composable has been or is being dismissed.
|
||||||
|
*
|
||||||
|
* Use this to change the background of the [SwipeToDismissBox] if you want different actions
|
||||||
|
* on each side.
|
||||||
|
*/
|
||||||
|
val dismissDirection: SwipeToDismissBoxValue
|
||||||
|
get() = when {
|
||||||
|
offset == 0f || offset.isNaN() -> SwipeToDismissBoxValue.Settled
|
||||||
|
offset > 0f -> SwipeToDismissBoxValue.StartToEnd
|
||||||
|
else -> SwipeToDismissBoxValue.EndToStart
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the state without any animation and suspend until it's set
|
||||||
|
*
|
||||||
|
* @param targetValue The new target value
|
||||||
|
*/
|
||||||
|
suspend fun snapTo(targetValue: SwipeToDismissBoxValue) {
|
||||||
|
anchoredDraggableState.snapTo(targetValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the component to the default position with animation and suspend until it if fully
|
||||||
|
* reset or animation has been cancelled. This method will throw [CancellationException] if
|
||||||
|
* the animation is interrupted
|
||||||
|
*
|
||||||
|
* @return the reason the reset animation ended
|
||||||
|
*/
|
||||||
|
suspend fun reset() = anchoredDraggableState.animateTo(
|
||||||
|
targetValue = SwipeToDismissBoxValue.Settled,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the component in the given [direction], with an animation and suspend. This method
|
||||||
|
* will throw [CancellationException] if the animation is interrupted
|
||||||
|
*
|
||||||
|
* @param direction The dismiss direction.
|
||||||
|
*/
|
||||||
|
suspend fun dismiss(direction: SwipeToDismissBoxValue) {
|
||||||
|
anchoredDraggableState.animateTo(targetValue = direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default [Saver] implementation for [SwipeToDismissBoxState].
|
||||||
|
*/
|
||||||
|
fun Saver(
|
||||||
|
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean,
|
||||||
|
positionalThreshold: (totalDistance: Float) -> Float,
|
||||||
|
density: Density,
|
||||||
|
) = Saver<SwipeToDismissBoxState, SwipeToDismissBoxValue>(
|
||||||
|
save = { it.currentValue },
|
||||||
|
restore = {
|
||||||
|
SwipeToDismissBoxState(
|
||||||
|
it,
|
||||||
|
density,
|
||||||
|
confirmValueChange,
|
||||||
|
positionalThreshold,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and [remember] a [SwipeToDismissBoxState].
|
||||||
|
*
|
||||||
|
* @param initialValue The initial value of the state.
|
||||||
|
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||||
|
* @param positionalThreshold The positional threshold to be used when calculating the target state
|
||||||
|
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
|
||||||
|
* the start of a transition. It will be, depending on the direction of the interaction, added or
|
||||||
|
* subtracted from/to the origin offset. It should always be a positive value.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberSwipeToDismissBoxState(
|
||||||
|
initialValue: SwipeToDismissBoxValue = SwipeToDismissBoxValue.Settled,
|
||||||
|
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
|
||||||
|
positionalThreshold: (totalDistance: Float) -> Float =
|
||||||
|
SwipeToDismissBoxDefaults.positionalThreshold,
|
||||||
|
): SwipeToDismissBoxState {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
return rememberSaveable(
|
||||||
|
saver = SwipeToDismissBoxState.Saver(
|
||||||
|
confirmValueChange = confirmValueChange,
|
||||||
|
density = density,
|
||||||
|
positionalThreshold = positionalThreshold,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
SwipeToDismissBoxState(initialValue, density, confirmValueChange, positionalThreshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable that can be dismissed by swiping left or right.
|
||||||
|
*
|
||||||
|
* @param state The state of this component.
|
||||||
|
* @param backgroundContent A composable that is stacked behind the [content] and is exposed when
|
||||||
|
* the content is swiped. You can/should use the [state] to have different backgrounds on each side.
|
||||||
|
* @param modifier Optional [Modifier] for this component.
|
||||||
|
* @param enableDismissFromStartToEnd Whether SwipeToDismissBox can be dismissed from start to end.
|
||||||
|
* @param enableDismissFromEndToStart Whether SwipeToDismissBox can be dismissed from end to start.
|
||||||
|
* @param gesturesEnabled Whether swipe-to-dismiss can be interacted by gestures.
|
||||||
|
* @param content The content that can be dismissed.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SwipeToDismissBox(
|
||||||
|
state: SwipeToDismissBoxState,
|
||||||
|
backgroundContent: @Composable RowScope.() -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enableDismissFromStartToEnd: Boolean = true,
|
||||||
|
enableDismissFromEndToStart: Boolean = true,
|
||||||
|
gesturesEnabled: Boolean = true,
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier
|
||||||
|
.anchoredHorizontalDraggable(
|
||||||
|
state = state.anchoredDraggableState,
|
||||||
|
enableDragFromStartToEnd = enableDismissFromStartToEnd,
|
||||||
|
enableDragFromEndToStart = enableDismissFromEndToStart,
|
||||||
|
enabled = gesturesEnabled && state.currentValue == SwipeToDismissBoxValue.Settled,
|
||||||
|
),
|
||||||
|
propagateMinConstraints = true,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
content = backgroundContent,
|
||||||
|
modifier = Modifier.matchParentSize(),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
content = content,
|
||||||
|
modifier = Modifier.draggableAnchors(
|
||||||
|
state.anchoredDraggableState,
|
||||||
|
Orientation.Horizontal,
|
||||||
|
) { size, _ ->
|
||||||
|
val width = size.width.toFloat()
|
||||||
|
return@draggableAnchors DraggableAnchors {
|
||||||
|
SwipeToDismissBoxValue.Settled at 0f
|
||||||
|
if (enableDismissFromStartToEnd) {
|
||||||
|
SwipeToDismissBoxValue.StartToEnd at width
|
||||||
|
}
|
||||||
|
if (enableDismissFromEndToStart) {
|
||||||
|
SwipeToDismissBoxValue.EndToStart at -width
|
||||||
|
}
|
||||||
|
} to state.targetValue
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DismissVelocityThreshold = 125.dp
|
||||||
+14
-62
@@ -56,7 +56,8 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
// Current quickscale state
|
// Current quickscale state
|
||||||
private val quickScaleThreshold: Float
|
private val quickScaleThreshold =
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
|
||||||
|
|
||||||
// Long click handler
|
// Long click handler
|
||||||
private var longClickJob: Job? = null
|
private var longClickJob: Job? = null
|
||||||
@@ -85,9 +86,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
@ScaleType
|
@ScaleType
|
||||||
private var minimumScaleType = SCALE_TYPE_CENTER_INSIDE
|
private var minimumScaleType = SCALE_TYPE_CENTER_INSIDE
|
||||||
|
|
||||||
// Whether to crop borders.
|
|
||||||
private var cropBorders = false
|
|
||||||
|
|
||||||
// Gesture detection settings
|
// Gesture detection settings
|
||||||
private var panEnabled = true
|
private var panEnabled = true
|
||||||
|
|
||||||
@@ -186,15 +184,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
var isReady = false
|
var isReady = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
|
||||||
* Call to find whether the main image (base layer tiles where relevant) have been loaded. Before
|
|
||||||
* this event the view is blank unless a preview was provided.
|
|
||||||
*
|
|
||||||
* @return true if the main image (not the preview) has been loaded and is ready to display.
|
|
||||||
*/
|
|
||||||
var isImageLoaded = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
// Event listener
|
// Event listener
|
||||||
private var onImageEventListener: OnImageEventListener? = null
|
private var onImageEventListener: OnImageEventListener? = null
|
||||||
|
|
||||||
@@ -218,11 +207,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
private val mtrx = Matrix()
|
private val mtrx = Matrix()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setMinimumDpi(160)
|
|
||||||
setDoubleTapZoomDpi(160)
|
|
||||||
setGestureDetector(context)
|
setGestureDetector(context)
|
||||||
quickScaleThreshold =
|
|
||||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,7 +255,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
sHeight = 0
|
sHeight = 0
|
||||||
srcRect.setEmpty()
|
srcRect.setEmpty()
|
||||||
isReady = false
|
isReady = false
|
||||||
isImageLoaded = false
|
|
||||||
bitmap = null
|
bitmap = null
|
||||||
bitmapIsCached = false
|
bitmapIsCached = false
|
||||||
}
|
}
|
||||||
@@ -768,19 +752,13 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the base layer of tiles or full size bitmap is ready.
|
|
||||||
*/
|
|
||||||
private val isBaseLayerReady: Boolean
|
|
||||||
get() = bitmap != null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether view and image dimensions are known and either a preview, full size image or
|
* Check whether view and image dimensions are known and either a preview, full size image or
|
||||||
* base layer tiles are loaded. First time, send ready event to listener. The next draw will
|
* base layer tiles are loaded. First time, send ready event to listener. The next draw will
|
||||||
* display an image.
|
* display an image.
|
||||||
*/
|
*/
|
||||||
private fun checkReady(): Boolean {
|
private fun checkReady(): Boolean {
|
||||||
val ready = width > 0 && height > 0 && sWidth > 0 && sHeight > 0 && (bitmap != null || isBaseLayerReady)
|
val ready = width > 0 && height > 0 && sWidth > 0 && sHeight > 0 && bitmap != null
|
||||||
if (!isReady && ready) {
|
if (!isReady && ready) {
|
||||||
preDraw()
|
preDraw()
|
||||||
isReady = true
|
isReady = true
|
||||||
@@ -789,19 +767,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
return ready
|
return ready
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether either the full size bitmap or base layer tiles are loaded. First time, send image
|
|
||||||
* loaded event to listener.
|
|
||||||
*/
|
|
||||||
private fun checkImageLoaded(): Boolean {
|
|
||||||
val imageLoaded = isBaseLayerReady
|
|
||||||
if (!isImageLoaded && imageLoaded) {
|
|
||||||
preDraw()
|
|
||||||
isImageLoaded = true
|
|
||||||
}
|
|
||||||
return imageLoaded
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets scale and translate ready for the next draw.
|
* Sets scale and translate ready for the next draw.
|
||||||
*/
|
*/
|
||||||
@@ -832,13 +797,14 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
* is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an
|
* is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an
|
||||||
* animation should be.
|
* animation should be.
|
||||||
*
|
*
|
||||||
* @param _center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.
|
* @param maybeCenter Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.
|
||||||
* @param sat The scale we want and the translation we're aiming for. The values are adjusted to be valid.
|
* @param sat The scale we want and the translation we're aiming for. The values are adjusted to be valid.
|
||||||
*/
|
*/
|
||||||
private fun fitToBounds(_center: Boolean, sat: ScaleAndTranslate) {
|
private fun fitToBounds(maybeCenter: Boolean, sat: ScaleAndTranslate) {
|
||||||
var center = _center
|
val center = if (panLimit == PAN_LIMIT_OUTSIDE && isReady) {
|
||||||
if (panLimit == PAN_LIMIT_OUTSIDE && isReady) {
|
false
|
||||||
center = false
|
} else {
|
||||||
|
maybeCenter
|
||||||
}
|
}
|
||||||
val vTranslate = sat.vTranslate
|
val vTranslate = sat.vTranslate
|
||||||
val scale = limitedScale(sat.scale)
|
val scale = limitedScale(sat.scale)
|
||||||
@@ -920,21 +886,9 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
this.bitmap = bitmap
|
this.bitmap = bitmap
|
||||||
sWidth = srcRect.width()
|
sWidth = srcRect.width()
|
||||||
sHeight = srcRect.height()
|
sHeight = srcRect.height()
|
||||||
val ready = checkReady()
|
checkReady()
|
||||||
val imageLoaded = checkImageLoaded()
|
invalidate()
|
||||||
if (ready || imageLoaded) {
|
requestLayout()
|
||||||
invalidate()
|
|
||||||
requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set border crop of non-filled (white or black) content.
|
|
||||||
*
|
|
||||||
* @param cropBorders Whether to crop image borders.
|
|
||||||
*/
|
|
||||||
fun setCropBorders(cropBorders: Boolean) {
|
|
||||||
this.cropBorders = cropBorders
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -987,7 +941,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
* @param sTarget target object for result. The same instance is also returned.
|
* @param sTarget target object for result. The same instance is also returned.
|
||||||
* @return a coordinate representing the corresponding source coordinate. This is the same instance passed to the sTarget param.
|
* @return a coordinate representing the corresponding source coordinate. This is the same instance passed to the sTarget param.
|
||||||
*/
|
*/
|
||||||
fun viewToSourceCoord(vx: Float, vy: Float, sTarget: PointF = PointF()): PointF? {
|
private fun viewToSourceCoord(vx: Float, vy: Float, sTarget: PointF = PointF()): PointF? {
|
||||||
if (vTranslate == null) {
|
if (vTranslate == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1027,7 +981,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
* @param vTarget target object for result. The same instance is also returned.
|
* @param vTarget target object for result. The same instance is also returned.
|
||||||
* @return view coordinates. This is the same instance passed to the vTarget param.
|
* @return view coordinates. This is the same instance passed to the vTarget param.
|
||||||
*/
|
*/
|
||||||
fun sourceToViewCoord(sx: Float, sy: Float, vTarget: PointF = PointF()): PointF? {
|
private fun sourceToViewCoord(sx: Float, sy: Float, vTarget: PointF = PointF()): PointF? {
|
||||||
if (vTranslate == null) {
|
if (vTranslate == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1704,7 +1658,5 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
|||||||
* State change originated from a double tap zoom anim.
|
* State change originated from a double tap zoom anim.
|
||||||
*/
|
*/
|
||||||
const val ORIGIN_DOUBLE_TAP_ZOOM = 4
|
const val ORIGIN_DOUBLE_TAP_ZOOM = 4
|
||||||
|
|
||||||
private val TAG = SubsamplingScaleImageView::class.java.simpleName
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* https://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.google.accompanist.systemuicontroller
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.view.View
|
||||||
|
import android.view.Window
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class which provides easy-to-use utilities for updating the System UI bar
|
||||||
|
* colors within Jetpack Compose.
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
interface SystemUiController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control for the behavior of the system bars. This value should be one of the
|
||||||
|
* [WindowInsetsControllerCompat] behavior constants:
|
||||||
|
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH] (Deprecated),
|
||||||
|
* [WindowInsetsControllerCompat.BEHAVIOR_DEFAULT] and
|
||||||
|
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE].
|
||||||
|
*/
|
||||||
|
var systemBarsBehavior: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property which holds the status bar visibility. If set to true, show the status bar,
|
||||||
|
* otherwise hide the status bar.
|
||||||
|
*/
|
||||||
|
var isStatusBarVisible: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property which holds the navigation bar visibility. If set to true, show the navigation bar,
|
||||||
|
* otherwise hide the navigation bar.
|
||||||
|
*/
|
||||||
|
var isNavigationBarVisible: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property which holds the status & navigation bar visibility. If set to true, show both bars,
|
||||||
|
* otherwise hide both bars.
|
||||||
|
*/
|
||||||
|
var isSystemBarsVisible: Boolean
|
||||||
|
get() = isNavigationBarVisible && isStatusBarVisible
|
||||||
|
set(value) {
|
||||||
|
isStatusBarVisible = value
|
||||||
|
isNavigationBarVisible = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers a [SystemUiController] for the given [window].
|
||||||
|
*
|
||||||
|
* If no [window] is provided, an attempt to find the correct [Window] is made.
|
||||||
|
*
|
||||||
|
* First, if the [LocalView]'s parent is a [DialogWindowProvider], then that dialog's [Window] will
|
||||||
|
* be used.
|
||||||
|
*
|
||||||
|
* Second, we attempt to find [Window] for the [Activity] containing the [LocalView].
|
||||||
|
*
|
||||||
|
* If none of these are found (such as may happen in a preview), then the functionality of the
|
||||||
|
* returned [SystemUiController] will be degraded, but won't throw an exception.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberSystemUiController(
|
||||||
|
window: Window? = findWindow(),
|
||||||
|
): SystemUiController {
|
||||||
|
val view = LocalView.current
|
||||||
|
return remember(view, window) { AndroidSystemUiController(view, window) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun findWindow(): Window? =
|
||||||
|
(LocalView.current.parent as? DialogWindowProvider)?.window
|
||||||
|
?: LocalView.current.context.findWindow()
|
||||||
|
|
||||||
|
private tailrec fun Context.findWindow(): Window? =
|
||||||
|
when (this) {
|
||||||
|
is Activity -> window
|
||||||
|
is ContextWrapper -> baseContext.findWindow()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class for setting the navigation and status bar colors for a [View], gracefully
|
||||||
|
* degrading behavior based upon API level.
|
||||||
|
*
|
||||||
|
* Typically you would use [rememberSystemUiController] to remember an instance of this.
|
||||||
|
*/
|
||||||
|
internal class AndroidSystemUiController(
|
||||||
|
private val view: View,
|
||||||
|
window: Window?,
|
||||||
|
) : SystemUiController {
|
||||||
|
private val windowInsetsController = window?.let {
|
||||||
|
WindowCompat.getInsetsController(it, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var systemBarsBehavior: Int
|
||||||
|
get() = windowInsetsController?.systemBarsBehavior ?: 0
|
||||||
|
set(value) {
|
||||||
|
windowInsetsController?.systemBarsBehavior = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isStatusBarVisible: Boolean
|
||||||
|
get() {
|
||||||
|
return ViewCompat.getRootWindowInsets(view)
|
||||||
|
?.isVisible(WindowInsetsCompat.Type.statusBars()) == true
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (value) {
|
||||||
|
windowInsetsController?.show(WindowInsetsCompat.Type.statusBars())
|
||||||
|
} else {
|
||||||
|
windowInsetsController?.hide(WindowInsetsCompat.Type.statusBars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isNavigationBarVisible: Boolean
|
||||||
|
get() {
|
||||||
|
return ViewCompat.getRootWindowInsets(view)
|
||||||
|
?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (value) {
|
||||||
|
windowInsetsController?.show(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
} else {
|
||||||
|
windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,12 +46,14 @@ import com.hippo.ehviewer.coil.MergeInterceptor
|
|||||||
import com.hippo.ehviewer.dailycheck.checkDawn
|
import com.hippo.ehviewer.dailycheck.checkDawn
|
||||||
import com.hippo.ehviewer.dao.SearchDatabase
|
import com.hippo.ehviewer.dao.SearchDatabase
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
|
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||||
import com.hippo.ehviewer.ktbuilder.diskCache
|
import com.hippo.ehviewer.ktbuilder.diskCache
|
||||||
import com.hippo.ehviewer.ktbuilder.imageLoader
|
import com.hippo.ehviewer.ktbuilder.imageLoader
|
||||||
import com.hippo.ehviewer.ktor.CronetEngine
|
import com.hippo.ehviewer.ktor.CronetEngine
|
||||||
import com.hippo.ehviewer.legacy.cleanObsoleteCache
|
import com.hippo.ehviewer.legacy.cleanObsoleteCache
|
||||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||||
import com.hippo.ehviewer.ui.lockObserver
|
import com.hippo.ehviewer.ui.lockObserver
|
||||||
|
import com.hippo.ehviewer.ui.tools.dataStateFlow
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.Crash
|
import com.hippo.ehviewer.util.Crash
|
||||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||||
@@ -103,13 +105,13 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
System.loadLibrary("ehviewer")
|
System.loadLibrary("ehviewer")
|
||||||
lifecycleScope.launchIO {
|
lifecycleScope.launchIO {
|
||||||
|
launch { EhTagDatabase }
|
||||||
|
launch { EhDB }
|
||||||
|
dataStateFlow.value
|
||||||
launch {
|
launch {
|
||||||
EhTagDatabase
|
if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode.key !in Settings.prefs) {
|
||||||
}
|
Settings.downloadFilterMode.value = DownloadsFilterMode.CUSTOM.flag
|
||||||
launch {
|
}
|
||||||
EhDB
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
DownloadManager.readMetadataFromLocal()
|
DownloadManager.readMetadataFromLocal()
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import android.net.Uri
|
|||||||
import arrow.fx.coroutines.release
|
import arrow.fx.coroutines.release
|
||||||
import arrow.fx.coroutines.resource
|
import arrow.fx.coroutines.resource
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
|
import com.hippo.ehviewer.dao.DownloadArtist
|
||||||
import com.hippo.ehviewer.dao.DownloadDirname
|
import com.hippo.ehviewer.dao.DownloadDirname
|
||||||
import com.hippo.ehviewer.dao.DownloadInfo
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
import com.hippo.ehviewer.dao.DownloadLabel
|
import com.hippo.ehviewer.dao.DownloadLabel
|
||||||
@@ -113,8 +114,11 @@ object EhDB {
|
|||||||
dao.insertOrIgnore(downloadDirnameList)
|
dao.insertOrIgnore(downloadDirnameList)
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadsCount
|
val downloadsCountByLabel
|
||||||
get() = db.downloadsDao().count()
|
get() = db.downloadsDao().countByLabel()
|
||||||
|
|
||||||
|
val downloadsCountByArtist
|
||||||
|
get() = db.downloadsDao().countByArtist()
|
||||||
|
|
||||||
suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list()
|
suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list()
|
||||||
|
|
||||||
@@ -142,6 +146,14 @@ object EhDB {
|
|||||||
dao.fill(raw.position)
|
dao.fill(raw.position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun putDownloadArtist(gid: Long, artists: List<DownloadArtist>) {
|
||||||
|
if (artists.isNotEmpty()) {
|
||||||
|
val dao = db.downloadArtistDao()
|
||||||
|
dao.deleteByGid(gid)
|
||||||
|
dao.insert(artists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun removeLocalFavorites(galleryInfo: BaseGalleryInfo) {
|
suspend fun removeLocalFavorites(galleryInfo: BaseGalleryInfo) {
|
||||||
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
|
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
|
||||||
deleteGalleryInfo(galleryInfo)
|
deleteGalleryInfo(galleryInfo)
|
||||||
@@ -279,49 +291,39 @@ object EhDB {
|
|||||||
it.close()
|
it.close()
|
||||||
context.deleteDatabase(tempDBName)
|
context.deleteDatabase(tempDBName)
|
||||||
} use { oldDB ->
|
} use { oldDB ->
|
||||||
runCatching {
|
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
|
||||||
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
|
|
||||||
|
db.progressDao().insertOrIgnore(oldDB.progressDao().list())
|
||||||
|
|
||||||
|
val downloadLabelList = oldDB.downloadLabelDao().list()
|
||||||
|
DownloadManager.addDownloadLabel(downloadLabelList)
|
||||||
|
|
||||||
|
oldDB.downloadDirnameDao().list().let {
|
||||||
|
importDownloadDirname(it)
|
||||||
}
|
}
|
||||||
runCatching {
|
|
||||||
db.progressDao().insertOrIgnore(oldDB.progressDao().list())
|
val downloadInfoList = oldDB.downloadsDao().joinList().asReversed()
|
||||||
|
DownloadManager.addDownload(downloadInfoList)
|
||||||
|
|
||||||
|
val historyInfoList = oldDB.historyDao().list()
|
||||||
|
importHistoryInfo(historyInfoList)
|
||||||
|
|
||||||
|
val quickSearchList = oldDB.quickSearchDao().list()
|
||||||
|
val currentQuickSearchList = db.quickSearchDao().list()
|
||||||
|
val offset = currentQuickSearchList.size
|
||||||
|
val importList = quickSearchList.filter { newQS ->
|
||||||
|
currentQuickSearchList.none { it.name == newQS.name }
|
||||||
|
}.onEachIndexed { index, q -> q.position = index + offset }
|
||||||
|
importQuickSearch(importList)
|
||||||
|
|
||||||
|
oldDB.localFavoritesDao().list().let {
|
||||||
|
importLocalFavorites(it)
|
||||||
}
|
}
|
||||||
runCatching {
|
|
||||||
val downloadLabelList = oldDB.downloadLabelDao().list()
|
val filterList = oldDB.filterDao().list()
|
||||||
DownloadManager.addDownloadLabel(downloadLabelList)
|
val currentFilterList = db.filterDao().list()
|
||||||
}
|
filterList.forEach {
|
||||||
runCatching {
|
if (it !in currentFilterList) addFilter(it)
|
||||||
oldDB.downloadDirnameDao().list().let {
|
|
||||||
importDownloadDirname(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runCatching {
|
|
||||||
val downloadInfoList = oldDB.downloadsDao().joinList().asReversed()
|
|
||||||
DownloadManager.addDownload(downloadInfoList)
|
|
||||||
}
|
|
||||||
runCatching {
|
|
||||||
val historyInfoList = oldDB.historyDao().list()
|
|
||||||
importHistoryInfo(historyInfoList)
|
|
||||||
}
|
|
||||||
runCatching {
|
|
||||||
val quickSearchList = oldDB.quickSearchDao().list()
|
|
||||||
val currentQuickSearchList = db.quickSearchDao().list()
|
|
||||||
val offset = currentQuickSearchList.size
|
|
||||||
val importList = quickSearchList.filter { newQS ->
|
|
||||||
currentQuickSearchList.none { it.name == newQS.name }
|
|
||||||
}.onEachIndexed { index, q -> q.position = index + offset }
|
|
||||||
importQuickSearch(importList)
|
|
||||||
}
|
|
||||||
runCatching {
|
|
||||||
oldDB.localFavoritesDao().list().let {
|
|
||||||
importLocalFavorites(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runCatching {
|
|
||||||
val filterList = oldDB.filterDao().list()
|
|
||||||
val currentFilterList = db.filterDao().list()
|
|
||||||
filterList.forEach {
|
|
||||||
if (it !in currentFilterList) addFilter(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import androidx.compose.runtime.State
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||||
|
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||||
import com.hippo.ehviewer.download.SortMode
|
import com.hippo.ehviewer.download.SortMode
|
||||||
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -110,6 +112,8 @@ object Settings : DataStorePreferences(null) {
|
|||||||
val listThumbSize = intPref("list_tile_size", 40)
|
val listThumbSize = intPref("list_tile_size", 40)
|
||||||
val languageFilter = intPref("language_filter", -1)
|
val languageFilter = intPref("language_filter", -1)
|
||||||
val downloadSortMode = intPref("download_sort_mode", SortMode.Default.flag)
|
val downloadSortMode = intPref("download_sort_mode", SortMode.Default.flag)
|
||||||
|
val downloadFilterMode = intPref("download_filter_mode", DownloadsFilterMode.Default.flag)
|
||||||
|
val hasSignedIn = boolPref("has_signed_in", EhCookieStore.hasSignedIn())
|
||||||
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
|
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
|
||||||
val blackDarkTheme = boolPref("black_dark_theme", false)
|
val blackDarkTheme = boolPref("black_dark_theme", false)
|
||||||
val gridView = boolPref("grid_view", false)
|
val gridView = boolPref("grid_view", false)
|
||||||
@@ -117,7 +121,8 @@ object Settings : DataStorePreferences(null) {
|
|||||||
val qSSaveProgress = boolPref("qs_save_progress", true)
|
val qSSaveProgress = boolPref("qs_save_progress", true)
|
||||||
val security = boolPref("require_unlock", false)
|
val security = boolPref("require_unlock", false)
|
||||||
val animateItems = boolPref("animate_items", true)
|
val animateItems = boolPref("animate_items", true)
|
||||||
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
|
val displayName = stringOrNullPref("display_name", null)
|
||||||
|
val recentDownloadLabel = stringOrNullPref("recent_download_label", "")
|
||||||
|
|
||||||
var downloadScheme by stringOrNullPref("image_scheme", null)
|
var downloadScheme by stringOrNullPref("image_scheme", null)
|
||||||
var downloadAuthority by stringOrNullPref("image_authority", null)
|
var downloadAuthority by stringOrNullPref("image_authority", null)
|
||||||
@@ -144,7 +149,7 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
|
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
|
||||||
var enabledSecurity by boolPref("enable_secure", false)
|
var enabledSecurity by boolPref("enable_secure", false)
|
||||||
var backupBeforeUpdate by boolPref("backup_before_update", true)
|
var backupBeforeUpdate by boolPref("backup_before_update", true)
|
||||||
var useCIUpdateChannel by boolPref("ci_update_channel", false)
|
var useCIUpdateChannel by boolPref("ci_update_channel", AppConfig.isSnapshot)
|
||||||
var mediaScan by boolPref("media_scan", false).observed { updateWhenKeepMediaStatusChanges() }
|
var mediaScan by boolPref("media_scan", false).observed { updateWhenKeepMediaStatusChanges() }
|
||||||
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
|
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
|
||||||
var saveParseErrorBody by boolPref("save_parse_error_body", true)
|
var saveParseErrorBody by boolPref("save_parse_error_body", true)
|
||||||
@@ -160,21 +165,13 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
||||||
var securityDelay by intPref("require_unlock_delay", 0)
|
var securityDelay by intPref("require_unlock_delay", 0)
|
||||||
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
|
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
|
||||||
var searchCategory by intPref("search_pref", EhUtils.ALL_CATEGORY)
|
|
||||||
var requestNewsTime by intPref("request_news_time", 0).observed { updateWhenRequestNewsChanges() }
|
var requestNewsTime by intPref("request_news_time", 0).observed { updateWhenRequestNewsChanges() }
|
||||||
var updateIntervalDays by intPref("update_interval_days", 7)
|
var updateIntervalDays by intPref("update_interval_days", 7)
|
||||||
var lastDawnDays by intPref("last_dawn_days", 0)
|
var lastDawnDays by intPref("last_dawn_days", 0)
|
||||||
var recentToplist by stringPref("recent_toplist", "11")
|
var recentToplist by stringPref("recent_toplist", "11")
|
||||||
var userAgent by stringPref("user_agent", CHROME_USER_AGENT)
|
var userAgent by stringPref("user_agent", CHROME_USER_AGENT)
|
||||||
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
|
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
|
||||||
var displayName by stringOrNullPref("display_name", null)
|
var lastUpdateTime by longPref("last_update_time", BuildConfig.COMMIT_TIME)
|
||||||
var avatar by stringOrNullPref("avatar", null)
|
|
||||||
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
|
|
||||||
var lastUpdateTime by longPref("last_update_time", 0)
|
|
||||||
var favDialogTheta by floatPref("fav_select_dialog_delta", 0F)
|
|
||||||
|
|
||||||
// TODO: Remove this after swipe gestures are correctly handled in compose
|
|
||||||
var touchSlopFactor by intPref("touch_slop", 3)
|
|
||||||
|
|
||||||
// Tachiyomi Reader
|
// Tachiyomi Reader
|
||||||
var newReader by boolPref("new_compose_reader", false)
|
var newReader by boolPref("new_compose_reader", false)
|
||||||
@@ -203,7 +200,8 @@ object Settings : DataStorePreferences(null) {
|
|||||||
val invertedColors = boolPref("pref_inverted_colors", false)
|
val invertedColors = boolPref("pref_inverted_colors", false)
|
||||||
val readerWebtoonNav = intPref("reader_navigation_mode_webtoon", 0)
|
val readerWebtoonNav = intPref("reader_navigation_mode_webtoon", 0)
|
||||||
val readerPagerNav = intPref("reader_navigation_mode_pager", 0)
|
val readerPagerNav = intPref("reader_navigation_mode_pager", 0)
|
||||||
val readerWebtoonNavInverted = intPref("reader_tapping_inverted_webtoon", 0)
|
val readerPagerNavInverted = intPref("reader_tapping_inverted_2", 0)
|
||||||
|
val readerWebtoonNavInverted = intPref("reader_tapping_inverted_webtoon_2", 0)
|
||||||
val webtoonSidePadding = intPref("webtoon_side_padding", 0)
|
val webtoonSidePadding = intPref("webtoon_side_padding", 0)
|
||||||
val navigateToPan = boolPref("navigate_pan", true)
|
val navigateToPan = boolPref("navigate_pan", true)
|
||||||
val imageScaleType = intPref("pref_image_scale_type_key", 1)
|
val imageScaleType = intPref("pref_image_scale_type_key", 1)
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package com.hippo.ehviewer
|
|||||||
|
|
||||||
import android.app.UiModeManager
|
import android.app.UiModeManager
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.os.LocaleListCompat
|
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.client.EhTagDatabase
|
import com.hippo.ehviewer.client.EhTagDatabase
|
||||||
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
|
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
|
||||||
@@ -27,15 +25,6 @@ fun <T, R : Settings.Delegate<T>> R.observed(func: (Unit) -> Unit) = apply { col
|
|||||||
fun <T, R : Settings.Delegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { flowGetter().collect { flow.emit(Unit) } } }
|
fun <T, R : Settings.Delegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { flowGetter().collect { flow.emit(Unit) } } }
|
||||||
fun <T, R : PrefDelegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { changesFlow().collect { flow.emit(Unit) } } }
|
fun <T, R : PrefDelegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { changesFlow().collect { flow.emit(Unit) } } }
|
||||||
|
|
||||||
fun updateWhenLocaleChanges() {
|
|
||||||
val newValue = Settings.language
|
|
||||||
if ("system" == newValue) {
|
|
||||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
|
|
||||||
} else {
|
|
||||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateWhenKeepMediaStatusChanges() {
|
fun updateWhenKeepMediaStatusChanges() {
|
||||||
collectScope.launchIO {
|
collectScope.launchIO {
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -64,7 +53,7 @@ fun updateWhenRequestNewsChanges() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateWhenGallerySiteChanges() {
|
fun updateWhenGallerySiteChanges() {
|
||||||
if (!Settings.needSignIn && EhCookieStore.hasSignedIn()) {
|
if (Settings.hasSignedIn.value) {
|
||||||
collectScope.launchIO {
|
collectScope.launchIO {
|
||||||
runCatching {
|
runCatching {
|
||||||
EhEngine.getUConfig()
|
EhEngine.getUConfig()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import io.ktor.http.renderSetCookieHeader
|
|||||||
|
|
||||||
object EhCookieStore : CookiesStorage {
|
object EhCookieStore : CookiesStorage {
|
||||||
private val manager = CookieManager.getInstance()
|
private val manager = CookieManager.getInstance()
|
||||||
fun signOut() = manager.removeAllCookies(null)
|
fun removeAllCookies() = manager.removeAllCookies(null)
|
||||||
fun contains(url: String, name: String) = load(Url(url)).fastAny { it.name == name }
|
fun contains(url: String, name: String) = load(Url(url)).fastAny { it.name == name }
|
||||||
|
|
||||||
fun hasSignedIn(): Boolean {
|
fun hasSignedIn(): Boolean {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package com.hippo.ehviewer.client
|
|||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.left
|
import arrow.core.left
|
||||||
|
import arrow.core.partially2
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import arrow.fx.coroutines.parMap
|
import arrow.fx.coroutines.parMap
|
||||||
import arrow.fx.coroutines.parZip
|
import arrow.fx.coroutines.parZip
|
||||||
@@ -44,8 +45,10 @@ import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
|||||||
import com.hippo.ehviewer.client.parser.HomeParser
|
import com.hippo.ehviewer.client.parser.HomeParser
|
||||||
import com.hippo.ehviewer.client.parser.ProfileParser
|
import com.hippo.ehviewer.client.parser.ProfileParser
|
||||||
import com.hippo.ehviewer.client.parser.RateGalleryResult
|
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.TorrentParser
|
||||||
import com.hippo.ehviewer.client.parser.TorrentResult
|
import com.hippo.ehviewer.client.parser.TorrentResult
|
||||||
|
import com.hippo.ehviewer.client.parser.UserConfigParser
|
||||||
import com.hippo.ehviewer.client.parser.VoteCommentResult
|
import com.hippo.ehviewer.client.parser.VoteCommentResult
|
||||||
import com.hippo.ehviewer.client.parser.VoteTagParser
|
import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||||
import com.hippo.ehviewer.dailycheck.showEventNotification
|
import com.hippo.ehviewer.dailycheck.showEventNotification
|
||||||
@@ -62,6 +65,7 @@ import io.ktor.utils.io.pool.useInstance
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
@@ -80,11 +84,9 @@ private const val MAX_REQUEST_SIZE = 25
|
|||||||
private const val MAX_SEQUENTIAL_REQUESTS = 5
|
private const val MAX_SEQUENTIAL_REQUESTS = 5
|
||||||
private const val REQUEST_INTERVAL = 5000L
|
private const val REQUEST_INTERVAL = 5000L
|
||||||
|
|
||||||
private const val U_CONFIG_TEXT = "Selected Profile"
|
|
||||||
|
|
||||||
fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
|
fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
|
||||||
val dir = AppConfig.externalParseErrorDir ?: return
|
val dir = AppConfig.externalParseErrorDir ?: return
|
||||||
val file = File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt")
|
val file = File(dir, ReadableTime.getFilenamableTime() + ".txt")
|
||||||
file.sink().buffer().use { sink ->
|
file.sink().buffer().use { sink ->
|
||||||
with(sink) {
|
with(sink) {
|
||||||
writeUtf8(e.message + "\n")
|
writeUtf8(e.message + "\n")
|
||||||
@@ -177,16 +179,20 @@ object EhEngine {
|
|||||||
return ehRequest(url, referer).fetchUsingAsByteBuffer(TorrentParser::parse)
|
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): ArchiveParser.Result {
|
||||||
.fetchUsingAsText(ArchiveParser::parse)
|
val funds = if (EhUtils.isExHentai) getFunds() else null
|
||||||
.apply { funds = funds ?: ehRequest(EhUrl.URL_FUNDS).fetchUsingAsText(HomeParser::parseFunds) }
|
return ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
|
||||||
|
.fetchUsingAsText(ArchiveParser::parse.partially2(funds))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getImageLimits() = parZip(
|
suspend fun getImageLimits() = parZip(
|
||||||
{ ehRequest(EhUrl.URL_HOME).fetchUsingAsByteBuffer(HomeParser::parse) },
|
{ ehRequest(EhUrl.URL_HOME).fetchUsingAsByteBuffer(HomeParser::parse) },
|
||||||
{ ehRequest(EhUrl.URL_FUNDS).fetchUsingAsText(HomeParser::parseFunds) },
|
{ getFunds() },
|
||||||
{ limits, funds -> HomeParser.Result(limits, funds) },
|
{ limits, funds -> HomeParser.Result(limits, funds) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private suspend fun getFunds() = ehRequest(EhUrl.URL_FUNDS).fetchUsingAsText(HomeParser::parseFunds)
|
||||||
|
|
||||||
suspend fun getNews(parse: Boolean) = ehRequest(EhUrl.URL_NEWS, EhUrl.REFERER_E)
|
suspend fun getNews(parse: Boolean) = ehRequest(EhUrl.URL_NEWS, EhUrl.REFERER_E)
|
||||||
.fetchUsingAsText { if (parse) EventPaneParser.parse(this) else null }
|
.fetchUsingAsText { if (parse) EventPaneParser.parse(this) else null }
|
||||||
|
|
||||||
@@ -197,12 +203,12 @@ object EhEngine {
|
|||||||
|
|
||||||
suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {
|
suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {
|
||||||
runSuspendCatching {
|
runSuspendCatching {
|
||||||
ehRequest(url).fetchUsingAsText { check(U_CONFIG_TEXT in this) { "Unable to load config from $url!" } }
|
ehRequest(url).fetchUsingAsByteBuffer(UserConfigParser::parse)
|
||||||
}.onFailure { throwable ->
|
}.onFailure { throwable ->
|
||||||
// It may get redirected when accessing ex for the first time
|
// It may get redirected when accessing ex for the first time
|
||||||
if (url == EhUrl.URL_UCONFIG_EX) {
|
if (url == EhUrl.URL_UCONFIG_EX) {
|
||||||
logcat(throwable)
|
logcat(throwable)
|
||||||
ehRequest(url).fetchUsingAsText { check(U_CONFIG_TEXT in this) { "Unable to load config from $url!" } }
|
ehRequest(url).fetchUsingAsByteBuffer(UserConfigParser::parse)
|
||||||
} else {
|
} else {
|
||||||
throw throwable
|
throw throwable
|
||||||
}
|
}
|
||||||
@@ -234,6 +240,22 @@ object EhEngine {
|
|||||||
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsByteBuffer(FavoritesParser::parse)
|
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsByteBuffer(FavoritesParser::parse)
|
||||||
.apply { galleryInfoList.fillInfo(url) }
|
.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) {
|
suspend fun commentGallery(url: String, comment: String, id: Long = -1) = ehRequest(url, url, EhUrl.origin) {
|
||||||
formBody {
|
formBody {
|
||||||
if (id == -1L) {
|
if (id == -1L) {
|
||||||
@@ -431,13 +453,13 @@ object EhEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addFavorites(galleryList: List<Pair<Long, String>>, dstCat: Int) {
|
suspend fun addFavorites(galleryList: List<Pair<Long, String>>, dstCat: Int) {
|
||||||
galleryList.chunked(MAX_SEQUENTIAL_REQUESTS).forEachIndexed { index, chunk ->
|
galleryList.forEach { (gid, token) ->
|
||||||
if (index != 0) {
|
// https://github.com/FooIbar/EhViewer/issues/1190
|
||||||
delay(REQUEST_INTERVAL)
|
// Workaround for duplicate items when sorting by favorited time
|
||||||
}
|
val timeTaken = measureTimeMillis {
|
||||||
chunk.parMap { (gid, token) ->
|
|
||||||
modifyFavorites(gid, token, dstCat)
|
modifyFavorites(gid, token, dstCat)
|
||||||
}
|
}
|
||||||
|
delay(1000 - timeTaken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.client
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.BuildConfig
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
|
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
@@ -50,7 +51,7 @@ inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
|
|||||||
|
|
||||||
val json = Json { ignoreUnknownKeys = true }
|
val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
const val CHROME_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36"
|
const val CHROME_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Mobile Safari/537.36"
|
||||||
const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
||||||
const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.5"
|
const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.5"
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ object EhUrl {
|
|||||||
const val API_E = "https://api.e-hentai.org/api.php"
|
const val API_E = "https://api.e-hentai.org/api.php"
|
||||||
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
|
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
|
||||||
const val URL_MY_TAGS_E = HOST_E + "mytags"
|
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_E = "https://e-hentai.org/popular"
|
||||||
const val URL_POPULAR_EX = "https://exhentai.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"
|
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
|
||||||
|
|||||||
@@ -152,9 +152,9 @@ object EhUtils {
|
|||||||
val categoryTextColor = Color(0xffe6e0e9)
|
val categoryTextColor = Color(0xffe6e0e9)
|
||||||
|
|
||||||
fun signOut() {
|
fun signOut() {
|
||||||
EhCookieStore.signOut()
|
EhCookieStore.removeAllCookies()
|
||||||
Settings.avatar = null
|
Settings.displayName.value = null
|
||||||
Settings.displayName = null
|
Settings.hasSignedIn.value = false
|
||||||
Settings.gallerySite = EhUrl.SITE_E
|
Settings.gallerySite = EhUrl.SITE_E
|
||||||
Settings.needSignIn = true
|
Settings.needSignIn = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ data class ListUrlBuilder(
|
|||||||
fun build(): String {
|
fun build(): String {
|
||||||
return when (mode) {
|
return when (mode) {
|
||||||
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl(EhUrl.WATCHED_PATH.takeIf { mode == MODE_SUBSCRIPTION }) {
|
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl(EhUrl.WATCHED_PATH.takeIf { mode == MODE_SUBSCRIPTION }) {
|
||||||
|
val category = if (!Settings.hasSignedIn.value && category <= 0) EhUtils.NON_H else category
|
||||||
if (category > 0) {
|
if (category > 0) {
|
||||||
addQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
|
addQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ object ArchiveParser {
|
|||||||
private const val ERROR_INSUFFICIENT_FUNDS =
|
private const val ERROR_INSUFFICIENT_FUNDS =
|
||||||
"You do not have enough funds to download this archive."
|
"You do not have enough funds to download this archive."
|
||||||
|
|
||||||
fun parse(body: String): Result {
|
fun parse(body: String, maybeFunds: Funds?): Result {
|
||||||
val m = PATTERN_HATH_FORM.find(body)!!
|
val m = PATTERN_HATH_FORM.find(body)!!
|
||||||
val paramOr = m.groupValues[1]
|
val paramOr = m.groupValues[1]
|
||||||
val archiveList = ArrayList<Archive>()
|
val archiveList = ArrayList<Archive>()
|
||||||
@@ -59,14 +59,12 @@ object ArchiveParser {
|
|||||||
val item = Archive(res, name, size, cost, true)
|
val item = Archive(res, name, size, cost, true)
|
||||||
archiveList.add(item)
|
archiveList.add(item)
|
||||||
}
|
}
|
||||||
val result = Result(paramOr, archiveList, null)
|
val funds = maybeFunds ?: PATTERN_CURRENT_FUNDS.find(body)!!.groupValues.run {
|
||||||
PATTERN_CURRENT_FUNDS.find(body)?.groupValues?.run {
|
|
||||||
val fundsGP = ParserUtils.parseInt(get(1), 0)
|
val fundsGP = ParserUtils.parseInt(get(1), 0)
|
||||||
val fundsC = ParserUtils.parseInt(get(2), 0)
|
val fundsC = ParserUtils.parseInt(get(2), 0)
|
||||||
val funds = HomeParser.Funds(fundsGP, fundsC)
|
Funds("%,d".format(fundsGP), "%,d".format(fundsC))
|
||||||
result.funds = funds
|
|
||||||
}
|
}
|
||||||
return result
|
return Result(paramOr, archiveList, funds)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseArchiveUrl(body: String): String? {
|
fun parseArchiveUrl(body: String): String? {
|
||||||
@@ -80,13 +78,13 @@ object ArchiveParser {
|
|||||||
// TODO: Check more errors
|
// TODO: Check more errors
|
||||||
}
|
}
|
||||||
|
|
||||||
class Archive(
|
data class Result(val paramOr: String, val archiveList: List<Archive>, val funds: Funds)
|
||||||
val res: String,
|
|
||||||
val name: String,
|
|
||||||
val size: String,
|
|
||||||
val cost: String,
|
|
||||||
val isHAtH: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Result(val paramOr: String, val archiveList: List<Archive>, var funds: HomeParser.Funds?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Archive(
|
||||||
|
val res: String,
|
||||||
|
val name: String,
|
||||||
|
val size: String,
|
||||||
|
val cost: String,
|
||||||
|
val isHAtH: Boolean,
|
||||||
|
)
|
||||||
@@ -25,18 +25,18 @@ object HomeParser {
|
|||||||
PATTERN_FUNDS.find(body)?.groupValues?.run {
|
PATTERN_FUNDS.find(body)?.groupValues?.run {
|
||||||
val fundsC = ParserUtils.parseInt(get(1), 0)
|
val fundsC = ParserUtils.parseInt(get(1), 0)
|
||||||
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
|
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
|
||||||
return Funds(fundsGP, fundsC)
|
return Funds("%,d+".format(fundsGP), "%,d".format(fundsC))
|
||||||
}
|
}
|
||||||
throw ParseException("Parse funds error")
|
throw ParseException("Parse funds error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Funds(val fundsGP: Int, val fundsC: Int) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Result(val limits: Limits, val funds: Funds) : Parcelable
|
data class Result(val limits: Limits, val funds: Funds) : Parcelable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Funds(val gp: String, val credit: String) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.util.toEpochMillis
|
||||||
import com.hippo.ehviewer.util.toIntOrDefault
|
import com.hippo.ehviewer.util.toIntOrDefault
|
||||||
import com.hippo.ehviewer.util.toLocalDateTime
|
import com.hippo.ehviewer.util.toLocalDateTime
|
||||||
import com.hippo.ehviewer.util.toLongOrDefault
|
import com.hippo.ehviewer.util.toLongOrDefault
|
||||||
@@ -36,11 +37,14 @@ object ParserUtils {
|
|||||||
minute()
|
minute()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun formatDate(time: Long): String {
|
fun formatDate(time: Long): String {
|
||||||
return formatter.format(time.toLocalDateTime())
|
return formatter.format(time.toLocalDateTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parseDate(str: String): Long {
|
||||||
|
return formatter.parse(str).toEpochMillis()
|
||||||
|
}
|
||||||
|
|
||||||
fun trim(str: String?): String {
|
fun trim(str: String?): String {
|
||||||
return str?.unescapeXml()?.trim() ?: ""
|
return str?.unescapeXml()?.trim() ?: ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
object SignInParser {
|
||||||
|
private val NAME_PATTERN = Regex("<p>You are now logged in as: (.+?)<")
|
||||||
|
private val ERROR_PATTERN = Regex(
|
||||||
|
"<h4>The error returned was:</h4>\\s*<p>(.+?)</p>" +
|
||||||
|
"|<span class=\"postcolor\">(.+?)</span>",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun parse(body: String): String {
|
||||||
|
return NAME_PATTERN.find(body)?.let {
|
||||||
|
it.groupValues[1]
|
||||||
|
} ?: ERROR_PATTERN.find(body)?.let {
|
||||||
|
throw EhException(it.groupValues[1].ifEmpty { it.groupValues[2] })
|
||||||
|
} ?: throw ParseException("Can't parse sign in")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,16 +14,18 @@ object TorrentParser {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Torrent(
|
data class Torrent(
|
||||||
|
val outdated: Boolean,
|
||||||
val posted: String,
|
val posted: String,
|
||||||
val size: String,
|
val size: String,
|
||||||
val seeds: Int,
|
val seeds: Int,
|
||||||
val peers: Int,
|
val peers: Int,
|
||||||
val downloads: Int,
|
val downloads: Int,
|
||||||
|
val uploader: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Torrent.format() = "[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloads]"
|
fun Torrent.format() = "↑$seeds ↓$peers ✓$downloads"
|
||||||
|
|
||||||
typealias TorrentResult = List<Torrent>
|
typealias TorrentResult = List<Torrent>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
object UserConfigParser {
|
||||||
|
private external fun parseFavCat(body: ByteBuffer, size: Int = body.limit()): Int
|
||||||
|
|
||||||
|
fun parse(body: ByteBuffer) = runCatching {
|
||||||
|
val favCat = unmarshalParsingAs<Array<String>>(body, ::parseFavCat)
|
||||||
|
Settings.favCat = favCat
|
||||||
|
}.onFailure {
|
||||||
|
throw ParseException("Can't parse user config", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
@@ -83,7 +82,7 @@ val today
|
|||||||
get() = (Clock.System.now().epochSeconds / 86400).toInt()
|
get() = (Clock.System.now().epochSeconds / 86400).toInt()
|
||||||
|
|
||||||
suspend fun checkDawn() = runCatching {
|
suspend fun checkDawn() = runCatching {
|
||||||
if (EhCookieStore.hasSignedIn() && Settings.lastDawnDays != today) {
|
if (Settings.hasSignedIn.value && Settings.lastDawnDays != today) {
|
||||||
EhEngine.getNews(true)?.let {
|
EhEngine.getNews(true)?.let {
|
||||||
Settings.lastDawnDays = today
|
Settings.lastDawnDays = today
|
||||||
showEventNotification(it)
|
showEventNotification(it)
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
|||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class,
|
BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class, DownloadArtist::class,
|
||||||
Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, ProgressInfo::class, QuickSearch::class,
|
Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, ProgressInfo::class, QuickSearch::class,
|
||||||
GalleryEntityFts::class,
|
GalleryEntityFts::class,
|
||||||
],
|
],
|
||||||
version = 22,
|
version = 23,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 4, to = 5, spec = Schema4to5::class),
|
AutoMigration(from = 4, to = 5, spec = Schema4to5::class),
|
||||||
@@ -32,11 +32,13 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
|||||||
AutoMigration(from = 19, to = 20),
|
AutoMigration(from = 19, to = 20),
|
||||||
AutoMigration(from = 20, to = 21, spec = Schema20to21::class),
|
AutoMigration(from = 20, to = 21, spec = Schema20to21::class),
|
||||||
AutoMigration(from = 21, to = 22, spec = Schema21to22::class),
|
AutoMigration(from = 21, to = 22, spec = Schema21to22::class),
|
||||||
|
AutoMigration(from = 22, to = 23),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(FilterModeConverter::class)
|
@TypeConverters(FilterModeConverter::class)
|
||||||
abstract class EhDatabase : RoomDatabase() {
|
abstract class EhDatabase : RoomDatabase() {
|
||||||
abstract fun galleryDao(): GalleryDao
|
abstract fun galleryDao(): GalleryDao
|
||||||
|
abstract fun downloadArtistDao(): DownloadArtistDao
|
||||||
abstract fun downloadDirnameDao(): DownloadDirnameDao
|
abstract fun downloadDirnameDao(): DownloadDirnameDao
|
||||||
abstract fun downloadLabelDao(): DownloadLabelDao
|
abstract fun downloadLabelDao(): DownloadLabelDao
|
||||||
abstract fun downloadsDao(): DownloadsDao
|
abstract fun downloadsDao(): DownloadsDao
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.hippo.ehviewer.dao
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "DOWNLOAD_ARTISTS",
|
||||||
|
primaryKeys = ["GID", "ARTIST"],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(DownloadEntity::class, ["GID"], ["GID"], onDelete = ForeignKey.CASCADE),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
data class DownloadArtist(
|
||||||
|
@ColumnInfo(name = "GID")
|
||||||
|
val gid: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "ARTIST")
|
||||||
|
val artist: String,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(gid: Long, artists: Iterable<String>) = artists.map { DownloadArtist(gid, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.hippo.ehviewer.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface DownloadArtistDao {
|
||||||
|
@Insert
|
||||||
|
suspend fun insert(downloadArtists: List<DownloadArtist>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM DOWNLOAD_ARTISTS WHERE GID = :gid")
|
||||||
|
suspend fun deleteByGid(gid: Long)
|
||||||
|
}
|
||||||
@@ -67,9 +67,13 @@ data class DownloadInfo(
|
|||||||
@ColumnInfo(name = "DIRNAME")
|
@ColumnInfo(name = "DIRNAME")
|
||||||
val dirname: String?,
|
val dirname: String?,
|
||||||
|
|
||||||
|
@Relation(parentColumn = "GID", entityColumn = "GID")
|
||||||
|
var artistInfoList: List<DownloadArtist> = emptyList(),
|
||||||
|
|
||||||
@Embedded
|
@Embedded
|
||||||
val downloadInfo: DownloadEntity = DownloadEntity(galleryInfo.gid),
|
val downloadInfo: DownloadEntity = DownloadEntity(galleryInfo.gid),
|
||||||
) : GalleryInfo by galleryInfo, AbstractDownloadInfo by downloadInfo {
|
) : GalleryInfo by galleryInfo, AbstractDownloadInfo by downloadInfo {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val STATE_INVALID = -1
|
const val STATE_INVALID = -1
|
||||||
const val STATE_NONE = 0
|
const val STATE_NONE = 0
|
||||||
@@ -79,3 +83,6 @@ data class DownloadInfo(
|
|||||||
const val STATE_FAILED = 4
|
const val STATE_FAILED = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val DownloadInfo.artists: List<String>
|
||||||
|
get() = artistInfoList.map { it.artist }
|
||||||
@@ -14,7 +14,11 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
interface DownloadsDao {
|
interface DownloadsDao {
|
||||||
@Suppress("ktlint:standard:annotation")
|
@Suppress("ktlint:standard:annotation")
|
||||||
@Query("SELECT LABEL, COUNT(*) AS COUNT FROM DOWNLOADS LEFT JOIN DOWNLOAD_LABELS USING(LABEL) GROUP BY LABEL")
|
@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>>
|
fun countByLabel(): Flow<Map<@MapColumn("LABEL") String?, @MapColumn("COUNT") Int>>
|
||||||
|
|
||||||
|
@Suppress("ktlint:standard:annotation")
|
||||||
|
@Query("SELECT ARTIST, COUNT(*) AS COUNT FROM DOWNLOADS LEFT JOIN DOWNLOAD_ARTISTS USING(GID) GROUP BY ARTIST ORDER BY COUNT DESC, ARTIST")
|
||||||
|
fun countByArtist(): Flow<Map<@MapColumn("ARTIST") String?, @MapColumn("COUNT") Int>>
|
||||||
|
|
||||||
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME")
|
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME")
|
||||||
suspend fun list(): List<DownloadEntity>
|
suspend fun list(): List<DownloadEntity>
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ interface SearchDao {
|
|||||||
@Query("DELETE FROM suggestions WHERE `query` = :query")
|
@Query("DELETE FROM suggestions WHERE `query` = :query")
|
||||||
suspend fun deleteQuery(query: String)
|
suspend fun deleteQuery(query: String)
|
||||||
|
|
||||||
@Query("SELECT * FROM suggestions WHERE `query` LIKE :prefix || '%' ORDER BY date DESC LIMIT :limit")
|
@Query("SELECT DISTINCT `query` FROM suggestions WHERE `query` LIKE :prefix || '%' ORDER BY date DESC LIMIT :limit")
|
||||||
suspend fun rawSuggestions(prefix: String, limit: Int): Array<Search>
|
suspend fun rawSuggestions(prefix: String, limit: Int): List<String>
|
||||||
|
|
||||||
@Query("SELECT * FROM suggestions ORDER BY date DESC LIMIT :limit")
|
@Query("SELECT DISTINCT `query` FROM suggestions ORDER BY date DESC LIMIT :limit")
|
||||||
suspend fun list(limit: Int): Array<Search>
|
suspend fun list(limit: Int): List<String>
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insert(search: Search)
|
suspend fun insert(search: Search)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import com.hippo.ehviewer.EhDB
|
|||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
|
import com.hippo.ehviewer.dao.DownloadArtist
|
||||||
import com.hippo.ehviewer.dao.DownloadInfo
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
import com.hippo.ehviewer.dao.DownloadLabel
|
import com.hippo.ehviewer.dao.DownloadLabel
|
||||||
import com.hippo.ehviewer.image.Image
|
import com.hippo.ehviewer.image.Image
|
||||||
@@ -174,7 +175,6 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
info = DownloadInfo(galleryInfo, galleryInfo.putToDownloadDir())
|
info = DownloadInfo(galleryInfo, galleryInfo.putToDownloadDir())
|
||||||
info.label = label
|
info.label = label
|
||||||
info.state = DownloadInfo.STATE_WAIT
|
info.state = DownloadInfo.STATE_WAIT
|
||||||
|
|
||||||
// Add to all download list and map
|
// Add to all download list and map
|
||||||
allInfoList.insertWith(info, comparator())
|
allInfoList.insertWith(info, comparator())
|
||||||
mAllInfoMap[galleryInfo.gid] = info
|
mAllInfoMap[galleryInfo.gid] = info
|
||||||
@@ -266,6 +266,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
|
|
||||||
// Save to
|
// Save to
|
||||||
EhDB.putDownloadInfo(info)
|
EhDB.putDownloadInfo(info)
|
||||||
|
EhDB.putDownloadArtist(info.gid, info.artistInfoList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,26 +531,63 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun readMetadataFromLocal() {
|
suspend fun readMetadataFromLocal() {
|
||||||
val list = allInfoList.filter { it.pages == 0 || it.simpleTags == null }.parMapNotNull(concurrency = 5) { info ->
|
val list = allInfoList.mapNotNull {
|
||||||
|
val updateGallery = it.pages == 0 || it.simpleTags == null
|
||||||
|
val updateArtist = it.artistInfoList.isEmpty()
|
||||||
|
if (updateGallery || updateArtist) {
|
||||||
|
Triple(it, updateGallery, updateArtist)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.parMapNotNull(concurrency = 5) { (info, updateGallery, updateArtist) ->
|
||||||
info.downloadDir?.run {
|
info.downloadDir?.run {
|
||||||
val comicInfo = findFile(COMIC_INFO_FILE)?.let { readComicInfo(it) }
|
val comicInfo = findFile(COMIC_INFO_FILE)?.let { readComicInfo(it) }
|
||||||
if (comicInfo != null) {
|
if (comicInfo != null) {
|
||||||
info.pages = comicInfo.pageCount
|
val galleryInfo = if (updateGallery) {
|
||||||
info.simpleTags = comicInfo.toSimpleTags()
|
info.pages = comicInfo.pageCount
|
||||||
info.galleryInfo
|
info.simpleTags = comicInfo.toSimpleTags()
|
||||||
|
info.galleryInfo
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val artistList = if (updateArtist && comicInfo.penciller != null) {
|
||||||
|
info.artistInfoList = DownloadArtist.from(info.gid, comicInfo.penciller)
|
||||||
|
info.gid to info.artistInfoList
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (galleryInfo != null || artistList != null) {
|
||||||
|
galleryInfo to artistList
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
} else if (info.pages == 0) {
|
} else if (info.pages == 0) {
|
||||||
findFile(SPIDER_INFO_FILENAME)?.let {
|
val galleryInfo = findFile(SPIDER_INFO_FILENAME)?.let {
|
||||||
readCompatFromUniFile(it)?.run {
|
readCompatFromUniFile(it)?.run {
|
||||||
info.pages = pages
|
info.pages = pages
|
||||||
info.galleryInfo
|
info.galleryInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
galleryInfo?.let { it to null }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EhDB.updateGalleryInfo(list)
|
|
||||||
|
val galleryInfoList = mutableListOf<BaseGalleryInfo>()
|
||||||
|
list.forEach { (info, artists) ->
|
||||||
|
info?.let {
|
||||||
|
galleryInfoList.add(it)
|
||||||
|
}
|
||||||
|
artists?.let { (gid, updateList) ->
|
||||||
|
EhDB.putDownloadArtist(gid, updateList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (galleryInfoList.isNotEmpty()) EhDB.updateGalleryInfo(galleryInfoList)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isIdle: Boolean
|
val isIdle: Boolean
|
||||||
@@ -797,5 +835,5 @@ var downloadLocation: UniFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation / it }
|
val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation / it }
|
||||||
val DownloadInfo.archiveFile get() = downloadDir?.findFile("$gid.cbz")
|
val DownloadInfo.archiveFile get() = downloadDir?.run { findFile("$gid.cbz") ?: findFile("$gid.zip") }
|
||||||
val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid")
|
val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid")
|
||||||
@@ -34,7 +34,6 @@ import com.hippo.ehviewer.client.EhUtils
|
|||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
import com.hippo.ehviewer.dao.DownloadInfo
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
import com.hippo.ehviewer.ui.MainActivity
|
import com.hippo.ehviewer.ui.MainActivity
|
||||||
import com.hippo.ehviewer.ui.screen.DownloadsFragment
|
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.ReadableTime
|
import com.hippo.ehviewer.util.ReadableTime
|
||||||
import com.hippo.ehviewer.util.SimpleHandler
|
import com.hippo.ehviewer.util.SimpleHandler
|
||||||
@@ -187,7 +186,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
clearIntent.action = ACTION_CLEAR
|
clearIntent.action = ACTION_CLEAR
|
||||||
val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
|
val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putString(DownloadsFragment.KEY_ACTION, DownloadsFragment.ACTION_CLEAR_DOWNLOAD_SERVICE)
|
bundle.putString(KEY_ACTION, ACTION_CLEAR_DOWNLOAD_SERVICE)
|
||||||
val activityIntent = Intent(this, MainActivity::class.java)
|
val activityIntent = Intent(this, MainActivity::class.java)
|
||||||
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
||||||
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
||||||
@@ -243,7 +242,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
}
|
}
|
||||||
ensureDownloadingBuilder()
|
ensureDownloadingBuilder()
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putLong(DownloadsFragment.KEY_GID, info.gid)
|
bundle.putLong(KEY_GID, info.gid)
|
||||||
val activityIntent = Intent(this, MainActivity::class.java)
|
val activityIntent = Intent(this, MainActivity::class.java)
|
||||||
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
||||||
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
||||||
@@ -540,6 +539,8 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
|||||||
const val KEY_LABEL = "label"
|
const val KEY_LABEL = "label"
|
||||||
const val KEY_GID = "gid"
|
const val KEY_GID = "gid"
|
||||||
const val KEY_GID_LIST = "gid_list"
|
const val KEY_GID_LIST = "gid_list"
|
||||||
|
const val KEY_ACTION = "action"
|
||||||
|
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
|
||||||
private const val ID_DOWNLOADING = 1
|
private const val ID_DOWNLOADING = 1
|
||||||
private const val ID_DOWNLOADED = 2
|
private const val ID_DOWNLOADED = 2
|
||||||
private const val ID_509 = 3
|
private const val ID_509 = 3
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.hippo.ehviewer.download
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
|
import com.hippo.ehviewer.dao.artists
|
||||||
|
|
||||||
|
enum class DownloadsFilterMode(
|
||||||
|
val flag: Int,
|
||||||
|
val take: (info: DownloadInfo, label: String?) -> Boolean,
|
||||||
|
) {
|
||||||
|
CUSTOM(flag = 0, take = { info, label ->
|
||||||
|
label == "" || info.label == label
|
||||||
|
}),
|
||||||
|
ARTIST(flag = 1, take = { info, label ->
|
||||||
|
if (null == label) {
|
||||||
|
info.artistInfoList.isEmpty()
|
||||||
|
} else {
|
||||||
|
label == "" || info.artists.contains(label)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val Default = ARTIST
|
||||||
|
fun from(flag: Int) = DownloadsFilterMode.entries[flag]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ class ArchivePageLoader(
|
|||||||
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
||||||
logcat(DEBUG_TAG) { "Open archive ${file.uri.displayPath}" }
|
logcat(DEBUG_TAG) { "Open archive ${file.uri.displayPath}" }
|
||||||
pfd = file.openFileDescriptor("r")
|
pfd = file.openFileDescriptor("r")
|
||||||
size = openArchive(pfd.fd, pfd.statSize, gid == 0L)
|
size = openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name!!.endsWith(".zip"))
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import com.hippo.ehviewer.spider.SpiderQueen
|
|||||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
|
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
|
||||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
|
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
|
||||||
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
|
||||||
class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
||||||
@@ -43,11 +42,11 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val title by lazy {
|
override val title by lazy {
|
||||||
FileUtils.sanitizeFilename(EhUtils.getSuitableTitle(mGalleryInfo))
|
EhUtils.getSuitableTitle(mGalleryInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getImageExtension(index: Int): String {
|
override fun getImageExtension(index: Int): String? {
|
||||||
return mSpiderQueen.getExtension(index)!!
|
return mSpiderQueen.getExtension(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun save(index: Int, file: UniFile): Boolean {
|
override fun save(index: Int, file: UniFile): Boolean {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package com.hippo.ehviewer.gallery
|
|||||||
|
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -59,16 +60,15 @@ abstract class PageLoader2(private val gid: Long, var startPage: Int) : PageLoad
|
|||||||
|
|
||||||
protected abstract val title: String
|
protected abstract val title: String
|
||||||
|
|
||||||
protected abstract fun getImageExtension(index: Int): String
|
protected abstract fun getImageExtension(index: Int): String?
|
||||||
|
|
||||||
fun getImageFilename(index: Int): String {
|
fun getImageFilename(index: Int): String? = getImageExtension(index)?.let {
|
||||||
return "$title - ${index + 1}.${getImageExtension(index)}"
|
FileUtils.sanitizeFilename("$title - ${index + 1}.${it.lowercase()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun save(index: Int, file: UniFile): Boolean
|
abstract fun save(index: Int, file: UniFile): Boolean
|
||||||
|
|
||||||
fun saveToDir(index: Int, dir: UniFile): UniFile? {
|
fun saveToDir(index: Int, dir: UniFile): UniFile? = getImageFilename(index)?.let { filename ->
|
||||||
val filename = getImageFilename(index)
|
(dir / filename).takeIf { save(index, it) }
|
||||||
return (dir / filename).takeIf { save(index, it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ fun GalleryInfo.getComicInfo(): ComicInfo {
|
|||||||
}
|
}
|
||||||
return ComicInfo(
|
return ComicInfo(
|
||||||
series = title,
|
series = title,
|
||||||
alternateSeries = titleJpn.takeUnless { it.isNullOrBlank() },
|
alternateSeries = titleJpn?.ifBlank { null },
|
||||||
writer = groups.ifEmpty { null },
|
writer = groups.ifEmpty { null },
|
||||||
penciller = artists.ifEmpty { null },
|
penciller = artists.ifEmpty { null },
|
||||||
genre = otherTags.ifEmpty { null },
|
genre = otherTags.ifEmpty { null },
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import com.hippo.ehviewer.client.executeSafely
|
|||||||
import com.hippo.ehviewer.client.getImageKey
|
import com.hippo.ehviewer.client.getImageKey
|
||||||
import com.hippo.ehviewer.coil.read
|
import com.hippo.ehviewer.coil.read
|
||||||
import com.hippo.ehviewer.coil.suspendEdit
|
import com.hippo.ehviewer.coil.suspendEdit
|
||||||
|
import com.hippo.ehviewer.dao.DownloadArtist
|
||||||
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
import com.hippo.ehviewer.download.downloadLocation
|
import com.hippo.ehviewer.download.downloadLocation
|
||||||
import com.hippo.ehviewer.download.tempDownloadDir
|
import com.hippo.ehviewer.download.tempDownloadDir
|
||||||
import com.hippo.ehviewer.image.UniFileSource
|
import com.hippo.ehviewer.image.UniFileSource
|
||||||
@@ -41,7 +43,7 @@ import com.hippo.ehviewer.util.sendTo
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.unifile.asUniFile
|
||||||
import com.hippo.unifile.openOutputStream
|
import com.hippo.unifile.openOutputStream
|
||||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
import com.hippo.unifile.sha1
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import io.ktor.client.plugins.onDownload
|
import io.ktor.client.plugins.onDownload
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
@@ -194,6 +196,9 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
outFile.openOutputStream().use {
|
outFile.openOutputStream().use {
|
||||||
response.bodyAsChannel().copyTo(it.channel)
|
response.bodyAsChannel().copyTo(it.channel)
|
||||||
}
|
}
|
||||||
|
val expected = FileHashRegex.find(url)?.run { groupValues[0] }
|
||||||
|
val actual = outFile.sha1()
|
||||||
|
check(expected == actual) { "File hash mismatch: expected $expected, but got $actual\nURL: $url" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +279,7 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
val archived = saveAsCbz && dir?.findFile(archiveName) != null
|
val archived = saveAsCbz && dir?.findFile(archiveName) != null
|
||||||
if (archived) {
|
if (archived) {
|
||||||
dir.listFiles().parMap(concurrency = 10) {
|
dir.listFiles().parMap(concurrency = 10) {
|
||||||
if (it.name?.matches(filenamePattern) == true) {
|
if (it.name?.matches(FileNameRegex) == true) {
|
||||||
it.delete()
|
it.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,11 +326,15 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
createFile(COMIC_INFO_FILE)?.also {
|
createFile(COMIC_INFO_FILE)?.also {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (info !is GalleryDetail && fetchMetadata) {
|
if (info !is GalleryDetail && fetchMetadata) {
|
||||||
withNonCancellableContext {
|
EhEngine.fillGalleryListByApi(listOf(info))
|
||||||
EhEngine.fillGalleryListByApi(listOf(info))
|
}
|
||||||
|
info.getComicInfo().apply {
|
||||||
|
write(it)
|
||||||
|
DownloadManager.getDownloadInfo(gid)?.let { downloadInfo ->
|
||||||
|
downloadInfo.artistInfoList = DownloadArtist.from(gid, penciller.orEmpty())
|
||||||
|
EhDB.putDownloadArtist(gid, downloadInfo.artistInfoList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info.getComicInfo().write(it)
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
logcat(it)
|
logcat(it)
|
||||||
}
|
}
|
||||||
@@ -334,7 +343,8 @@ class SpiderDen(val info: GalleryInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val filenamePattern = Regex("^\\d{8}\\.\\w{3,4}")
|
private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
|
||||||
|
private val FileHashRegex = Regex("[0-9a-f]{40}")
|
||||||
|
|
||||||
fun perFilename(index: Int, extension: String = ""): String {
|
fun perFilename(index: Int, extension: String = ""): String {
|
||||||
return "%08d.%s".format(index + 1, extension)
|
return "%08d.%s".format(index + 1, extension)
|
||||||
|
|||||||
@@ -244,12 +244,15 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
if (archiveJob.isActive) {
|
if (archiveJob.isActive) {
|
||||||
archiveJob.join()
|
archiveJob.join()
|
||||||
}
|
}
|
||||||
runCatching {
|
if (!mSpiderDen.postArchive()) {
|
||||||
if (!mSpiderDen.postArchive()) {
|
if (mWorkerScope.isDownloadMode) {
|
||||||
|
mSpiderDen.writeComicInfo()
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
writeSpiderInfoToLocal()
|
writeSpiderInfoToLocal()
|
||||||
|
}.onFailure {
|
||||||
|
logcat(it)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
|
||||||
logcat(it)
|
|
||||||
}
|
}
|
||||||
queenScope.cancel()
|
queenScope.cancel()
|
||||||
}
|
}
|
||||||
@@ -483,13 +486,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
val gid = galleryInfo.gid
|
val gid = galleryInfo.gid
|
||||||
return (sQueenMap[gid] ?: SpiderQueen(galleryInfo).also { sQueenMap[gid] = it }).apply {
|
return (sQueenMap[gid] ?: SpiderQueen(galleryInfo).also { sQueenMap[gid] = it }).apply {
|
||||||
setMode(mode)
|
setMode(mode)
|
||||||
launch {
|
launch { updateMode() }
|
||||||
// Will create download dir if not exists
|
|
||||||
updateMode()
|
|
||||||
if (mode == MODE_DOWNLOAD) {
|
|
||||||
mSpiderDen.writeComicInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +513,8 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
private val downloadTimeout = Settings.downloadTimeout.seconds
|
private val downloadTimeout = Settings.downloadTimeout.seconds
|
||||||
private val delayLock = Mutex()
|
private val delayLock = Mutex()
|
||||||
private var lastRequestTime = TimeSource.Monotonic.markNow()
|
private var lastRequestTime = TimeSource.Monotonic.markNow()
|
||||||
private var isDownloadMode = false
|
var isDownloadMode = false
|
||||||
|
private set
|
||||||
|
|
||||||
fun cancelDecode(index: Int) {
|
fun cancelDecode(index: Int) {
|
||||||
decoder.cancel(index)
|
decoder.cancel(index)
|
||||||
@@ -648,15 +646,15 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
|||||||
pToken,
|
pToken,
|
||||||
localShowKey,
|
localShowKey,
|
||||||
previousPToken,
|
previousPToken,
|
||||||
).let {
|
)
|
||||||
check509(it.imageUrl)
|
}.getOrElse {
|
||||||
imageUrl = it.imageUrl
|
|
||||||
skipHathKey = it.skipHathKey
|
|
||||||
originImageUrl = it.originImageUrl
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
forceHtml = true
|
forceHtml = true
|
||||||
return@repeat
|
return@repeat
|
||||||
|
}.let {
|
||||||
|
check509(it.imageUrl)
|
||||||
|
imageUrl = it.imageUrl
|
||||||
|
skipHathKey = it.skipHathKey
|
||||||
|
originImageUrl = it.originImageUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
@@ -161,7 +160,7 @@ suspend fun DialogState.startDownload(
|
|||||||
add(it.label)
|
add(it.label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val (selected, checked) = showSelectItemWithCheckBox(
|
val (selected, checked) = awaitSelectItemWithCheckBox(
|
||||||
items,
|
items,
|
||||||
title = R.string.download,
|
title = R.string.download,
|
||||||
checkBoxText = R.string.remember_download_label,
|
checkBoxText = R.string.remember_download_label,
|
||||||
@@ -190,7 +189,7 @@ suspend fun DialogState.startDownload(
|
|||||||
|
|
||||||
suspend fun DialogState.modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
|
suspend fun DialogState.modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
|
||||||
val localFavorited = EhDB.containLocalFavorites(galleryInfo.gid)
|
val localFavorited = EhDB.containLocalFavorites(galleryInfo.gid)
|
||||||
if (EhCookieStore.hasSignedIn()) {
|
if (Settings.hasSignedIn.value) {
|
||||||
val isFavorited = galleryInfo.favoriteSlot != NOT_FAVORITED
|
val isFavorited = galleryInfo.favoriteSlot != NOT_FAVORITED
|
||||||
val defaultFavSlot = Settings.defaultFavSlot
|
val defaultFavSlot = Settings.defaultFavSlot
|
||||||
if (defaultFavSlot == -2) {
|
if (defaultFavSlot == -2) {
|
||||||
@@ -206,7 +205,7 @@ suspend fun DialogState.modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
|
|||||||
add(localFav)
|
add(localFav)
|
||||||
addAll(cloudFav)
|
addAll(cloudFav)
|
||||||
}
|
}
|
||||||
val (slot, note) = showSelectItemWithIconAndTextField(
|
val (slot, note) = awaitSelectItemWithIconAndTextField(
|
||||||
items,
|
items,
|
||||||
title = R.string.add_favorites_dialog_title,
|
title = R.string.add_favorites_dialog_title,
|
||||||
hint = R.string.favorite_note,
|
hint = R.string.favorite_note,
|
||||||
@@ -311,7 +310,7 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
|
|||||||
add(Icons.AutoMirrored.Default.DriveFileMove to R.string.download_move_dialog_title)
|
add(Icons.AutoMirrored.Default.DriveFileMove to R.string.download_move_dialog_title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val selected = showSelectItemWithIcon(items, EhUtils.getSuitableTitle(info))
|
val selected = awaitSelectItemWithIcon(items, EhUtils.getSuitableTitle(info))
|
||||||
with(findActivity<MainActivity>()) {
|
with(findActivity<MainActivity>()) {
|
||||||
when (selected) {
|
when (selected) {
|
||||||
0 -> {
|
0 -> {
|
||||||
@@ -410,7 +409,7 @@ suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) {
|
|||||||
add(it.label)
|
add(it.label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val selected = showSelectItem(labels, R.string.download_move_dialog_title)
|
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
|
||||||
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
|
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
|
||||||
val label = if (selected == 0) null else labels[selected]
|
val label = if (selected == 0) null else labels[selected]
|
||||||
DownloadManager.changeLabel(listOf(downloadInfo), label)
|
DownloadManager.changeLabel(listOf(downloadInfo), label)
|
||||||
@@ -424,19 +423,19 @@ suspend fun DialogState.showMoveDownloadLabelList(list: Collection<DownloadInfo>
|
|||||||
add(it.label)
|
add(it.label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val selected = showSelectItem(labels, R.string.download_move_dialog_title)
|
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
|
||||||
val label = if (selected == 0) null else labels[selected]
|
val label = if (selected == 0) null else labels[selected]
|
||||||
DownloadManager.changeLabel(list, label)
|
DownloadManager.changeLabel(list, label)
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun DialogState.showDatePicker(): String? {
|
suspend fun DialogState.awaitSelectDate(): String? {
|
||||||
val initial = LocalDate(2007, 3, 21)
|
val initial = LocalDate(2007, 3, 21)
|
||||||
val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY)
|
val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY)
|
||||||
val initialMillis = initial.toEpochMillis()
|
val initialMillis = initial.toEpochMillis()
|
||||||
val yesterdayMillis = yesterday.toEpochMillis()
|
val yesterdayMillis = yesterday.toEpochMillis()
|
||||||
val dateRange = initialMillis..yesterdayMillis
|
val dateRange = initialMillis..yesterdayMillis
|
||||||
val dateMillis = showDatePicker(
|
val dateMillis = awaitSelectDate(
|
||||||
title = R.string.go_to,
|
title = R.string.go_to,
|
||||||
yearRange = initial.year..yesterday.year,
|
yearRange = initial.year..yesterday.year,
|
||||||
selectableDates = object : SelectableDates {
|
selectableDates = object : SelectableDates {
|
||||||
|
|||||||
@@ -16,16 +16,26 @@
|
|||||||
package com.hippo.ehviewer.ui
|
package com.hippo.ehviewer.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.navigation.ActivityNavigator
|
import androidx.navigation.ActivityNavigator
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastR
|
||||||
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
||||||
|
|
||||||
abstract class EhActivity : AppCompatActivity() {
|
abstract class EhActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
// https://issuetracker.google.com/204791558
|
||||||
|
// Fix system bars insets still exist in fullscreen mode on API < 30
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (!isAtLeastR) {
|
||||||
|
with(window.decorView) {
|
||||||
|
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import androidx.compose.material.icons.filled.History
|
|||||||
import androidx.compose.material.icons.filled.Home
|
import androidx.compose.material.icons.filled.Home
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Whatshot
|
import androidx.compose.material.icons.filled.Whatshot
|
||||||
|
import androidx.compose.material3.DrawerDefaults
|
||||||
import androidx.compose.material3.DrawerState
|
import androidx.compose.material3.DrawerState
|
||||||
import androidx.compose.material3.DrawerState2
|
import androidx.compose.material3.DrawerState2
|
||||||
import androidx.compose.material3.DrawerValue
|
import androidx.compose.material3.DrawerValue
|
||||||
@@ -64,9 +65,9 @@ import androidx.compose.material3.SnackbarHost
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.material3.rememberDrawerState2
|
import androidx.compose.material3.rememberDrawerState2
|
||||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -76,6 +77,7 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.currentCompositeKeyHash
|
import androidx.compose.runtime.currentCompositeKeyHash
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -92,9 +94,9 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
@@ -127,12 +129,10 @@ import com.hippo.ehviewer.ui.destinations.WhatshotScreenDestination
|
|||||||
import com.hippo.ehviewer.ui.screen.asDst
|
import com.hippo.ehviewer.ui.screen.asDst
|
||||||
import com.hippo.ehviewer.ui.screen.asDstWith
|
import com.hippo.ehviewer.ui.screen.asDstWith
|
||||||
import com.hippo.ehviewer.ui.screen.navWithUrl
|
import com.hippo.ehviewer.ui.screen.navWithUrl
|
||||||
import com.hippo.ehviewer.ui.screen.navigateTo
|
|
||||||
import com.hippo.ehviewer.ui.settings.showNewVersion
|
import com.hippo.ehviewer.ui.settings.showNewVersion
|
||||||
import com.hippo.ehviewer.ui.tools.DialogState
|
import com.hippo.ehviewer.ui.tools.DialogState
|
||||||
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
|
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
|
||||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||||
import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider
|
|
||||||
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
||||||
import com.hippo.ehviewer.updater.AppUpdater
|
import com.hippo.ehviewer.updater.AppUpdater
|
||||||
import com.hippo.ehviewer.util.addTextToClipboard
|
import com.hippo.ehviewer.util.addTextToClipboard
|
||||||
@@ -147,6 +147,7 @@ import com.hippo.unifile.sha1
|
|||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||||
import com.ramcosta.composedestinations.utils.currentDestinationAsState
|
import com.ramcosta.composedestinations.utils.currentDestinationAsState
|
||||||
|
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@@ -218,6 +219,7 @@ class MainActivity : EhActivity() {
|
|||||||
val snackbarState = remember { SnackbarHostState() }
|
val snackbarState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
val navigator = navController.rememberDestinationsNavigator()
|
||||||
fun closeDrawer(callback: () -> Unit = {}) = scope.launch {
|
fun closeDrawer(callback: () -> Unit = {}) = scope.launch {
|
||||||
navDrawerState.close()
|
navDrawerState.close()
|
||||||
callback()
|
callback()
|
||||||
@@ -236,7 +238,7 @@ class MainActivity : EhActivity() {
|
|||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
navController.navigateTo(DownloadScreenDestination)
|
navigator.navigate(DownloadScreenDestination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +262,7 @@ class MainActivity : EhActivity() {
|
|||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_VIEW -> {
|
Intent.ACTION_VIEW -> {
|
||||||
val url = intent.data?.toString()
|
val url = intent.data?.toString()
|
||||||
if (url != null && !navController.navWithUrl(url)) {
|
if (url != null && !navigator.navWithUrl(url)) {
|
||||||
val new = dialogState.awaitInputText(initial = url, title = cannotParse)
|
val new = dialogState.awaitInputText(initial = url, title = cannotParse)
|
||||||
addTextToClipboard(new)
|
addTextToClipboard(new)
|
||||||
}
|
}
|
||||||
@@ -269,14 +271,14 @@ class MainActivity : EhActivity() {
|
|||||||
val type = intent.type
|
val type = intent.type
|
||||||
if ("text/plain" == type) {
|
if ("text/plain" == type) {
|
||||||
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
|
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
if (keyword != null && !navController.navWithUrl(keyword)) {
|
if (keyword != null && !navigator.navWithUrl(keyword)) {
|
||||||
navController.navigateTo(ListUrlBuilder(mKeyword = keyword).asDst())
|
navigator.navigate(ListUrlBuilder(mKeyword = keyword).asDst())
|
||||||
}
|
}
|
||||||
} else if (type != null && type.startsWith("image/")) {
|
} else if (type != null && type.startsWith("image/")) {
|
||||||
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
|
||||||
if (null != uri) {
|
if (null != uri) {
|
||||||
val hash = withIOContext { uri.asUniFile().sha1() }
|
val hash = withIOContext { uri.asUniFile().sha1() }
|
||||||
navController.navigateTo(
|
navigator.navigate(
|
||||||
ListUrlBuilder(
|
ListUrlBuilder(
|
||||||
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
|
mode = ListUrlBuilder.MODE_IMAGE_SEARCH,
|
||||||
hash = hash,
|
hash = hash,
|
||||||
@@ -286,8 +288,11 @@ class MainActivity : EhActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DownloadService.ACTION_START_DOWNLOADSCENE -> {
|
DownloadService.ACTION_START_DOWNLOADSCENE -> {
|
||||||
val args = intent.getBundleExtra(DownloadService.ACTION_START_DOWNLOADSCENE_ARGS)
|
val args = intent.getBundleExtra(DownloadService.ACTION_START_DOWNLOADSCENE_ARGS)!!
|
||||||
navController.navigateTo(DownloadsScreenDestination)
|
if (args.getString(DownloadService.KEY_ACTION) == DownloadService.ACTION_CLEAR_DOWNLOAD_SERVICE) {
|
||||||
|
DownloadService.clear()
|
||||||
|
}
|
||||||
|
navigator.navigate(DownloadsScreenDestination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,11 +332,11 @@ class MainActivity : EhActivity() {
|
|||||||
val result1 = GalleryDetailUrlParser.parse(text, false)
|
val result1 = GalleryDetailUrlParser.parse(text, false)
|
||||||
var launch: (() -> Unit)? = null
|
var launch: (() -> Unit)? = null
|
||||||
if (result1 != null) {
|
if (result1 != null) {
|
||||||
launch = { navController.navigateTo(result1.gid asDstWith result1.token) }
|
launch = { navigator.navigate(result1.gid asDstWith result1.token) }
|
||||||
}
|
}
|
||||||
val result2 = GalleryPageUrlParser.parse(text, false)
|
val result2 = GalleryPageUrlParser.parse(text, false)
|
||||||
if (result2 != null) {
|
if (result2 != null) {
|
||||||
launch = { navController.navigateTo(ProgressScreenDestination(result2.gid, result2.pToken, result2.page)) }
|
launch = { navigator.navigate(ProgressScreenDestination(result2.gid, result2.pToken, result2.page)) }
|
||||||
}
|
}
|
||||||
launch?.let {
|
launch?.let {
|
||||||
val ret = snackbarState.showSnackbar(snackMessage, snackAction, true)
|
val ret = snackbarState.showSnackbar(snackMessage, snackAction, true)
|
||||||
@@ -343,17 +348,18 @@ class MainActivity : EhActivity() {
|
|||||||
onPauseOrDispose { job.cancel() }
|
onPauseOrDispose { job.cancel() }
|
||||||
}
|
}
|
||||||
val currentDestination by navController.currentDestinationAsState()
|
val currentDestination by navController.currentDestinationAsState()
|
||||||
val lockDrawerHandle = remember { mutableStateListOf<Int>() }
|
val drawerHandle = remember { mutableStateListOf<Int>() }
|
||||||
var snackbarFabPadding by remember { mutableStateOf(0.dp) }
|
var snackbarFabPadding by remember { mutableStateOf(0.dp) }
|
||||||
val drawerLocked = lockDrawerHandle.isNotEmpty()
|
val drawerEnabled = drawerHandle.isNotEmpty()
|
||||||
val viewConfiguration = LocalViewConfiguration.current
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
val adaptiveInfo = currentWindowAdaptiveInfo()
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalNavDrawerState provides navDrawerState,
|
LocalNavDrawerState provides navDrawerState,
|
||||||
LocalSideSheetState provides sideSheetState,
|
LocalSideSheetState provides sideSheetState,
|
||||||
LocalDrawerLockHandle provides lockDrawerHandle,
|
LocalDrawerHandle provides drawerHandle,
|
||||||
LocalSnackBarHostState provides snackbarState,
|
LocalSnackBarHostState provides snackbarState,
|
||||||
LocalSnackBarFabPadding provides animateDpAsState(snackbarFabPadding, label = "SnackbarFabPadding"),
|
LocalSnackBarFabPadding provides animateDpAsState(snackbarFabPadding, label = "SnackbarFabPadding"),
|
||||||
|
LocalWindowSizeClass provides adaptiveInfo.windowSizeClass,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
@@ -367,92 +373,85 @@ class MainActivity : EhActivity() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
LocalTouchSlopProvider(Settings.touchSlopFactor.toFloat()) {
|
var minOffset by remember {
|
||||||
val minOffset = -with(density) { 360.dp.toPx() }
|
mutableFloatStateOf(-with(density) { DrawerDefaults.MaximumDrawerWidth.toPx() })
|
||||||
ModalNavigationDrawer(
|
}
|
||||||
drawerContent = {
|
ModalNavigationDrawer(
|
||||||
ModalDrawerSheet(
|
drawerContent = {
|
||||||
drawerState = navDrawerState,
|
ModalDrawerSheet(
|
||||||
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 56).dp),
|
drawerState = navDrawerState,
|
||||||
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Start),
|
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 56).dp)
|
||||||
|
.onSizeChanged { minOffset = -it.width.toFloat() },
|
||||||
|
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Start),
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(scrollState)
|
||||||
|
.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)),
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
Image(
|
||||||
Column(
|
painter = painterResource(id = R.drawable.sadpanda_low_poly),
|
||||||
modifier = Modifier.verticalScroll(scrollState)
|
contentDescription = null,
|
||||||
.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)),
|
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||||
) {
|
contentScale = ContentScale.FillWidth,
|
||||||
Image(
|
)
|
||||||
painter = painterResource(id = R.drawable.sadpanda_low_poly),
|
navItems.forEach { (direction, stringId, icon) ->
|
||||||
contentDescription = null,
|
NavigationDrawerItem(
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
label = {
|
||||||
contentScale = ContentScale.FillWidth,
|
Text(text = stringResource(id = stringId))
|
||||||
|
},
|
||||||
|
selected = currentDestination === direction,
|
||||||
|
onClick = {
|
||||||
|
navigator.navigate(direction)
|
||||||
|
closeDrawer()
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
|
icon = {
|
||||||
|
Icon(imageVector = icon, contentDescription = null)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
navItems.forEach { (direction, stringId, icon) ->
|
}
|
||||||
NavigationDrawerItem(
|
}
|
||||||
label = {
|
}
|
||||||
Text(text = stringResource(id = stringId))
|
},
|
||||||
},
|
drawerState = navDrawerState,
|
||||||
selected = currentDestination === direction,
|
gesturesEnabled = drawerEnabled && sideSheetState.isClosed || navDrawerState.isOpen,
|
||||||
onClick = {
|
) {
|
||||||
navController.navigate(direction.route)
|
val sheet = sideSheet.firstOrNull()
|
||||||
closeDrawer()
|
val radius by remember {
|
||||||
},
|
snapshotFlow {
|
||||||
modifier = Modifier.padding(horizontal = 12.dp),
|
val step = calculateFraction(minOffset, 0f, navDrawerState.currentOffset)
|
||||||
icon = {
|
with(density) { lerp(0, 10, step).dp.toPx() }
|
||||||
Icon(imageVector = icon, contentDescription = null)
|
}
|
||||||
},
|
}.collectAsState(0f)
|
||||||
)
|
ModalSideDrawer(
|
||||||
}
|
drawerContent = {
|
||||||
|
if (sheet != null) {
|
||||||
|
ModalDrawerSheet(
|
||||||
|
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 112).dp),
|
||||||
|
drawerShape = ShapeDefaults.Large.copy(topEnd = CornerSize(0), bottomEnd = CornerSize(0)),
|
||||||
|
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.End),
|
||||||
|
) {
|
||||||
|
sheet(sideSheetState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
drawerState = navDrawerState,
|
modifier = Modifier.graphicsLayer {
|
||||||
gesturesEnabled = !drawerLocked || navDrawerState.isOpen,
|
if (radius != 0f) {
|
||||||
|
renderEffect = BlurEffect(radius, radius, TileMode.Clamp)
|
||||||
|
shape = RectangleShape
|
||||||
|
clip = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawerState = sideSheetState,
|
||||||
|
gesturesEnabled = sheet != null && drawerEnabled,
|
||||||
) {
|
) {
|
||||||
val sheet = sideSheet.firstOrNull()
|
DestinationsNavHost(
|
||||||
val radius by remember {
|
navGraph = NavGraphs.root,
|
||||||
snapshotFlow {
|
startRoute = if (Settings.needSignIn) SignInScreenDestination else StartDestination,
|
||||||
val step = calculateFraction(minOffset, 0f, navDrawerState.currentOffset)
|
defaultTransitions = rememberEhNavAnim(),
|
||||||
with(density) { lerp(0, 10, step).dp.toPx() }
|
navController = navController,
|
||||||
}
|
)
|
||||||
}.collectAsState(0f)
|
|
||||||
ModalSideDrawer(
|
|
||||||
drawerContent = {
|
|
||||||
if (sheet != null) {
|
|
||||||
ModalDrawerSheet(
|
|
||||||
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 112).dp),
|
|
||||||
drawerShape = ShapeDefaults.Large.copy(topEnd = CornerSize(0), bottomEnd = CornerSize(0)),
|
|
||||||
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Vertical + WindowInsetsSides.End),
|
|
||||||
) {
|
|
||||||
CompositionLocalProvider(LocalViewConfiguration provides viewConfiguration) {
|
|
||||||
sheet(sideSheetState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.graphicsLayer {
|
|
||||||
if (radius != 0f) {
|
|
||||||
renderEffect = BlurEffect(radius, radius, TileMode.Clamp)
|
|
||||||
shape = RectangleShape
|
|
||||||
clip = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
drawerState = sideSheetState,
|
|
||||||
gesturesEnabled = sheet != null && !drawerLocked,
|
|
||||||
) {
|
|
||||||
val windowSizeClass = calculateWindowSizeClass(this)
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalViewConfiguration provides viewConfiguration,
|
|
||||||
LocalWindowSizeClass provides windowSizeClass,
|
|
||||||
) {
|
|
||||||
DestinationsNavHost(
|
|
||||||
navGraph = NavGraphs.root,
|
|
||||||
startRoute = if (Settings.needSignIn) SignInScreenDestination else StartDestination,
|
|
||||||
defaultTransitions = rememberEhNavAnim(),
|
|
||||||
navController = navController,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,16 +530,15 @@ class MainActivity : EhActivity() {
|
|||||||
|
|
||||||
val LocalNavDrawerState = compositionLocalOf<DrawerState> { error("CompositionLocal LocalNavDrawerState not present!") }
|
val LocalNavDrawerState = compositionLocalOf<DrawerState> { error("CompositionLocal LocalNavDrawerState not present!") }
|
||||||
val LocalSideSheetState = compositionLocalOf<DrawerState2> { error("CompositionLocal LocalSideSheetState not present!") }
|
val LocalSideSheetState = compositionLocalOf<DrawerState2> { error("CompositionLocal LocalSideSheetState not present!") }
|
||||||
val LocalDrawerLockHandle = compositionLocalOf<SnapshotStateList<Int>> { error("CompositionLocal LocalDrawerLockHandle not present!") }
|
val LocalDrawerHandle = compositionLocalOf<SnapshotStateList<Int>> { error("CompositionLocal LocalDrawerHandle not present!") }
|
||||||
val LocalSnackBarHostState = compositionLocalOf<SnackbarHostState> { error("CompositionLocal LocalSnackBarHostState not present!") }
|
val LocalSnackBarHostState = compositionLocalOf<SnackbarHostState> { error("CompositionLocal LocalSnackBarHostState not present!") }
|
||||||
val LocalSnackBarFabPadding = compositionLocalOf<State<Dp>> { error("CompositionLocal LocalSnackBarFabPadding not present!") }
|
val LocalSnackBarFabPadding = compositionLocalOf<State<Dp>> { error("CompositionLocal LocalSnackBarFabPadding not present!") }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LockDrawer(value: Boolean) {
|
fun DrawerHandle(enabled: Boolean) {
|
||||||
val updated by rememberUpdatedState(value)
|
if (enabled) {
|
||||||
if (updated) {
|
|
||||||
val current = currentCompositeKeyHash
|
val current = currentCompositeKeyHash
|
||||||
val handle = LocalDrawerLockHandle.current
|
val handle = LocalDrawerHandle.current
|
||||||
DisposableEffect(current) {
|
DisposableEffect(current) {
|
||||||
handle.add(current)
|
handle.add(current)
|
||||||
onDispose {
|
onDispose {
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ suspend fun postLogin() = coroutineScope {
|
|||||||
launch {
|
launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
EhEngine.getProfile().run {
|
EhEngine.getProfile().run {
|
||||||
Settings.displayName = displayName
|
Settings.displayName.value = displayName
|
||||||
Settings.avatar = avatar
|
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
logcat(it)
|
logcat(it)
|
||||||
@@ -41,5 +40,6 @@ suspend fun postLogin() = coroutineScope {
|
|||||||
}.onFailure {
|
}.onFailure {
|
||||||
Settings.gallerySite = EhUrl.SITE_E
|
Settings.gallerySite = EhUrl.SITE_E
|
||||||
}
|
}
|
||||||
|
Settings.hasSignedIn.value = true
|
||||||
Settings.needSignIn = false
|
Settings.needSignIn = false
|
||||||
}
|
}
|
||||||
@@ -14,48 +14,176 @@ import androidx.compose.foundation.layout.systemBarsPadding
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.autofill.AutofillType
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.window.core.layout.WindowWidthSizeClass
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.ui.StartDestination
|
import com.hippo.ehviewer.ui.StartDestination
|
||||||
import com.hippo.ehviewer.ui.destinations.WebViewSignInScreenDestination
|
import com.hippo.ehviewer.ui.destinations.WebViewSignInScreenDestination
|
||||||
import com.hippo.ehviewer.ui.openBrowser
|
import com.hippo.ehviewer.ui.openBrowser
|
||||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||||
|
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||||
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
||||||
|
import com.hippo.ehviewer.ui.tools.autofill
|
||||||
|
import com.hippo.ehviewer.util.displayString
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
|
||||||
@Destination<RootGraph>(start = true)
|
@Destination<RootGraph>(start = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun SignInScreen(navigator: DestinationsNavigator) {
|
fun SignInScreen(navigator: DestinationsNavigator) {
|
||||||
LockDrawer(true)
|
|
||||||
val windowSizeClass = LocalWindowSizeClass.current
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
var isProgressIndicatorVisible by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showUsernameError by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showPasswordError by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var username by rememberSaveable { mutableStateOf("") }
|
||||||
|
var password by rememberSaveable { mutableStateOf("") }
|
||||||
|
var passwordHidden by rememberSaveable { mutableStateOf(true) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
var signInJob by remember { mutableStateOf<Job?>(null) }
|
||||||
|
val dialogState = LocalDialogState.current
|
||||||
|
|
||||||
|
fun signIn() {
|
||||||
|
if (signInJob?.isActive == true) return
|
||||||
|
if (username.isEmpty()) {
|
||||||
|
showUsernameError = true
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
showUsernameError = false
|
||||||
|
}
|
||||||
|
if (password.isEmpty()) {
|
||||||
|
showPasswordError = true
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
showPasswordError = false
|
||||||
|
}
|
||||||
|
focusManager.clearFocus()
|
||||||
|
isProgressIndicatorVisible = true
|
||||||
|
|
||||||
|
EhUtils.signOut()
|
||||||
|
signInJob = coroutineScope.launchIO {
|
||||||
|
runCatching {
|
||||||
|
EhEngine.signIn(username, password)
|
||||||
|
}.onFailure {
|
||||||
|
withUIContext {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
dialogState.awaitPermissionOrCancel(
|
||||||
|
confirmText = R.string.get_it,
|
||||||
|
title = R.string.sign_in_failed,
|
||||||
|
showCancelButton = false,
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
"""
|
||||||
|
${it.displayString()}
|
||||||
|
${stringResource(R.string.sign_in_failed_tip, stringResource(R.string.sign_in_via_webview))}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
isProgressIndicatorVisible = false
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
withNonCancellableContext { postLogin() }
|
||||||
|
withUIContext { navigator.popNavigate(StartDestination) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UsernameAndPasswordTextField() {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = username,
|
||||||
|
onValueChange = { username = it },
|
||||||
|
modifier = Modifier.width(dimensionResource(id = R.dimen.single_max_width)).autofill(
|
||||||
|
autofillTypes = listOf(AutofillType.Username),
|
||||||
|
onFill = { username = it },
|
||||||
|
),
|
||||||
|
label = { Text(stringResource(R.string.username)) },
|
||||||
|
supportingText = { if (showUsernameError) Text(stringResource(R.string.error_username_cannot_empty)) },
|
||||||
|
trailingIcon = { if (showUsernameError) Icon(imageVector = Icons.Filled.Info, contentDescription = null) },
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||||
|
singleLine = true,
|
||||||
|
isError = showUsernameError,
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it },
|
||||||
|
modifier = Modifier.width(dimensionResource(id = R.dimen.single_max_width)).autofill(
|
||||||
|
autofillTypes = listOf(AutofillType.Password),
|
||||||
|
onFill = { password = it },
|
||||||
|
),
|
||||||
|
label = { Text(stringResource(R.string.password)) },
|
||||||
|
visualTransformation = if (passwordHidden) PasswordVisualTransformation() else VisualTransformation.None,
|
||||||
|
supportingText = { if (showPasswordError) Text(stringResource(R.string.error_password_cannot_empty)) },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { signIn() }),
|
||||||
|
trailingIcon = {
|
||||||
|
if (showPasswordError) {
|
||||||
|
Icon(imageVector = Icons.Filled.Info, contentDescription = null)
|
||||||
|
} else {
|
||||||
|
IconButton(onClick = { passwordHidden = !passwordHidden }) {
|
||||||
|
val visibilityIcon = if (passwordHidden) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
|
||||||
|
Icon(imageVector = visibilityIcon, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
isError = showPasswordError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
when (windowSizeClass.widthSizeClass) {
|
when (windowSizeClass.windowWidthSizeClass) {
|
||||||
WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> {
|
WindowWidthSizeClass.COMPACT, WindowWidthSizeClass.MEDIUM -> {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = R.dimen.keyline_margin)),
|
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = R.dimen.keyline_margin)),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -65,6 +193,7 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin)),
|
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin)),
|
||||||
)
|
)
|
||||||
|
UsernameAndPasswordTextField()
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.app_waring),
|
text = stringResource(id = R.string.app_waring),
|
||||||
modifier = Modifier.widthIn(max = dimensionResource(id = R.dimen.single_max_width)).padding(top = 24.dp),
|
modifier = Modifier.widthIn(max = dimensionResource(id = R.dimen.single_max_width)).padding(top = 24.dp),
|
||||||
@@ -73,7 +202,7 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.app_waring_2),
|
text = stringResource(id = R.string.app_waring_2),
|
||||||
modifier = Modifier.widthIn(max = dimensionResource(id = R.dimen.single_max_width)).padding(top = 12.dp),
|
modifier = Modifier.widthIn(max = dimensionResource(id = R.dimen.single_max_width)).padding(top = 12.dp),
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.keyline_margin)))
|
Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.keyline_margin)))
|
||||||
Row(modifier = Modifier.padding(top = dimensionResource(R.dimen.keyline_margin))) {
|
Row(modifier = Modifier.padding(top = dimensionResource(R.dimen.keyline_margin))) {
|
||||||
@@ -84,32 +213,49 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
|||||||
Text(text = stringResource(id = R.string.register))
|
Text(text = stringResource(id = R.string.register))
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
|
onClick = ::signIn,
|
||||||
modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
|
modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = R.string.sign_in))
|
Text(text = stringResource(id = R.string.sign_in))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextButton(
|
Row(modifier = Modifier.padding(horizontal = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
onClick = {
|
TextButton(
|
||||||
Settings.needSignIn = false
|
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
|
||||||
Settings.gallerySite = EhUrl.SITE_E
|
modifier = Modifier.weight(1f),
|
||||||
navigator.popNavigate(StartDestination)
|
) {
|
||||||
},
|
Text(
|
||||||
) {
|
text = buildAnnotatedString {
|
||||||
Text(
|
withStyle(
|
||||||
text = buildAnnotatedString {
|
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||||
withStyle(
|
) {
|
||||||
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
append(stringResource(id = R.string.sign_in_via_webview))
|
||||||
) {
|
}
|
||||||
append(stringResource(id = R.string.guest_mode))
|
},
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
Settings.needSignIn = false
|
||||||
|
Settings.gallerySite = EhUrl.SITE_E
|
||||||
|
navigator.popNavigate(StartDestination)
|
||||||
},
|
},
|
||||||
)
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = buildAnnotatedString {
|
||||||
|
withStyle(
|
||||||
|
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||||
|
) {
|
||||||
|
append(stringResource(id = R.string.guest_mode))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowWidthSizeClass.Expanded -> {
|
WindowWidthSizeClass.EXPANDED -> {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = R.dimen.keyline_margin)),
|
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = R.dimen.keyline_margin)),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -132,17 +278,18 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.app_waring_2),
|
text = stringResource(id = R.string.app_waring_2),
|
||||||
modifier = Modifier.widthIn(max = 360.dp),
|
modifier = Modifier.widthIn(max = 360.dp),
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
UsernameAndPasswordTextField()
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Row(horizontalArrangement = Arrangement.Center) {
|
Row(horizontalArrangement = Arrangement.Center) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
|
onClick = ::signIn,
|
||||||
modifier = Modifier.padding(horizontal = 4.dp).width(128.dp),
|
modifier = Modifier.padding(horizontal = 4.dp).width(128.dp),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = R.string.sign_in))
|
Text(text = stringResource(id = R.string.sign_in))
|
||||||
@@ -155,27 +302,46 @@ fun SignInScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
TextButton(
|
Row(horizontalArrangement = Arrangement.Center) {
|
||||||
onClick = {
|
TextButton(
|
||||||
Settings.needSignIn = false
|
onClick = { navigator.navigate(WebViewSignInScreenDestination) },
|
||||||
Settings.gallerySite = EhUrl.SITE_E
|
modifier = Modifier.padding(horizontal = 4.dp).width(128.dp),
|
||||||
navigator.popNavigate(StartDestination)
|
) {
|
||||||
},
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
text = buildAnnotatedString {
|
||||||
) {
|
withStyle(
|
||||||
Text(
|
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||||
text = buildAnnotatedString {
|
) {
|
||||||
withStyle(
|
append(stringResource(id = R.string.sign_in_via_webview))
|
||||||
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
}
|
||||||
) {
|
},
|
||||||
append(stringResource(id = R.string.guest_mode))
|
)
|
||||||
}
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
Settings.needSignIn = false
|
||||||
|
Settings.gallerySite = EhUrl.SITE_E
|
||||||
|
navigator.popNavigate(StartDestination)
|
||||||
},
|
},
|
||||||
)
|
modifier = Modifier.padding(horizontal = 4.dp).width(128.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = buildAnnotatedString {
|
||||||
|
withStyle(
|
||||||
|
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||||
|
) {
|
||||||
|
append(stringResource(id = R.string.guest_mode))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isProgressIndicatorVisible) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,6 @@ import com.google.accompanist.web.rememberWebViewState
|
|||||||
import com.hippo.ehviewer.client.EhCookieStore
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.ui.StartDestination
|
import com.hippo.ehviewer.ui.StartDestination
|
||||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||||
import com.hippo.ehviewer.util.setDefaultSettings
|
import com.hippo.ehviewer.util.setDefaultSettings
|
||||||
@@ -28,7 +27,6 @@ import io.ktor.http.Url
|
|||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun WebViewSignInScreen(navigator: DestinationsNavigator) {
|
fun WebViewSignInScreen(navigator: DestinationsNavigator) {
|
||||||
LockDrawer(true)
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val state = rememberWebViewState(url = EhUrl.URL_SIGN_IN)
|
val state = rememberWebViewState(url = EhUrl.URL_SIGN_IN)
|
||||||
val client = remember {
|
val client = remember {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.hippo.ehviewer.ui.main
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
|
||||||
object AdvanceTable {
|
object AdvanceTable {
|
||||||
const val SH = 0x1
|
const val SH = 0x1
|
||||||
@@ -12,7 +14,9 @@ object AdvanceTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@Serializable
|
||||||
data class AdvancedSearchOption(
|
data class AdvancedSearchOption(
|
||||||
|
@Transient
|
||||||
val advanceSearch: Int = 0,
|
val advanceSearch: Int = 0,
|
||||||
val minRating: Int = 0,
|
val minRating: Int = 0,
|
||||||
val fromPage: Int = 0,
|
val fromPage: Int = 0,
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.hippo.ehviewer.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.hippo.ehviewer.R
|
||||||
|
import com.hippo.ehviewer.client.parser.Archive
|
||||||
|
import com.hippo.ehviewer.client.parser.Funds
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ArchiveList(
|
||||||
|
funds: Funds,
|
||||||
|
items: List<Archive>,
|
||||||
|
onItemClick: (Archive) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val currentFunds = stringResource(R.string.current_funds)
|
||||||
|
val archiveFree = stringResource(R.string.archive_free)
|
||||||
|
val archiveOriginal = stringResource(R.string.archive_original)
|
||||||
|
val archiveResample = stringResource(R.string.archive_resample)
|
||||||
|
val fundsStyle = MaterialTheme.typography.labelLarge
|
||||||
|
val (hAtH, nonHAtH) = remember(items) { items.partition { it.isHAtH } }
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
modifier = modifier,
|
||||||
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Text(text = currentFunds, textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
FundsItem(
|
||||||
|
type = "GP",
|
||||||
|
amount = funds.gp,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textStyle = fundsStyle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
FundsItem(
|
||||||
|
type = "C",
|
||||||
|
amount = funds.credit,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textStyle = fundsStyle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(nonHAtH) {
|
||||||
|
ArchiveItem(
|
||||||
|
name = if (it.res == "org") archiveOriginal else archiveResample,
|
||||||
|
cost = if (it.cost == "Free!") archiveFree else it.cost,
|
||||||
|
size = it.size,
|
||||||
|
onClick = { onItemClick(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
Text(text = "H@H")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(hAtH) {
|
||||||
|
ArchiveItem(
|
||||||
|
name = if (it.res == "org") archiveOriginal else it.name,
|
||||||
|
cost = if (it.cost == "Free") archiveFree else it.cost,
|
||||||
|
size = it.size,
|
||||||
|
onClick = { onItemClick(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ArchiveItem(
|
||||||
|
name: String,
|
||||||
|
cost: String,
|
||||||
|
size: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val labelStyle = MaterialTheme.typography.labelMedium
|
||||||
|
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = cost,
|
||||||
|
style = labelStyle,
|
||||||
|
)
|
||||||
|
Button(onClick = onClick) {
|
||||||
|
Text(text = name)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = size,
|
||||||
|
style = labelStyle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,7 +107,7 @@ fun DownloadCard(
|
|||||||
movableContentOf<DownloadInfo> {
|
movableContentOf<DownloadInfo> {
|
||||||
EhAsyncCropThumb(
|
EhAsyncCropThumb(
|
||||||
key = it,
|
key = it,
|
||||||
modifier = Modifier.aspectRatio(DEFAULT_ASPECT).fillMaxSize(),
|
modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import coil3.Image
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.compose.AsyncImagePainter
|
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||||
@@ -29,13 +28,15 @@ fun requestOf(model: GalleryInfo): ImageRequest {
|
|||||||
fun EhAsyncThumb(
|
fun EhAsyncThumb(
|
||||||
model: GalleryInfo,
|
model: GalleryInfo,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
placeholder: Painter? = null,
|
onSuccess: ((Image) -> Unit)? = null,
|
||||||
contentScale: ContentScale = ContentScale.Fit,
|
contentScale: ContentScale = ContentScale.Fit,
|
||||||
) = AsyncImage(
|
) = AsyncImage(
|
||||||
model = requestOf(model),
|
model = requestOf(model),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
placeholder = placeholder,
|
onSuccess = onSuccess?.let { callback ->
|
||||||
|
{ callback(it.result.image) }
|
||||||
|
},
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,11 +50,9 @@ fun EhAsyncCropThumb(
|
|||||||
model = requestOf(key),
|
model = requestOf(key),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onState = {
|
onSuccess = {
|
||||||
if (it is AsyncImagePainter.State.Success) {
|
if (it.result.image.shouldCrop) {
|
||||||
if (it.result.image.shouldCrop) {
|
contentScale = ContentScale.Crop
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -31,28 +30,27 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
import com.hippo.ehviewer.ui.tools.PredictiveBackEasing
|
import com.hippo.ehviewer.ui.tools.PredictiveBackEasing
|
||||||
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
||||||
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.coroutines.onEachLatest
|
import moe.tarsin.coroutines.onEachLatest
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
|
||||||
|
|
||||||
enum class FabLayoutValue {
|
@Stable
|
||||||
Hidden,
|
enum class FabLayoutValue { Hidden, Primary, Expand }
|
||||||
Primary,
|
|
||||||
Expand,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Stable
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberFabLayoutState(initialValue: FabLayoutValue = FabLayoutValue.Primary): FabLayoutState {
|
fun rememberFabLayoutState(initialValue: FabLayoutValue = FabLayoutValue.Primary) = remember {
|
||||||
return remember { FabLayoutState(initialValue) }
|
FabLayoutState(initialValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@@ -98,9 +96,11 @@ class FabLayoutState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun interface FabBuilder {
|
fun interface FabBuilder {
|
||||||
fun onClick(icon: ImageVector, that: suspend () -> Unit)
|
fun onClick(icon: ImageVector, autoClose: Boolean, that: suspend () -> Unit)
|
||||||
|
fun onClick(icon: ImageVector, that: suspend () -> Unit) = onClick(icon, true, that)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context(CoroutineScope)
|
||||||
@Composable
|
@Composable
|
||||||
fun FabLayout(
|
fun FabLayout(
|
||||||
hidden: Boolean,
|
hidden: Boolean,
|
||||||
@@ -133,7 +133,11 @@ fun FabLayout(
|
|||||||
record { buildFab(builder) }
|
record { buildFab(builder) }
|
||||||
transform { onEachLatest { state.waitCollapse() } }
|
transform { onEachLatest { state.waitCollapse() } }
|
||||||
}
|
}
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val interval = remember(density) { with(density) { FabInterval.roundToPx() } }
|
||||||
|
val padding = remember(density) { with(density) { FabPadding.roundToPx() } }
|
||||||
|
|
||||||
if (updatedExpanded && autoCancel) {
|
if (updatedExpanded && autoCancel) {
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier.fillMaxSize().pointerInput(Unit) {
|
modifier = Modifier.fillMaxSize().pointerInput(Unit) {
|
||||||
@@ -141,6 +145,7 @@ fun FabLayout(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val animatedProgress by state.expandProgress.asState()
|
val animatedProgress by state.expandProgress.asState()
|
||||||
PredictiveBackHandler(updatedExpanded) { flow ->
|
PredictiveBackHandler(updatedExpanded) { flow ->
|
||||||
try {
|
try {
|
||||||
@@ -152,9 +157,7 @@ fun FabLayout(
|
|||||||
}
|
}
|
||||||
onExpandChanged(false)
|
onExpandChanged(false)
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
coroutineScope.launch {
|
launch { state.expand() }
|
||||||
state.expand()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
@@ -167,18 +170,16 @@ fun FabLayout(
|
|||||||
contentAlignment = Alignment.BottomEnd,
|
contentAlignment = Alignment.BottomEnd,
|
||||||
) {
|
) {
|
||||||
with(secondaryFab) {
|
with(secondaryFab) {
|
||||||
forEachIndexed { index, (imageVector, onClick) ->
|
forEachIndexed { index, (imageVector, autoClose, onClick) ->
|
||||||
SmallFloatingActionButton(
|
SmallFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
coroutineScope.launchIO {
|
launch(Dispatchers.Default) {
|
||||||
runSuspendCatching {
|
onClick()
|
||||||
onClick()
|
if (autoClose) onExpandChanged(false)
|
||||||
}
|
|
||||||
onExpandChanged(false)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(20.dp).offset {
|
modifier = Modifier.padding(20.dp).offset {
|
||||||
val distance = lerp(0, 150 * (size - index) + 50, animatedProgress)
|
val distance = lerp(0, interval * (size - index) + padding, animatedProgress)
|
||||||
IntOffset(0, -distance)
|
IntOffset(0, -distance)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -209,9 +210,11 @@ fun FabLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildFab(builder: FabBuilder.() -> Unit) = buildList {
|
private fun buildFab(builder: FabBuilder.() -> Unit) = buildList {
|
||||||
builder { icon, action ->
|
builder { icon, autoClose, action ->
|
||||||
add(icon to action)
|
add(Triple(icon, autoClose, action))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val FabInterval = 56.dp // Small FAB height + 16 dp
|
||||||
|
private val FabPadding = 8.dp // (FAB height - small FAB height) / 2
|
||||||
const val FAB_ANIMATE_TIME = 300
|
const val FAB_ANIMATE_TIME = 300
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.hippo.ehviewer.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.material3.Badge
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FundsItem(
|
||||||
|
type: String,
|
||||||
|
amount: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
textStyle: TextStyle = LocalTextStyle.current,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Badge(containerColor = MaterialTheme.colorScheme.primaryContainer) {
|
||||||
|
Text(text = type)
|
||||||
|
}
|
||||||
|
Text(text = amount, style = textStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,13 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
@@ -25,16 +26,14 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.contentColorFor
|
import androidx.compose.material3.contentColorFor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.BrushPainter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.layout.layoutId
|
import androidx.compose.ui.layout.layoutId
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -108,7 +107,7 @@ fun GalleryInfoListItem(
|
|||||||
Card {
|
Card {
|
||||||
EhAsyncCropThumb(
|
EhAsyncCropThumb(
|
||||||
key = info,
|
key = info,
|
||||||
modifier = Modifier.aspectRatio(DEFAULT_ASPECT).fillMaxSize(),
|
modifier = Modifier.aspectRatio(DEFAULT_RATIO).fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ConstraintLayout(
|
ConstraintLayout(
|
||||||
@@ -206,7 +205,8 @@ fun GalleryInfoGridItem(
|
|||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
info: GalleryInfo,
|
info: GalleryInfo,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
badgeText: String? = info.simpleLanguage,
|
showLanguage: Boolean = true,
|
||||||
|
showPages: Boolean = true,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
) = ElevatedCard(
|
) = ElevatedCard(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -215,19 +215,20 @@ fun GalleryInfoGridItem(
|
|||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
val placeholder = remember {
|
var ratio by remember(info) {
|
||||||
val aspect = if (info.thumbHeight != 0) {
|
val ratio = if (info.thumbHeight != 0) {
|
||||||
(info.thumbWidth.toFloat() / info.thumbHeight).coerceIn(MIN_ASPECT, MAX_ASPECT)
|
(info.thumbWidth.toFloat() / info.thumbHeight).coerceIn(MIN_RATIO, MAX_RATIO)
|
||||||
} else {
|
} else {
|
||||||
DEFAULT_ASPECT
|
DEFAULT_RATIO
|
||||||
}
|
}
|
||||||
BrushPainter(Brush.linearGradient(PlaceholderColors, end = Offset(aspect, 1f)))
|
mutableFloatStateOf(ratio)
|
||||||
}
|
}
|
||||||
EhAsyncThumb(
|
EhAsyncThumb(
|
||||||
model = info,
|
model = info,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.aspectRatio(ratio),
|
||||||
placeholder = placeholder,
|
onSuccess = {
|
||||||
contentScale = ContentScale.Crop,
|
ratio = (it.width.toFloat() / it.height).coerceIn(MIN_RATIO, MAX_RATIO)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
val categoryColor = EhUtils.getCategoryColor(info.category)
|
val categoryColor = EhUtils.getCategoryColor(info.category)
|
||||||
Badge(
|
Badge(
|
||||||
@@ -235,15 +236,20 @@ fun GalleryInfoGridItem(
|
|||||||
containerColor = categoryColor,
|
containerColor = categoryColor,
|
||||||
contentColor = if (Settings.harmonizeCategoryColor) contentColorFor(categoryColor) else EhUtils.categoryTextColor,
|
contentColor = if (Settings.harmonizeCategoryColor) contentColorFor(categoryColor) else EhUtils.categoryTextColor,
|
||||||
) {
|
) {
|
||||||
if (badgeText != null) {
|
val shouldShowLanguage = showLanguage && info.simpleLanguage != null
|
||||||
Text(text = badgeText)
|
if (showPages && info.pages > 0) {
|
||||||
|
Text(text = "${info.pages}")
|
||||||
|
if (shouldShowLanguage) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldShowLanguage) {
|
||||||
|
Text(text = "${info.simpleLanguage}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val PlaceholderColors = listOf(Color.Transparent, Color.Transparent)
|
private const val MIN_RATIO = 0.5F
|
||||||
|
private const val MAX_RATIO = 1.5F
|
||||||
private const val MIN_ASPECT = 0.33F
|
const val DEFAULT_RATIO = 0.67F
|
||||||
private const val MAX_ASPECT = 1.5F
|
|
||||||
const val DEFAULT_ASPECT = 0.67F
|
|
||||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
@@ -25,19 +26,24 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.ShapeDefaults
|
import androidx.compose.material3.ShapeDefaults
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshState
|
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||||
|
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
||||||
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
@@ -54,7 +60,10 @@ import com.hippo.ehviewer.ui.screen.collectDetailSizeAsState
|
|||||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid
|
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid
|
||||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid
|
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid
|
||||||
import com.hippo.ehviewer.util.displayString
|
import com.hippo.ehviewer.util.displayString
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
operator fun PaddingValues.plus(r: PaddingValues) = object : PaddingValues {
|
operator fun PaddingValues.plus(r: PaddingValues) = object : PaddingValues {
|
||||||
@@ -65,6 +74,7 @@ operator fun PaddingValues.plus(r: PaddingValues) = object : PaddingValues {
|
|||||||
override fun calculateTopPadding() = l.calculateTopPadding() + r.calculateTopPadding()
|
override fun calculateTopPadding() = l.calculateTopPadding() + r.calculateTopPadding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context(CoroutineScope)
|
||||||
@Composable
|
@Composable
|
||||||
fun GalleryList(
|
fun GalleryList(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -76,20 +86,41 @@ fun GalleryList(
|
|||||||
detailItemContent: @Composable (LazyGridItemScope.(BaseGalleryInfo) -> Unit),
|
detailItemContent: @Composable (LazyGridItemScope.(BaseGalleryInfo) -> Unit),
|
||||||
thumbListState: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
|
thumbListState: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
|
||||||
thumbItemContent: @Composable (LazyStaggeredGridItemScope.(BaseGalleryInfo) -> Unit),
|
thumbItemContent: @Composable (LazyStaggeredGridItemScope.(BaseGalleryInfo) -> Unit),
|
||||||
refreshState: PullToRefreshState,
|
searchBarOffsetY: () -> Int,
|
||||||
scrollToTopOnRefresh: Boolean = true,
|
scrollToTopOnRefresh: Boolean = true,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
onLoading: () -> Unit,
|
onLoading: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h)
|
val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h)
|
||||||
val marginV = dimensionResource(id = R.dimen.gallery_list_margin_v)
|
val marginV = dimensionResource(id = R.dimen.gallery_list_margin_v)
|
||||||
val combinedModifier = contentModifier.nestedScroll(refreshState.nestedScrollConnection)
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
val refreshState = rememberPullToRefreshState()
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxSize().pullToRefresh(
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
state = refreshState,
|
||||||
|
enabled = data.loadState.refresh is LoadState.NotLoading,
|
||||||
|
) {
|
||||||
|
isRefreshing = true
|
||||||
|
launch {
|
||||||
|
if (data.loadState.prepend.endOfPaginationReached) {
|
||||||
|
onRefresh()
|
||||||
|
} else {
|
||||||
|
data.retry()
|
||||||
|
}
|
||||||
|
snapshotFlow { data.loadState }.drop(1).first { state ->
|
||||||
|
state.prepend !is LoadState.Loading && state.refresh !is LoadState.Loading
|
||||||
|
}
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
if (listMode == 0) {
|
if (listMode == 0) {
|
||||||
val columnWidth by collectDetailSizeAsState()
|
val columnWidth by collectDetailSizeAsState()
|
||||||
FastScrollLazyVerticalGrid(
|
FastScrollLazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(columnWidth),
|
columns = GridCells.Adaptive(columnWidth),
|
||||||
modifier = combinedModifier,
|
modifier = contentModifier,
|
||||||
state = detailListState,
|
state = detailListState,
|
||||||
contentPadding = contentPadding + PaddingValues(marginH, marginV),
|
contentPadding = contentPadding + PaddingValues(marginH, marginV),
|
||||||
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)),
|
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)),
|
||||||
@@ -118,7 +149,7 @@ fun GalleryList(
|
|||||||
val thumbColumns by Settings.thumbColumns.collectAsState()
|
val thumbColumns by Settings.thumbColumns.collectAsState()
|
||||||
FastScrollLazyVerticalStaggeredGrid(
|
FastScrollLazyVerticalStaggeredGrid(
|
||||||
columns = StaggeredGridCells.Fixed(thumbColumns),
|
columns = StaggeredGridCells.Fixed(thumbColumns),
|
||||||
modifier = combinedModifier,
|
modifier = contentModifier,
|
||||||
state = thumbListState,
|
state = thumbListState,
|
||||||
verticalItemSpacing = gridInterval,
|
verticalItemSpacing = gridInterval,
|
||||||
horizontalArrangement = Arrangement.spacedBy(gridInterval),
|
horizontalArrangement = Arrangement.spacedBy(gridInterval),
|
||||||
@@ -144,23 +175,8 @@ fun GalleryList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refreshState.isRefreshing) {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (data.loadState.prepend.endOfPaginationReached) {
|
|
||||||
onRefresh()
|
|
||||||
} else {
|
|
||||||
data.retry()
|
|
||||||
}
|
|
||||||
snapshotFlow { data.loadState }.drop(1).collect { state ->
|
|
||||||
if (state.prepend !is LoadState.Loading && state.refresh !is LoadState.Loading) {
|
|
||||||
refreshState.endRefresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (val state = data.loadState.refresh) {
|
when (val state = data.loadState.refresh) {
|
||||||
is LoadState.Loading -> if (!refreshState.isRefreshing && scrollToTopOnRefresh) {
|
is LoadState.Loading -> if (!isRefreshing && scrollToTopOnRefresh) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
onLoading()
|
onLoading()
|
||||||
}
|
}
|
||||||
@@ -195,6 +211,13 @@ fun GalleryList(
|
|||||||
ErrorTip(modifier = Modifier.widthIn(max = 228.dp), text = stringResource(id = R.string.gallery_list_empty_hit))
|
ErrorTip(modifier = Modifier.widthIn(max = 228.dp), text = stringResource(id = R.string.gallery_list_empty_hit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PullToRefreshDefaults.Indicator(
|
||||||
|
state = refreshState,
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter).padding(top = contentPadding.calculateTopPadding())
|
||||||
|
.offset { IntOffset(0, searchBarOffsetY()) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ fun EhPreviewItem(
|
|||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
CrystalCard(
|
CrystalCard(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = Modifier.aspectRatio(DEFAULT_ASPECT),
|
modifier = Modifier.aspectRatio(DEFAULT_RATIO),
|
||||||
) {
|
) {
|
||||||
if (galleryPreview != null) {
|
if (galleryPreview != null) {
|
||||||
EhAsyncPreview(
|
EhAsyncPreview(
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package com.hippo.ehviewer.ui.main
|
|||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
@@ -12,9 +12,7 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.FilterAlt
|
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.outlined.FilterAlt
|
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
@@ -39,8 +37,9 @@ import com.hippo.ehviewer.Settings
|
|||||||
import com.hippo.ehviewer.asMutableState
|
import com.hippo.ehviewer.asMutableState
|
||||||
import com.hippo.ehviewer.client.EhTagDatabase
|
import com.hippo.ehviewer.client.EhTagDatabase
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANG_TAGS
|
||||||
import com.hippo.ehviewer.collectAsState
|
import com.hippo.ehviewer.collectAsState
|
||||||
|
import com.hippo.ehviewer.ui.tools.DropdownFilterChip
|
||||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||||
import com.hippo.ehviewer.ui.tools.thenIf
|
import com.hippo.ehviewer.ui.tools.thenIf
|
||||||
import com.hippo.ehviewer.util.toIntOrDefault
|
import com.hippo.ehviewer.util.toIntOrDefault
|
||||||
@@ -74,16 +73,12 @@ fun SearchFilter(
|
|||||||
fun isCategoryChecked(bit: Int) = category and bit != 0
|
fun isCategoryChecked(bit: Int) = category and bit != 0
|
||||||
val categories = remember(category) { categoryTable.sortedBy { !isCategoryChecked(it.first) } }
|
val categories = remember(category) { categoryTable.sortedBy { !isCategoryChecked(it.first) } }
|
||||||
LazyRow(
|
LazyRow(
|
||||||
// Workaround for https://issuetracker.google.com/332939169
|
contentPadding = PaddingValues(start = 8.dp, top = 8.dp, end = 16.dp),
|
||||||
// contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
modifier = Modifier.padding(top = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
// Workaround for the first item's animation
|
// Workaround for the first item's animation
|
||||||
// https://github.com/Calvin-LL/Reorderable/issues/4#issuecomment-1853131769
|
// https://github.com/Calvin-LL/Reorderable/issues/4#issuecomment-1853131769
|
||||||
item {
|
item {}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
}
|
|
||||||
items(categories, { it.first }) {
|
items(categories, { it.first }) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = isCategoryChecked(it.first),
|
selected = isCategoryChecked(it.first),
|
||||||
@@ -92,21 +87,6 @@ fun SearchFilter(
|
|||||||
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var languageFilter by Settings.languageFilter.asMutableState()
|
|
||||||
val languages = remember {
|
|
||||||
GalleryInfo.S_LANG_TAGS.map { tag ->
|
|
||||||
tag.substringAfter(':').let {
|
|
||||||
if (EhTagDatabase.initialized && EhTagDatabase.isTranslatable(context)) {
|
|
||||||
EhTagDatabase.getTranslation("l", it) ?: it
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.horizontalScroll(rememberScrollState()).padding(horizontal = 16.dp, vertical = 8.dp),
|
modifier = Modifier.horizontalScroll(rememberScrollState()).padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
@@ -114,22 +94,38 @@ fun SearchFilter(
|
|||||||
) {
|
) {
|
||||||
val any = stringResource(id = R.string.any)
|
val any = stringResource(id = R.string.any)
|
||||||
val languageStr = stringResource(id = R.string.key_language)
|
val languageStr = stringResource(id = R.string.key_language)
|
||||||
FilterChip(
|
val languages = remember {
|
||||||
selected = languageFilter != -1,
|
val translatable = EhTagDatabase.initialized && EhTagDatabase.isTranslatable(context)
|
||||||
onClick = {
|
List(S_LANG_TAGS.size + 1) { i ->
|
||||||
scope.launch {
|
if (i == 0) {
|
||||||
val items = listOf(any, *languages.toTypedArray())
|
any
|
||||||
languageFilter = dialogState.showSingleChoice(items, languageFilter + 1) - 1
|
} else {
|
||||||
|
val tag = S_LANG_TAGS[i - 1].substringAfter(':')
|
||||||
|
if (translatable) {
|
||||||
|
EhTagDatabase.getTranslation("l", tag) ?: tag
|
||||||
|
} else {
|
||||||
|
tag
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var languageFilter by Settings.languageFilter.asMutableState()
|
||||||
|
DropdownFilterChip(
|
||||||
|
label = languageStr,
|
||||||
|
menuItems = languages,
|
||||||
|
selectedItemIndex = languageFilter + 1,
|
||||||
|
onSelectedItemIndexChange = {
|
||||||
|
languageFilter = it - 1
|
||||||
},
|
},
|
||||||
label = {
|
)
|
||||||
Text(text = if (languageFilter == -1) languageStr else languages[languageFilter])
|
val minRatingItems = stringArrayResource(id = R.array.search_min_rating)
|
||||||
},
|
val minRatingStr = stringResource(id = R.string.search_sr)
|
||||||
trailingIcon = {
|
DropdownFilterChip(
|
||||||
Icon(
|
label = minRatingStr,
|
||||||
imageVector = if (languageFilter == -1) Icons.Outlined.FilterAlt else Icons.Default.FilterAlt,
|
menuItems = minRatingItems.asList(),
|
||||||
contentDescription = null,
|
selectedItemIndex = (advancedOption.minRating - 1).coerceAtLeast(0),
|
||||||
)
|
onSelectedItemIndexChange = {
|
||||||
|
onAdvancedOptionChanged(advancedOption.copy(minRating = if (it == 0) 0 else it + 1))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val pageErr1 = stringResource(R.string.search_sp_err1)
|
val pageErr1 = stringResource(R.string.search_sp_err1)
|
||||||
@@ -211,19 +207,6 @@ fun SearchFilter(
|
|||||||
},
|
},
|
||||||
label = { Text(text = pagesText) },
|
label = { Text(text = pagesText) },
|
||||||
)
|
)
|
||||||
val minRatingItems = stringArrayResource(id = R.array.search_min_rating)
|
|
||||||
val minRatingStr = stringResource(id = R.string.search_sr)
|
|
||||||
val minRatingIndex = (advancedOption.minRating - 1).coerceAtLeast(0)
|
|
||||||
FilterChip(
|
|
||||||
selected = advancedOption.minRating != 0,
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
val selected = dialogState.showSingleChoice(minRatingItems.toList(), minRatingIndex)
|
|
||||||
onAdvancedOptionChanged(advancedOption.copy(minRating = if (selected == 0) 0 else selected + 1))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = { Text(text = minRatingStr + minRatingItems[minRatingIndex]) },
|
|
||||||
)
|
|
||||||
fun checked(bit: Int) = advancedOption.advanceSearch and bit != 0
|
fun checked(bit: Int) = advancedOption.advanceSearch and bit != 0
|
||||||
fun AdvancedSearchOption.inv(bit: Int) = onAdvancedOptionChanged(copy(advanceSearch = advanceSearch xor bit))
|
fun AdvancedSearchOption.inv(bit: Int) = onAdvancedOptionChanged(copy(advanceSearch = advanceSearch xor bit))
|
||||||
FilterChip(
|
FilterChip(
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.hippo.ehviewer.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.MarqueeSpacing
|
||||||
|
import androidx.compose.foundation.basicMarquee
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.hippo.ehviewer.R
|
||||||
|
import com.hippo.ehviewer.client.parser.Torrent
|
||||||
|
import com.hippo.ehviewer.client.parser.format
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TorrentList(
|
||||||
|
items: List<Torrent>,
|
||||||
|
onItemClick: (Torrent) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val title = stringResource(R.string.torrents)
|
||||||
|
Column(
|
||||||
|
modifier = modifier.padding(vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(text = title, style = MaterialTheme.typography.headlineSmall)
|
||||||
|
val labelStyle = MaterialTheme.typography.labelLarge
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(items) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.clickable { onItemClick(it) }.minimumInteractiveComponentSize()
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = it.name,
|
||||||
|
modifier = Modifier.basicMarquee(
|
||||||
|
spacing = MarqueeSpacing(16.dp),
|
||||||
|
velocity = 60.dp,
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = it.posted,
|
||||||
|
modifier = Modifier.weight(2.5f),
|
||||||
|
color = if (it.outdated) MaterialTheme.colorScheme.error else Color.Unspecified,
|
||||||
|
style = labelStyle,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = it.uploader,
|
||||||
|
modifier = Modifier.weight(2.5f),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1,
|
||||||
|
style = labelStyle,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = it.size,
|
||||||
|
modifier = Modifier.weight(2f),
|
||||||
|
style = labelStyle,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = it.format(),
|
||||||
|
modifier = Modifier.weight(3f),
|
||||||
|
style = labelStyle,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.hippo.ehviewer.ui.reader
|
|||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
@@ -39,6 +40,7 @@ fun GalleryPager(
|
|||||||
onSelectPage: (ReaderPage) -> Unit,
|
onSelectPage: (ReaderPage) -> Unit,
|
||||||
onMenuRegionClick: () -> Unit,
|
onMenuRegionClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
) {
|
) {
|
||||||
val items = pageLoader.pages
|
val items = pageLoader.pages
|
||||||
when (type) {
|
when (type) {
|
||||||
@@ -46,6 +48,7 @@ fun GalleryPager(
|
|||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
contentPadding = contentPadding,
|
||||||
reverseLayout = type == RIGHT_TO_LEFT,
|
reverseLayout = type == RIGHT_TO_LEFT,
|
||||||
key = { it },
|
key = { it },
|
||||||
) { index ->
|
) { index ->
|
||||||
@@ -82,6 +85,7 @@ fun GalleryPager(
|
|||||||
VerticalPager(
|
VerticalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
contentPadding = contentPadding,
|
||||||
key = { it },
|
key = { it },
|
||||||
) { index ->
|
) { index ->
|
||||||
val page = items[index]
|
val page = items[index]
|
||||||
@@ -132,6 +136,7 @@ fun GalleryPager(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
|
contentPadding = contentPadding,
|
||||||
verticalArrangement = Arrangement.spacedBy(if (type != WEBTOON) 15.dp else 0.dp),
|
verticalArrangement = Arrangement.spacedBy(if (type != WEBTOON) 15.dp else 0.dp),
|
||||||
) {
|
) {
|
||||||
items(items, key = { it.index }) { page ->
|
items(items, key = { it.index }) { page ->
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ suspend fun save(page: ReaderPage) {
|
|||||||
val cannotSave = getString(R.string.error_cant_save_image)
|
val cannotSave = getString(R.string.error_cant_save_image)
|
||||||
if (granted) {
|
if (granted) {
|
||||||
val filename = getImageFilename(page.index)
|
val filename = getImageFilename(page.index)
|
||||||
|
if (filename == null) {
|
||||||
|
showSnackbar(cannotSave)
|
||||||
|
return
|
||||||
|
}
|
||||||
val extension = FileUtils.getExtensionFromFilename(filename)
|
val extension = FileUtils.getExtensionFromFilename(filename)
|
||||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "image/jpeg"
|
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "image/jpeg"
|
||||||
val values = ContentValues()
|
val values = ContentValues()
|
||||||
@@ -133,6 +137,10 @@ suspend fun save(page: ReaderPage) {
|
|||||||
context(SnackbarHostState, Context, PageLoader2)
|
context(SnackbarHostState, Context, PageLoader2)
|
||||||
suspend fun saveTo(page: ReaderPage) {
|
suspend fun saveTo(page: ReaderPage) {
|
||||||
val filename = getImageFilename(page.index)
|
val filename = getImageFilename(page.index)
|
||||||
|
if (filename == null) {
|
||||||
|
showSnackbar(getString(R.string.error_cant_save_image))
|
||||||
|
return
|
||||||
|
}
|
||||||
val extension = FileUtils.getExtensionFromFilename(filename)
|
val extension = FileUtils.getExtensionFromFilename(filename)
|
||||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "image/jpeg"
|
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "image/jpeg"
|
||||||
page.runSuspendCatching {
|
page.runSuspendCatching {
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.asMutableState
|
import com.hippo.ehviewer.asMutableState
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastP
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderGeneralSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
|
fun ReaderGeneralSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
|
||||||
@@ -39,11 +42,15 @@ fun ReaderGeneralSetting() = Column(modifier = Modifier.verticalScroll(rememberS
|
|||||||
title = stringResource(id = R.string.pref_fullscreen),
|
title = stringResource(id = R.string.pref_fullscreen),
|
||||||
field = fullscreen,
|
field = fullscreen,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = fullscreen.value) {
|
val view = LocalView.current
|
||||||
SwitchChoice(
|
val hasDisplayCutout = remember(view) { isAtLeastP && view.rootWindowInsets.displayCutout != null }
|
||||||
title = stringResource(id = R.string.pref_cutout_short),
|
if (hasDisplayCutout) {
|
||||||
field = Settings.cutoutShort.asMutableState(),
|
AnimatedVisibility(visible = fullscreen.value) {
|
||||||
)
|
SwitchChoice(
|
||||||
|
title = stringResource(id = R.string.pref_cutout_short),
|
||||||
|
field = Settings.cutoutShort.asMutableState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SwitchChoice(
|
SwitchChoice(
|
||||||
title = stringResource(id = R.string.pref_keep_screen_on),
|
title = stringResource(id = R.string.pref_keep_screen_on),
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ private fun PagerSetting() = Column {
|
|||||||
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
||||||
entries = stringArrayResource(id = R.array.invert_tapping_mode),
|
entries = stringArrayResource(id = R.array.invert_tapping_mode),
|
||||||
values = arrayOf("0", "1", "2", "3"),
|
values = arrayOf("0", "1", "2", "3"),
|
||||||
field = Settings.readerWebtoonNavInverted.asMutableState(),
|
field = Settings.readerPagerNavInverted.asMutableState(),
|
||||||
)
|
)
|
||||||
SwitchChoice(
|
SwitchChoice(
|
||||||
title = stringResource(id = R.string.pref_navigate_pan),
|
title = stringResource(id = R.string.pref_navigate_pan),
|
||||||
|
|||||||
@@ -4,10 +4,17 @@ import androidx.compose.animation.animateColorAsState
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.displayCutout
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.statusBarsIgnoringVisibility
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material3.BottomSheetDefaults
|
import androidx.compose.material3.BottomSheetDefaults
|
||||||
@@ -24,11 +31,15 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
@@ -37,7 +48,6 @@ import com.hippo.ehviewer.download.DownloadManager
|
|||||||
import com.hippo.ehviewer.download.archiveFile
|
import com.hippo.ehviewer.download.archiveFile
|
||||||
import com.hippo.ehviewer.gallery.ArchivePageLoader
|
import com.hippo.ehviewer.gallery.ArchivePageLoader
|
||||||
import com.hippo.ehviewer.gallery.EhPageLoader
|
import com.hippo.ehviewer.gallery.EhPageLoader
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.ui.composing
|
import com.hippo.ehviewer.ui.composing
|
||||||
import com.hippo.ehviewer.ui.tools.Deferred
|
import com.hippo.ehviewer.ui.tools.Deferred
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
@@ -56,7 +66,6 @@ import moe.tarsin.kt.unreachable
|
|||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsNavigator) = composing(navigator) {
|
fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsNavigator) = composing(navigator) {
|
||||||
LockDrawer(true)
|
|
||||||
ConfigureKeepScreenOn()
|
ConfigureKeepScreenOn()
|
||||||
val pageLoader = remember {
|
val pageLoader = remember {
|
||||||
val archive = DownloadManager.getDownloadInfo(info.gid)?.archiveFile
|
val archive = DownloadManager.getDownloadInfo(info.gid)?.archiveFile
|
||||||
@@ -76,6 +85,16 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
|||||||
val showSeekbar by Settings.showReaderSeekbar.collectAsState()
|
val showSeekbar by Settings.showReaderSeekbar.collectAsState()
|
||||||
val readingMode by Settings.readingMode.collectAsState { ReadingModeType.fromPreference(it) }
|
val readingMode by Settings.readingMode.collectAsState { ReadingModeType.fromPreference(it) }
|
||||||
val volumeKeysEnabled by Settings.readWithVolumeKeys.collectAsState()
|
val volumeKeysEnabled by Settings.readWithVolumeKeys.collectAsState()
|
||||||
|
val fullscreen by Settings.fullscreen.collectAsState()
|
||||||
|
val cutoutShort by Settings.cutoutShort.collectAsState()
|
||||||
|
val uiController = rememberSystemUiController()
|
||||||
|
DisposableEffect(uiController) {
|
||||||
|
uiController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
onDispose {
|
||||||
|
uiController.isSystemBarsVisible = true
|
||||||
|
uiController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
Deferred({ pageLoader.awaitReady() }) {
|
Deferred({ pageLoader.awaitReady() }) {
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
val pagerState = rememberPagerState { pageLoader.size }
|
val pagerState = rememberPagerState { pageLoader.size }
|
||||||
@@ -84,6 +103,13 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
|||||||
Box {
|
Box {
|
||||||
var appbarVisible by remember { mutableStateOf(false) }
|
var appbarVisible by remember { mutableStateOf(false) }
|
||||||
val bgColor by collectBackgroundColorAsState()
|
val bgColor by collectBackgroundColorAsState()
|
||||||
|
if (fullscreen) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { appbarVisible }.collect {
|
||||||
|
uiController.isSystemBarsVisible = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
VolumeKeysHandler(
|
VolumeKeysHandler(
|
||||||
enabled = { volumeKeysEnabled && !appbarVisible },
|
enabled = { volumeKeysEnabled && !appbarVisible },
|
||||||
movePrevious = { syncState.sliderScrollTo(syncState.sliderValue - 1) },
|
movePrevious = { syncState.sliderScrollTo(syncState.sliderValue - 1) },
|
||||||
@@ -102,8 +128,9 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
|||||||
val state = rememberModalBottomSheetState()
|
val state = rememberModalBottomSheetState()
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { dispose() },
|
onDismissRequest = { dispose() },
|
||||||
|
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
|
||||||
sheetState = state,
|
sheetState = state,
|
||||||
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
|
contentWindowInsets = { WindowInsets(0) },
|
||||||
) {
|
) {
|
||||||
ReaderPageSheetMeta(
|
ReaderPageSheetMeta(
|
||||||
retry = { pageLoader.retryPage(page.index) },
|
retry = { pageLoader.retryPage(page.index) },
|
||||||
@@ -121,6 +148,15 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
|||||||
},
|
},
|
||||||
onMenuRegionClick = { appbarVisible = !appbarVisible },
|
onMenuRegionClick = { appbarVisible = !appbarVisible },
|
||||||
modifier = Modifier.background(bgColor),
|
modifier = Modifier.background(bgColor),
|
||||||
|
contentPadding = if (fullscreen) {
|
||||||
|
if (cutoutShort) {
|
||||||
|
PaddingValues(0.dp)
|
||||||
|
} else {
|
||||||
|
WindowInsets.displayCutout.asPaddingValues()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
WindowInsets.systemBars.asPaddingValues()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
val brightness by Settings.customBrightness.collectAsState()
|
val brightness by Settings.customBrightness.collectAsState()
|
||||||
val brightnessValue by Settings.customBrightnessValue.collectAsState()
|
val brightnessValue by Settings.customBrightnessValue.collectAsState()
|
||||||
@@ -143,7 +179,7 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
|||||||
colorBlendMode = colorOverlayMode,
|
colorBlendMode = colorOverlayMode,
|
||||||
)
|
)
|
||||||
val showPageNumber by Settings.showPageNumber.collectAsState()
|
val showPageNumber by Settings.showPageNumber.collectAsState()
|
||||||
if (showPageNumber) {
|
if (showPageNumber && !appbarVisible) {
|
||||||
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
|
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
|
||||||
PageIndicatorText(
|
PageIndicatorText(
|
||||||
currentPage = syncState.sliderValue,
|
currentPage = syncState.sliderValue,
|
||||||
@@ -170,10 +206,11 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1, navigator: DestinationsN
|
|||||||
)
|
)
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { dispose() },
|
onDismissRequest = { dispose() },
|
||||||
|
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
|
||||||
// Yeah, I know color state should not be read here, but we have to do it...
|
// Yeah, I know color state should not be read here, but we have to do it...
|
||||||
scrimColor = scrim,
|
scrimColor = scrim,
|
||||||
dragHandle = null,
|
dragHandle = null,
|
||||||
windowInsets = WindowInsets.statusBarsIgnoringVisibility,
|
contentWindowInsets = { WindowInsets(0) },
|
||||||
) {
|
) {
|
||||||
SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
|
SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
|
||||||
isColorFilter = page == 2
|
isColorFilter = page == 2
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.hippo.ehviewer.ui.screen
|
||||||
|
|
||||||
|
// https://github.com/Kotlin/KEEP/blob/context-parameters/proposals/context-parameters.md#standard-library-support
|
||||||
|
context(T)
|
||||||
|
inline fun <reified T> implicit(): T = this@T
|
||||||
@@ -2,18 +2,20 @@ package com.hippo.ehviewer.ui.screen
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.hippo.ehviewer.dao.DownloadInfo
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
|
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||||
import com.hippo.ehviewer.util.containsIgnoreCase
|
import com.hippo.ehviewer.util.containsIgnoreCase
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class DownloadsFilterState(
|
data class DownloadsFilterState(
|
||||||
|
val mode: DownloadsFilterMode,
|
||||||
val label: String?,
|
val label: String?,
|
||||||
val state: Int = -1,
|
val state: Int = -1,
|
||||||
val keyword: String = "",
|
val keyword: String = "",
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
fun DownloadsFilterState.take(info: DownloadInfo) =
|
fun DownloadsFilterState.take(info: DownloadInfo) =
|
||||||
(label == "" || info.label == label) && (state == -1 || info.state == state) &&
|
mode.take(info, label) && (state == -1 || info.state == state) &&
|
||||||
with(info) {
|
with(info) {
|
||||||
title.containsIgnoreCase(keyword) || titleJpn.containsIgnoreCase(keyword) ||
|
title.containsIgnoreCase(keyword) || titleJpn.containsIgnoreCase(keyword) ||
|
||||||
simpleTags?.any { it.containsIgnoreCase(keyword) } ?: false
|
simpleTags?.any { it.containsIgnoreCase(keyword) } ?: false
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.hippo.ehviewer.ui.screen
|
package com.hippo.ehviewer.ui.screen
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.ViewConfiguration
|
import android.view.ViewConfiguration
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
@@ -10,11 +12,13 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
@@ -27,6 +31,7 @@ import androidx.compose.material.icons.automirrored.filled.Sort
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ViewList
|
import androidx.compose.material.icons.automirrored.filled.ViewList
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.DoneAll
|
import androidx.compose.material.icons.filled.DoneAll
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
import androidx.compose.material.icons.filled.GridView
|
import androidx.compose.material.icons.filled.GridView
|
||||||
@@ -34,6 +39,7 @@ import androidx.compose.material.icons.filled.MoreVert
|
|||||||
import androidx.compose.material.icons.filled.NewLabel
|
import androidx.compose.material.icons.filled.NewLabel
|
||||||
import androidx.compose.material.icons.filled.Pause
|
import androidx.compose.material.icons.filled.Pause
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Reorder
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Shuffle
|
import androidx.compose.material.icons.filled.Shuffle
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
@@ -43,13 +49,15 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
||||||
import androidx.compose.material3.SwipeToDismissBoxState
|
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.fork.SwipeToDismissBox
|
||||||
|
import androidx.compose.material3.fork.SwipeToDismissBoxState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
@@ -66,10 +74,7 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -80,36 +85,39 @@ import com.hippo.ehviewer.EhDB
|
|||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.asMutableState
|
import com.hippo.ehviewer.asMutableState
|
||||||
|
import com.hippo.ehviewer.client.EhTagDatabase
|
||||||
|
import com.hippo.ehviewer.client.data.TagNamespace
|
||||||
import com.hippo.ehviewer.collectAsState
|
import com.hippo.ehviewer.collectAsState
|
||||||
import com.hippo.ehviewer.dao.DownloadInfo
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
import com.hippo.ehviewer.download.DownloadManager.labelList
|
import com.hippo.ehviewer.download.DownloadManager.downloadInfoList
|
||||||
import com.hippo.ehviewer.download.DownloadService
|
import com.hippo.ehviewer.download.DownloadService
|
||||||
|
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||||
import com.hippo.ehviewer.download.SortMode
|
import com.hippo.ehviewer.download.SortMode
|
||||||
import com.hippo.ehviewer.icons.EhIcons
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
import com.hippo.ehviewer.icons.big.Download
|
import com.hippo.ehviewer.icons.big.Download
|
||||||
|
import com.hippo.ehviewer.ui.DrawerHandle
|
||||||
import com.hippo.ehviewer.ui.LocalSideSheetState
|
import com.hippo.ehviewer.ui.LocalSideSheetState
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.ui.MainActivity
|
|
||||||
import com.hippo.ehviewer.ui.composing
|
import com.hippo.ehviewer.ui.composing
|
||||||
import com.hippo.ehviewer.ui.confirmRemoveDownloadRange
|
import com.hippo.ehviewer.ui.confirmRemoveDownloadRange
|
||||||
import com.hippo.ehviewer.ui.main.DownloadCard
|
import com.hippo.ehviewer.ui.main.DownloadCard
|
||||||
import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME
|
import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME
|
||||||
import com.hippo.ehviewer.ui.main.FabLayout
|
import com.hippo.ehviewer.ui.main.FabLayout
|
||||||
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
|
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
|
||||||
|
import com.hippo.ehviewer.ui.main.plus
|
||||||
import com.hippo.ehviewer.ui.navToReader
|
import com.hippo.ehviewer.ui.navToReader
|
||||||
import com.hippo.ehviewer.ui.showMoveDownloadLabelList
|
import com.hippo.ehviewer.ui.showMoveDownloadLabelList
|
||||||
import com.hippo.ehviewer.ui.tools.Deferred
|
import com.hippo.ehviewer.ui.tools.Deferred
|
||||||
import com.hippo.ehviewer.ui.tools.DragHandle
|
|
||||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
|
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
|
||||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid
|
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid
|
||||||
import com.hippo.ehviewer.ui.tools.SwipeToDismissBox2
|
import com.hippo.ehviewer.ui.tools.HapticFeedbackType
|
||||||
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
||||||
import com.hippo.ehviewer.ui.tools.draggingHapticFeedback
|
import com.hippo.ehviewer.ui.tools.rememberHapticFeedback
|
||||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||||
import com.hippo.ehviewer.ui.tools.thenIf
|
import com.hippo.ehviewer.ui.tools.thenIf
|
||||||
import com.hippo.ehviewer.util.findActivity
|
|
||||||
import com.hippo.ehviewer.util.mapToLongArray
|
import com.hippo.ehviewer.util.mapToLongArray
|
||||||
|
import com.hippo.ehviewer.util.takeAndClear
|
||||||
|
import com.jamal.composeprefs3.ui.ifTrueThen
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
@@ -117,45 +125,57 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
|||||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.coroutines.onEachLatest
|
import moe.tarsin.coroutines.onEachLatest
|
||||||
import sh.calvin.reorderable.ReorderableItem
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
||||||
var gridView by Settings.gridView.asMutableState()
|
var gridView by Settings.gridView.asMutableState()
|
||||||
var sortMode by Settings.downloadSortMode.asMutableState()
|
var sortMode by Settings.downloadSortMode.asMutableState()
|
||||||
var filterState by rememberSaveable { mutableStateOf(DownloadsFilterState(Settings.recentDownloadLabel.value)) }
|
val filterMode by Settings.downloadFilterMode.collectAsState { DownloadsFilterMode.from(it) }
|
||||||
|
var filterState by rememberSaveable { mutableStateOf(DownloadsFilterState(filterMode, Settings.recentDownloadLabel.value)) }
|
||||||
var invalidateKey by rememberSaveable { mutableStateOf(false) }
|
var invalidateKey by rememberSaveable { mutableStateOf(false) }
|
||||||
var searchBarOffsetY by remember(filterState.label) { mutableIntStateOf(0) }
|
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
||||||
val animateItems by Settings.animateItems.collectAsState()
|
val animateItems by Settings.animateItems.collectAsState()
|
||||||
|
|
||||||
var fabExpanded by remember { mutableStateOf(false) }
|
var fabExpanded by remember { mutableStateOf(false) }
|
||||||
var fabHidden by remember { mutableStateOf(false) }
|
var fabHidden by remember { mutableStateOf(false) }
|
||||||
val checkedInfoMap = remember { mutableStateMapOf<Long, DownloadInfo>() }
|
val checkedInfoMap = remember { mutableStateMapOf<Long, DownloadInfo>() }
|
||||||
val selectMode by rememberUpdatedState(checkedInfoMap.isNotEmpty())
|
val selectMode by rememberUpdatedState(checkedInfoMap.isNotEmpty())
|
||||||
LockDrawer(selectMode)
|
DrawerHandle(!selectMode && !searchBarExpanded)
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val activity = remember { findActivity<MainActivity>() }
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val view = LocalView.current
|
val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(implicit<Context>()) && EhTagDatabase.initialized
|
||||||
|
val ehTags = EhTagDatabase.takeIf { canTranslate }
|
||||||
|
fun String.translateArtist() = ehTags?.getTranslation(TagNamespace.Artist.toPrefix(), this) ?: this
|
||||||
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
|
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
|
||||||
val allName = stringResource(R.string.download_all)
|
val allName = stringResource(R.string.download_all)
|
||||||
val defaultName = stringResource(R.string.default_download_label_name)
|
val defaultName = stringResource(R.string.default_download_label_name)
|
||||||
|
val unknownName = stringResource(R.string.unknown_artists)
|
||||||
|
val emptyLabelName = when (filterMode) {
|
||||||
|
DownloadsFilterMode.ARTIST -> unknownName
|
||||||
|
DownloadsFilterMode.CUSTOM -> defaultName
|
||||||
|
}
|
||||||
val title = stringResource(
|
val title = stringResource(
|
||||||
R.string.scene_download_title,
|
R.string.scene_download_title,
|
||||||
with(filterState) { if (label == "") allName else label ?: defaultName },
|
with(filterState) {
|
||||||
|
when (label) {
|
||||||
|
"" -> allName
|
||||||
|
null -> emptyLabelName
|
||||||
|
else -> if (mode == DownloadsFilterMode.ARTIST) label.translateArtist() else label
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
val hint = stringResource(R.string.search_bar_hint, title)
|
val hint = stringResource(R.string.search_bar_hint, title)
|
||||||
val sortModes = stringArrayResource(id = R.array.download_sort_modes)
|
val sortModes = stringArrayResource(id = R.array.download_sort_modes)
|
||||||
val downloadStates = stringArrayResource(id = R.array.download_state)
|
val downloadStates = stringArrayResource(id = R.array.download_state)
|
||||||
val list = remember(filterState, invalidateKey) {
|
val list = remember(filterState, invalidateKey) {
|
||||||
DownloadManager.downloadInfoList.filterTo(mutableStateListOf()) { info ->
|
downloadInfoList.filterTo(mutableStateListOf()) { info ->
|
||||||
filterState.take(info)
|
filterState.take(info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,21 +186,42 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
val labelEmpty = stringResource(R.string.label_text_is_empty)
|
val labelEmpty = stringResource(R.string.label_text_is_empty)
|
||||||
val defaultInvalid = stringResource(R.string.label_text_is_invalid)
|
val defaultInvalid = stringResource(R.string.label_text_is_invalid)
|
||||||
val labelExists = stringResource(R.string.label_text_exist)
|
val labelExists = stringResource(R.string.label_text_exist)
|
||||||
val downloadsCount by rememberInVM { EhDB.downloadsCount }.collectAsState(emptyMap())
|
val downloadsCountGroupByArtist by rememberInVM { EhDB.downloadsCountByArtist }.collectAsState(emptyMap())
|
||||||
val totalCount = remember(downloadsCount) { downloadsCount.values.sum() }
|
val downloadsCountGroupByLabel by rememberInVM { EhDB.downloadsCountByLabel }.collectAsState(emptyMap())
|
||||||
|
val downloadsCount = when (filterMode) {
|
||||||
|
DownloadsFilterMode.CUSTOM -> downloadsCountGroupByLabel
|
||||||
|
DownloadsFilterMode.ARTIST -> downloadsCountGroupByArtist
|
||||||
|
}
|
||||||
|
val artistList = remember(downloadsCountGroupByArtist) {
|
||||||
|
downloadsCountGroupByArtist.keys.mapNotNull { artist -> artist?.let { it to it } }
|
||||||
|
}
|
||||||
|
val labelList by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
DownloadManager.labelList.map { it.id!! to it.label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val groupList = when (filterMode) {
|
||||||
|
DownloadsFilterMode.CUSTOM -> labelList
|
||||||
|
DownloadsFilterMode.ARTIST -> artistList
|
||||||
|
}
|
||||||
|
val totalCount = remember(downloadsCountGroupByLabel) { downloadsCountGroupByLabel.values.sum() }
|
||||||
|
|
||||||
fun switchLabel(label: String?) {
|
fun switchLabel(label: String?) {
|
||||||
Settings.recentDownloadLabel.value = label
|
Settings.recentDownloadLabel.value = label
|
||||||
filterState = filterState.copy(label = label)
|
filterState = filterState.copy(label = label)
|
||||||
}
|
}
|
||||||
|
|
||||||
with(activity) {
|
LaunchedEffect(filterState) {
|
||||||
ProvideSideSheetContent { drawerState ->
|
searchBarOffsetY = 0
|
||||||
fun closeSheet() = launch { drawerState.close() }
|
}
|
||||||
TopAppBar(
|
|
||||||
title = { Text(text = labelsStr) },
|
ProvideSideSheetContent { drawerState ->
|
||||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
fun closeSheet() = launch { drawerState.close() }
|
||||||
actions = {
|
TopAppBar(
|
||||||
|
title = { Text(text = labelsStr) },
|
||||||
|
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
|
actions = {
|
||||||
|
if (DownloadsFilterMode.CUSTOM == filterMode) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
launch {
|
launch {
|
||||||
@@ -205,9 +246,9 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
val selected = if (!Settings.hasDefaultDownloadLabel) {
|
val selected = if (!Settings.hasDefaultDownloadLabel) {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
labelList.indexOfFirst { it.label == Settings.defaultDownloadLabel } + 2
|
DownloadManager.labelList.indexOfFirst { it.label == Settings.defaultDownloadLabel } + 2
|
||||||
}
|
}
|
||||||
showSelectActions(R.string.default_download_label, selected) {
|
awaitSelectAction(R.string.default_download_label, selected) {
|
||||||
onSelect(letMeSelect) {
|
onSelect(letMeSelect) {
|
||||||
Settings.hasDefaultDownloadLabel = false
|
Settings.hasDefaultDownloadLabel = false
|
||||||
}
|
}
|
||||||
@@ -215,149 +256,181 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
Settings.hasDefaultDownloadLabel = true
|
Settings.hasDefaultDownloadLabel = true
|
||||||
Settings.defaultDownloadLabel = null
|
Settings.defaultDownloadLabel = null
|
||||||
}
|
}
|
||||||
labelList.forEach { (label) ->
|
DownloadManager.labelList.forEach { (label) ->
|
||||||
onSelect(label) {
|
onSelect(label) {
|
||||||
Settings.hasDefaultDownloadLabel = true
|
Settings.hasDefaultDownloadLabel = true
|
||||||
Settings.defaultDownloadLabel = label
|
Settings.defaultDownloadLabel = label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Default.Settings, contentDescription = null)
|
Icon(imageVector = Icons.Default.Download, contentDescription = null)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
val labelsListState = rememberLazyListState()
|
|
||||||
val reorderableLabelState = rememberReorderableLazyColumnState(labelsListState) { from, to ->
|
|
||||||
val fromPosition = from.index - 2
|
|
||||||
val toPosition = to.index - 2
|
|
||||||
labelList.apply { add(toPosition, removeAt(fromPosition)) }
|
|
||||||
view.performHapticFeedback(draggingHapticFeedback)
|
|
||||||
}
|
|
||||||
var fromIndex by remember { mutableIntStateOf(-1) }
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
state = labelsListState,
|
|
||||||
// Workaround for https://issuetracker.google.com/332939169
|
|
||||||
// contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
ListItem(
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
switchLabel("")
|
|
||||||
closeSheet()
|
|
||||||
},
|
|
||||||
tonalElevation = 1.dp,
|
|
||||||
shadowElevation = 1.dp,
|
|
||||||
headlineContent = {
|
|
||||||
Text("$allName [$totalCount]")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
item {
|
val custom = stringResource(R.string.select_grouping_mode_custom)
|
||||||
ListItem(
|
val artist = stringResource(R.string.select_grouping_mode_artist)
|
||||||
modifier = Modifier.clickable {
|
IconButton(
|
||||||
switchLabel(null)
|
onClick = {
|
||||||
closeSheet()
|
launch {
|
||||||
},
|
awaitSelectAction(R.string.select_grouping_mode) {
|
||||||
tonalElevation = 1.dp,
|
val select = { mode: DownloadsFilterMode ->
|
||||||
shadowElevation = 1.dp,
|
filterState = filterState.copy(mode = mode, label = "")
|
||||||
headlineContent = {
|
Settings.downloadFilterMode.value = mode.flag
|
||||||
Text("$defaultName [${downloadsCount.getOrDefault(null, 0)}]")
|
Settings.recentDownloadLabel.value = ""
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
itemsIndexed(labelList, key = { _, item -> item.id!! }) { index, (label, _, id) ->
|
|
||||||
val item by rememberUpdatedState(label)
|
|
||||||
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
|
||||||
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
|
|
||||||
LaunchedEffect(dismissState) {
|
|
||||||
snapshotFlow { dismissState.currentValue }.collect {
|
|
||||||
if (it == SwipeToDismissBoxValue.EndToStart) {
|
|
||||||
runCatching {
|
|
||||||
awaitPermissionOrCancel(confirmText = R.string.delete) {
|
|
||||||
Text(text = stringResource(R.string.delete_label, item))
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
DownloadManager.deleteLabel(item)
|
|
||||||
when (filterState.label) {
|
|
||||||
item -> switchLabel("")
|
|
||||||
null -> invalidateKey = !invalidateKey
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
dismissState.reset()
|
|
||||||
}
|
}
|
||||||
|
onSelect(custom) { select(DownloadsFilterMode.CUSTOM) }
|
||||||
|
onSelect(artist) { select(DownloadsFilterMode.ARTIST) }
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Default.Settings, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val labelsListState = rememberLazyListState()
|
||||||
|
val editEnable = DownloadsFilterMode.CUSTOM == filterMode
|
||||||
|
val hapticFeedback = rememberHapticFeedback()
|
||||||
|
val reorderableLabelState = rememberReorderableLazyListState(labelsListState) { from, to ->
|
||||||
|
val fromPosition = from.index - 2
|
||||||
|
val toPosition = to.index - 2
|
||||||
|
DownloadManager.labelList.apply { add(toPosition, removeAt(fromPosition)) }
|
||||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
|
||||||
|
}
|
||||||
|
var fromIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = labelsListState,
|
||||||
|
contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
switchLabel("")
|
||||||
|
closeSheet()
|
||||||
|
},
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
shadowElevation = 1.dp,
|
||||||
|
headlineContent = {
|
||||||
|
Text("$allName [$totalCount]")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
switchLabel(null)
|
||||||
|
closeSheet()
|
||||||
|
},
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
shadowElevation = 1.dp,
|
||||||
|
headlineContent = {
|
||||||
|
Text("$emptyLabelName [${downloadsCount.getOrDefault(null, 0)}]")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(groupList, key = { _, (id) -> id }) { itemIndex, (id, label) ->
|
||||||
|
val index by rememberUpdatedState(itemIndex)
|
||||||
|
val item by rememberUpdatedState(label)
|
||||||
|
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
||||||
|
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
|
||||||
|
LaunchedEffect(dismissState) {
|
||||||
|
snapshotFlow { dismissState.currentValue }.collect {
|
||||||
|
if (it == SwipeToDismissBoxValue.EndToStart) {
|
||||||
|
runCatching {
|
||||||
|
awaitPermissionOrCancel(confirmText = R.string.delete) {
|
||||||
|
Text(text = stringResource(R.string.delete_label, item))
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
DownloadManager.deleteLabel(item)
|
||||||
|
when (filterState.label) {
|
||||||
|
item -> switchLabel("")
|
||||||
|
null -> invalidateKey = !invalidateKey
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
dismissState.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ReorderableItem(reorderableLabelState, key = id!!) { isDragging ->
|
}
|
||||||
SwipeToDismissBox2(
|
ReorderableItem(reorderableLabelState, enabled = editEnable, key = id) { isDragging ->
|
||||||
state = dismissState,
|
SwipeToDismissBox(
|
||||||
backgroundContent = {},
|
state = dismissState,
|
||||||
) {
|
backgroundContent = {},
|
||||||
val elevation by animateDpAsState(
|
enableDismissFromStartToEnd = false,
|
||||||
if (isDragging) {
|
gesturesEnabled = editEnable,
|
||||||
8.dp // md.sys.elevation.level4
|
) {
|
||||||
} else {
|
val elevation by animateDpAsState(
|
||||||
1.dp // md.sys.elevation.level1
|
if (isDragging) {
|
||||||
},
|
8.dp // md.sys.elevation.level4
|
||||||
label = "elevation",
|
} else {
|
||||||
)
|
1.dp // md.sys.elevation.level1
|
||||||
ListItem(
|
},
|
||||||
modifier = Modifier.clickable {
|
label = "elevation",
|
||||||
switchLabel(item)
|
)
|
||||||
closeSheet()
|
ListItem(
|
||||||
},
|
modifier = Modifier.clickable {
|
||||||
tonalElevation = 1.dp,
|
switchLabel(item)
|
||||||
shadowElevation = elevation,
|
closeSheet()
|
||||||
headlineContent = {
|
},
|
||||||
Text("$item [${downloadsCount.getOrDefault(item, 0)}]")
|
tonalElevation = 1.dp,
|
||||||
},
|
shadowElevation = elevation,
|
||||||
trailingContent = {
|
headlineContent = {
|
||||||
Row {
|
val name = if (filterMode == DownloadsFilterMode.ARTIST) label.translateArtist() else label
|
||||||
IconButton(
|
Text("$name [${downloadsCount.getOrDefault(item, 0)}]")
|
||||||
onClick = {
|
},
|
||||||
launch {
|
trailingContent = editEnable.ifTrueThen {
|
||||||
val new = awaitInputText(initial = item, title = renameLabel, hint = labelsStr) { text ->
|
Row {
|
||||||
when {
|
IconButton(
|
||||||
text.isBlank() -> labelEmpty
|
onClick = {
|
||||||
text == defaultName -> defaultInvalid
|
launch {
|
||||||
DownloadManager.containLabel(text) -> labelExists
|
val new = awaitInputText(initial = item, title = renameLabel, hint = labelsStr) { text ->
|
||||||
else -> null
|
when {
|
||||||
}
|
text.isBlank() -> labelEmpty
|
||||||
}
|
text == defaultName -> defaultInvalid
|
||||||
DownloadManager.renameLabel(item, new)
|
DownloadManager.containLabel(text) -> labelExists
|
||||||
if (filterState.label == item) {
|
else -> null
|
||||||
switchLabel(new)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
DownloadManager.renameLabel(item, new)
|
||||||
) {
|
if (filterState.label == item) {
|
||||||
Icon(imageVector = Icons.Default.Edit, contentDescription = null)
|
switchLabel(new)
|
||||||
}
|
}
|
||||||
DragHandle(
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Default.Edit, contentDescription = null)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {},
|
||||||
|
modifier = Modifier.draggableHandle(
|
||||||
onDragStarted = {
|
onDragStarted = {
|
||||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.START)
|
||||||
fromIndex = index
|
fromIndex = index
|
||||||
},
|
},
|
||||||
onDragStopped = {
|
onDragStopped = {
|
||||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.END)
|
||||||
if (fromIndex != -1) {
|
if (fromIndex != -1) {
|
||||||
if (fromIndex != index) {
|
if (fromIndex != index) {
|
||||||
val range = if (fromIndex < index) fromIndex..index else index..fromIndex
|
val range = if (fromIndex < index) fromIndex..index else index..fromIndex
|
||||||
val toUpdate = labelList.slice(range)
|
val toUpdate = DownloadManager.labelList.slice(range)
|
||||||
toUpdate.zip(range).forEach { it.first.position = it.second }
|
toUpdate.zip(range).forEach { it.first.position = it.second }
|
||||||
launchIO { EhDB.updateDownloadLabel(toUpdate) }
|
launchIO { EhDB.updateDownloadLabel(toUpdate) }
|
||||||
}
|
}
|
||||||
fromIndex = -1
|
fromIndex = -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Default.Reorder, contentDescription = null)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
},
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,14 +438,15 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SearchBarScreen(
|
SearchBarScreen(
|
||||||
|
onApplySearch = { filterState = filterState.copy(keyword = it) },
|
||||||
|
expanded = searchBarExpanded,
|
||||||
|
onExpandedChange = {
|
||||||
|
searchBarExpanded = it
|
||||||
|
fabHidden = it
|
||||||
|
if (it) checkedInfoMap.clear()
|
||||||
|
},
|
||||||
title = title,
|
title = title,
|
||||||
searchFieldHint = hint,
|
searchFieldHint = hint,
|
||||||
onApplySearch = { filterState = filterState.copy(keyword = it) },
|
|
||||||
onSearchExpanded = {
|
|
||||||
checkedInfoMap.clear()
|
|
||||||
fabHidden = true
|
|
||||||
},
|
|
||||||
onSearchHidden = { fabHidden = false },
|
|
||||||
searchBarOffsetY = { searchBarOffsetY },
|
searchBarOffsetY = { searchBarOffsetY },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
@@ -396,9 +470,9 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
text = { Text(text = stringResource(id = R.string.download_start_all)) },
|
text = { Text(text = stringResource(id = R.string.download_start_all)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
val intent = Intent(activity, DownloadService::class.java)
|
val intent = Intent(implicit<Activity>(), DownloadService::class.java)
|
||||||
intent.action = DownloadService.ACTION_START_ALL
|
intent.action = DownloadService.ACTION_START_ALL
|
||||||
ContextCompat.startForegroundService(activity, intent)
|
ContextCompat.startForegroundService(implicit<Activity>(), intent)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
@@ -430,27 +504,19 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
val gidList = list.filter { it.state != DownloadInfo.STATE_FINISH }.asReversed().mapToLongArray(DownloadInfo::gid)
|
val gidList = list.filter { it.state != DownloadInfo.STATE_FINISH }.asReversed().mapToLongArray(DownloadInfo::gid)
|
||||||
val intent = Intent(activity, DownloadService::class.java)
|
val intent = Intent(implicit<Activity>(), DownloadService::class.java)
|
||||||
intent.action = DownloadService.ACTION_START_RANGE
|
intent.action = DownloadService.ACTION_START_RANGE
|
||||||
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
|
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
|
||||||
ContextCompat.startForegroundService(activity, intent)
|
ContextCompat.startForegroundService(implicit<Activity>(), intent)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
val height by collectListThumbSizeAsState()
|
val height by collectListThumbSizeAsState()
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val realPadding = contentPadding + PaddingValues(dimensionResource(id = R.dimen.gallery_list_margin_h), dimensionResource(id = R.dimen.gallery_list_margin_v))
|
||||||
val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h)
|
|
||||||
val marginV = dimensionResource(id = R.dimen.gallery_list_margin_v)
|
|
||||||
val realPadding = PaddingValues(
|
|
||||||
top = contentPadding.calculateTopPadding() + marginV,
|
|
||||||
bottom = contentPadding.calculateBottomPadding() + marginV,
|
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection) + marginH,
|
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection) + marginH,
|
|
||||||
)
|
|
||||||
val searchBarConnection = remember {
|
val searchBarConnection = remember {
|
||||||
val slop = ViewConfiguration.get(context).scaledTouchSlop
|
val slop = ViewConfiguration.get(implicit<Context>()).scaledTouchSlop
|
||||||
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
@@ -470,6 +536,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
launchIO { EhDB.putHistoryInfo(info.galleryInfo) }
|
launchIO { EhDB.putHistoryInfo(info.galleryInfo) }
|
||||||
navToReader(info.galleryInfo)
|
navToReader(info.galleryInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
Crossfade(targetState = gridView, label = "Downloads") { showGridView ->
|
Crossfade(targetState = gridView, label = "Downloads") { showGridView ->
|
||||||
if (showGridView) {
|
if (showGridView) {
|
||||||
val gridInterval = dimensionResource(R.dimen.gallery_grid_interval)
|
val gridInterval = dimensionResource(R.dimen.gallery_grid_interval)
|
||||||
@@ -481,13 +548,13 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(gridInterval),
|
horizontalArrangement = Arrangement.spacedBy(gridInterval),
|
||||||
contentPadding = realPadding,
|
contentPadding = realPadding,
|
||||||
) {
|
) {
|
||||||
items(list) { info ->
|
items(list, key = { it.gid }) { info ->
|
||||||
GalleryInfoGridItem(
|
GalleryInfoGridItem(
|
||||||
onClick = ::onItemClick.partially1(info),
|
onClick = ::onItemClick.partially1(info),
|
||||||
onLongClick = { navigator.navigate(info.galleryInfo.asDst()) },
|
onLongClick = { navigate(info.galleryInfo.asDst()) },
|
||||||
info = info,
|
info = info,
|
||||||
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
||||||
badgeText = info.pages.takeIf { it > 0 }?.toString(),
|
showLanguage = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -516,16 +583,16 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onThumbClick = {
|
onThumbClick = {
|
||||||
navigator.navigate(info.galleryInfo.asDst())
|
navigate(info.galleryInfo.asDst())
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
checkedInfoMap[info.gid] = info
|
checkedInfoMap[info.gid] = info
|
||||||
},
|
},
|
||||||
onStart = {
|
onStart = {
|
||||||
val intent = Intent(activity, DownloadService::class.java)
|
val intent = Intent(implicit<Activity>(), DownloadService::class.java)
|
||||||
intent.action = DownloadService.ACTION_START
|
intent.action = DownloadService.ACTION_START
|
||||||
intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo)
|
intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo)
|
||||||
ContextCompat.startForegroundService(activity, intent)
|
ContextCompat.startForegroundService(implicit<Activity>(), intent)
|
||||||
},
|
},
|
||||||
onStop = { launchIO { DownloadManager.stopDownload(info.gid) } },
|
onStop = { launchIO { DownloadManager.stopDownload(info.gid) } },
|
||||||
info = info,
|
info = info,
|
||||||
@@ -586,7 +653,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}
|
}
|
||||||
onClick(Icons.AutoMirrored.Default.Sort) {
|
onClick(Icons.AutoMirrored.Default.Sort) {
|
||||||
val oldMode = SortMode.from(sortMode)
|
val oldMode = SortMode.from(sortMode)
|
||||||
val (selected, checked) = showSelectItemWithCheckBox(
|
val (selected, checked) = awaitSelectItemWithCheckBox(
|
||||||
sortModes.toList(),
|
sortModes.toList(),
|
||||||
R.string.sort_by,
|
R.string.sort_by,
|
||||||
R.string.group_by_download_label,
|
R.string.group_by_download_label,
|
||||||
@@ -599,7 +666,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
invalidateKey = !invalidateKey
|
invalidateKey = !invalidateKey
|
||||||
}
|
}
|
||||||
onClick(Icons.Default.FilterList) {
|
onClick(Icons.Default.FilterList) {
|
||||||
val state = showSingleChoice(
|
val state = awaitSingleChoice(
|
||||||
downloadStates.toList(),
|
downloadStates.toList(),
|
||||||
filterState.state + 1,
|
filterState.state + 1,
|
||||||
R.string.download_filter,
|
R.string.download_filter,
|
||||||
@@ -607,31 +674,28 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
filterState = filterState.copy(state = state)
|
filterState = filterState.copy(state = state)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onClick(Icons.Default.DoneAll) {
|
onClick(Icons.Default.DoneAll, autoClose = false) {
|
||||||
val info = list.associateBy { it.gid }
|
val info = list.associateBy { it.gid }
|
||||||
checkedInfoMap.putAll(info)
|
checkedInfoMap.putAll(info)
|
||||||
throw CancellationException()
|
|
||||||
}
|
}
|
||||||
onClick(Icons.Default.PlayArrow) {
|
onClick(Icons.Default.PlayArrow) {
|
||||||
val gidList = checkedInfoMap.run { toMap().values.also { clear() } }
|
val gidList = checkedInfoMap.takeAndClear().mapToLongArray(DownloadInfo::gid)
|
||||||
.mapToLongArray(DownloadInfo::gid)
|
val intent = Intent(implicit<Activity>(), DownloadService::class.java)
|
||||||
val intent = Intent(activity, DownloadService::class.java)
|
|
||||||
intent.action = DownloadService.ACTION_START_RANGE
|
intent.action = DownloadService.ACTION_START_RANGE
|
||||||
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
|
intent.putExtra(DownloadService.KEY_GID_LIST, gidList)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(implicit<Context>(), intent)
|
||||||
}
|
}
|
||||||
onClick(Icons.Default.Pause) {
|
onClick(Icons.Default.Pause) {
|
||||||
val gidList = checkedInfoMap.run { toMap().values.also { clear() } }
|
val gidList = checkedInfoMap.takeAndClear().mapToLongArray(DownloadInfo::gid)
|
||||||
.mapToLongArray(DownloadInfo::gid)
|
|
||||||
DownloadManager.stopRangeDownload(gidList)
|
DownloadManager.stopRangeDownload(gidList)
|
||||||
}
|
}
|
||||||
onClick(Icons.Default.Delete) {
|
onClick(Icons.Default.Delete) {
|
||||||
val infoList = checkedInfoMap.run { toMap().values.also { clear() } }
|
val infoList = checkedInfoMap.takeAndClear()
|
||||||
confirmRemoveDownloadRange(infoList)
|
confirmRemoveDownloadRange(infoList)
|
||||||
list.removeAll(infoList)
|
list.removeAll(infoList)
|
||||||
}
|
}
|
||||||
onClick(Icons.AutoMirrored.Default.DriveFileMove) {
|
onClick(Icons.AutoMirrored.Default.DriveFileMove) {
|
||||||
val infoList = checkedInfoMap.run { toMap().values.also { clear() } }
|
val infoList = checkedInfoMap.takeAndClear()
|
||||||
val toLabel = showMoveDownloadLabelList(infoList)
|
val toLabel = showMoveDownloadLabelList(infoList)
|
||||||
with(filterState) {
|
with(filterState) {
|
||||||
if (label != "" && label != toLabel) {
|
if (label != "" && label != toLabel) {
|
||||||
@@ -642,9 +706,3 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object DownloadsFragment {
|
|
||||||
const val KEY_GID = "gid"
|
|
||||||
const val KEY_ACTION = "action"
|
|
||||||
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.hippo.ehviewer.ui.screen
|
package com.hippo.ehviewer.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.view.ViewConfiguration
|
import android.view.ViewConfiguration
|
||||||
|
import androidx.collection.MutableLongSet
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
@@ -26,7 +28,6 @@ import androidx.compose.material3.ListItem
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -43,54 +44,53 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.LoadState
|
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import androidx.paging.filter
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||||
import com.hippo.ehviewer.collectAsState
|
import com.hippo.ehviewer.collectAsState
|
||||||
import com.hippo.ehviewer.icons.EhIcons
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
import com.hippo.ehviewer.icons.filled.GoTo
|
import com.hippo.ehviewer.icons.filled.GoTo
|
||||||
|
import com.hippo.ehviewer.ui.DrawerHandle
|
||||||
import com.hippo.ehviewer.ui.LocalSideSheetState
|
import com.hippo.ehviewer.ui.LocalSideSheetState
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
import com.hippo.ehviewer.ui.awaitSelectDate
|
||||||
import com.hippo.ehviewer.ui.composing
|
import com.hippo.ehviewer.ui.composing
|
||||||
import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME
|
import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME
|
||||||
import com.hippo.ehviewer.ui.main.FabLayout
|
import com.hippo.ehviewer.ui.main.FabLayout
|
||||||
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
|
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
|
||||||
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
|
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
|
||||||
import com.hippo.ehviewer.ui.main.GalleryList
|
import com.hippo.ehviewer.ui.main.GalleryList
|
||||||
import com.hippo.ehviewer.ui.showDatePicker
|
|
||||||
import com.hippo.ehviewer.ui.startDownload
|
import com.hippo.ehviewer.ui.startDownload
|
||||||
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
||||||
import com.hippo.ehviewer.ui.tools.foldToLoadResult
|
import com.hippo.ehviewer.ui.tools.foldToLoadResult
|
||||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||||
import com.hippo.ehviewer.ui.tools.thenIf
|
import com.hippo.ehviewer.ui.tools.thenIf
|
||||||
import com.hippo.ehviewer.util.displayString
|
|
||||||
import com.hippo.ehviewer.util.mapToLongArray
|
import com.hippo.ehviewer.util.mapToLongArray
|
||||||
|
import com.hippo.ehviewer.util.takeAndClear
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.coroutines.onEachLatest
|
import moe.tarsin.coroutines.onEachLatest
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
import moe.tarsin.coroutines.runSwallowingWithUI
|
||||||
|
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
@@ -99,9 +99,11 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
val localFavName = stringResource(R.string.local_favorites)
|
val localFavName = stringResource(R.string.local_favorites)
|
||||||
val cloudFavName = stringResource(R.string.cloud_favorites)
|
val cloudFavName = stringResource(R.string.cloud_favorites)
|
||||||
val animateItems by Settings.animateItems.collectAsState()
|
val animateItems by Settings.animateItems.collectAsState()
|
||||||
|
val hasSignedIn by Settings.hasSignedIn.collectAsState()
|
||||||
|
|
||||||
// Meta State
|
// Meta State
|
||||||
var urlBuilder by rememberSaveable { mutableStateOf(FavListUrlBuilder(favCat = Settings.recentFavCat)) }
|
var urlBuilder by rememberSaveable { mutableStateOf(FavListUrlBuilder(favCat = Settings.recentFavCat)) }
|
||||||
|
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
// Derived State
|
// Derived State
|
||||||
@@ -110,7 +112,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
val favCatName = remember(urlBuilder) {
|
val favCatName = remember(urlBuilder) {
|
||||||
when (val favCat = urlBuilder.favCat) {
|
when (val favCat = urlBuilder.favCat) {
|
||||||
in 0..9 -> Settings.favCat[favCat]
|
in 0..9 -> Settings.favCat[favCat]
|
||||||
FavListUrlBuilder.FAV_CAT_LOCAL -> localFavName
|
FavListUrlBuilder.FAV_CAT_LOCAL -> localFavName.also { searchBarOffsetY = 0 }
|
||||||
else -> cloudFavName
|
else -> cloudFavName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +121,6 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
} else {
|
} else {
|
||||||
stringResource(R.string.favorites_title_2, favCatName, keyword)
|
stringResource(R.string.favorites_title_2, favCatName, keyword)
|
||||||
}
|
}
|
||||||
val context = LocalContext.current
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val localFavCountFlow = rememberInVM { EhDB.localFavCount }
|
val localFavCountFlow = rememberInVM { EhDB.localFavCount }
|
||||||
val searchBarHint = stringResource(R.string.search_bar_hint, favCatName)
|
val searchBarHint = stringResource(R.string.search_bar_hint, favCatName)
|
||||||
@@ -164,7 +165,14 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.flow.cachedIn(viewModelScope)
|
}.flow.map { pagingData ->
|
||||||
|
// https://github.com/FooIbar/EhViewer/issues/1190
|
||||||
|
// Workaround for duplicate items when sorting by favorited time
|
||||||
|
val gidSet = MutableLongSet(50)
|
||||||
|
pagingData.filter {
|
||||||
|
gidSet.add(it.gid)
|
||||||
|
}
|
||||||
|
}.cachedIn(viewModelScope)
|
||||||
}.collectAsLazyPagingItems()
|
}.collectAsLazyPagingItems()
|
||||||
|
|
||||||
fun refresh(newUrlBuilder: FavListUrlBuilder = urlBuilder.copy(jumpTo = null, prev = null, next = null)) {
|
fun refresh(newUrlBuilder: FavListUrlBuilder = urlBuilder.copy(jumpTo = null, prev = null, next = null)) {
|
||||||
@@ -185,7 +193,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val localFav = stringResource(id = R.string.local_favorites) to localFavCount
|
val localFav = stringResource(id = R.string.local_favorites) to localFavCount
|
||||||
val faves = if (EhCookieStore.hasSignedIn()) {
|
val faves = if (hasSignedIn) {
|
||||||
arrayOf(
|
arrayOf(
|
||||||
localFav,
|
localFav,
|
||||||
stringResource(id = R.string.cloud_favorites) to Settings.favCloudCount,
|
stringResource(id = R.string.cloud_favorites) to Settings.favCloudCount,
|
||||||
@@ -213,26 +221,22 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val refreshState = rememberPullToRefreshState {
|
|
||||||
data.loadState.refresh is LoadState.NotLoading
|
|
||||||
}
|
|
||||||
|
|
||||||
var fabExpanded by remember { mutableStateOf(false) }
|
var fabExpanded by remember { mutableStateOf(false) }
|
||||||
var fabHidden by remember { mutableStateOf(false) }
|
var fabHidden by remember { mutableStateOf(false) }
|
||||||
val checkedInfoMap = remember { mutableStateMapOf<Long, BaseGalleryInfo>() }
|
val checkedInfoMap = remember { mutableStateMapOf<Long, BaseGalleryInfo>() }
|
||||||
val selectMode = checkedInfoMap.isNotEmpty()
|
val selectMode = checkedInfoMap.isNotEmpty()
|
||||||
LockDrawer(selectMode)
|
DrawerHandle(!selectMode && !searchBarExpanded)
|
||||||
|
|
||||||
SearchBarScreen(
|
SearchBarScreen(
|
||||||
|
onApplySearch = { refresh(FavListUrlBuilder(urlBuilder.favCat, it)) },
|
||||||
|
expanded = searchBarExpanded,
|
||||||
|
onExpandedChange = {
|
||||||
|
searchBarExpanded = it
|
||||||
|
fabHidden = it
|
||||||
|
if (it) checkedInfoMap.clear()
|
||||||
|
},
|
||||||
title = title,
|
title = title,
|
||||||
searchFieldHint = searchBarHint,
|
searchFieldHint = searchBarHint,
|
||||||
onApplySearch = { refresh(FavListUrlBuilder(urlBuilder.favCat, it)) },
|
|
||||||
onSearchExpanded = {
|
|
||||||
checkedInfoMap.clear()
|
|
||||||
fabHidden = true
|
|
||||||
},
|
|
||||||
onSearchHidden = { fabHidden = false },
|
|
||||||
refreshState = refreshState,
|
|
||||||
tagNamespace = !isLocalFav,
|
tagNamespace = !isLocalFav,
|
||||||
searchBarOffsetY = { searchBarOffsetY },
|
searchBarOffsetY = { searchBarOffsetY },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
@@ -246,7 +250,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
val height by collectListThumbSizeAsState()
|
val height by collectListThumbSizeAsState()
|
||||||
val showPages by Settings.showGalleryPages.collectAsState()
|
val showPages by Settings.showGalleryPages.collectAsState()
|
||||||
val searchBarConnection = remember {
|
val searchBarConnection = remember {
|
||||||
val slop = ViewConfiguration.get(context).scaledTouchSlop
|
val slop = ViewConfiguration.get(implicit<Context>()).scaledTouchSlop
|
||||||
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
@@ -281,7 +285,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
checkedInfoMap[info.gid] = info
|
checkedInfoMap[info.gid] = info
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navigator.navigate(info.asDst())
|
navigate(info.asDst())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
@@ -310,18 +314,19 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
checkedInfoMap[info.gid] = info
|
checkedInfoMap[info.gid] = info
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navigator.navigate(info.asDst())
|
navigate(info.asDst())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
checkedInfoMap[info.gid] = info
|
checkedInfoMap[info.gid] = info
|
||||||
},
|
},
|
||||||
info = info,
|
info = info,
|
||||||
|
showPages = showPages,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshState = refreshState,
|
searchBarOffsetY = { searchBarOffsetY },
|
||||||
scrollToTopOnRefresh = urlBuilder.favCat != FavListUrlBuilder.FAV_CAT_LOCAL,
|
scrollToTopOnRefresh = urlBuilder.favCat != FavListUrlBuilder.FAV_CAT_LOCAL,
|
||||||
onRefresh = { refresh() },
|
onRefresh = { refresh() },
|
||||||
onLoading = { searchBarOffsetY = 0 },
|
onLoading = { searchBarOffsetY = 0 },
|
||||||
@@ -351,16 +356,12 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
if (isLocalFav) {
|
if (isLocalFav) {
|
||||||
onClick(Icons.Default.Shuffle) {
|
onClick(Icons.Default.Shuffle) {
|
||||||
val random = EhDB.randomLocalFav()
|
val random = EhDB.randomLocalFav()
|
||||||
withUIContext {
|
withUIContext { navigate(random.asDst()) }
|
||||||
navigator.navigate(random.asDst())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClick(EhIcons.Default.GoTo) {
|
onClick(EhIcons.Default.GoTo) {
|
||||||
launch {
|
val date = awaitSelectDate()
|
||||||
val date = showDatePicker()
|
refresh(urlBuilder.copy(jumpTo = date))
|
||||||
refresh(urlBuilder.copy(jumpTo = date))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onClick(Icons.Default.Refresh) {
|
onClick(Icons.Default.Refresh) {
|
||||||
refresh()
|
refresh()
|
||||||
@@ -369,43 +370,47 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
refresh(urlBuilder.copy(jumpTo = null, prev = "1-0", next = null))
|
refresh(urlBuilder.copy(jumpTo = null, prev = "1-0", next = null))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onClick(Icons.Default.DoneAll) {
|
onClick(Icons.Default.DoneAll, autoClose = false) {
|
||||||
val info = data.itemSnapshotList.items.associateBy { it.gid }
|
val info = data.itemSnapshotList.items.associateBy { it.gid }
|
||||||
checkedInfoMap.putAll(info)
|
checkedInfoMap.putAll(info)
|
||||||
throw CancellationException()
|
|
||||||
}
|
}
|
||||||
onClick(Icons.Default.Download) {
|
onClick(Icons.Default.Download) {
|
||||||
val info = checkedInfoMap.run { toMap().values.also { clear() } }
|
val info = checkedInfoMap.takeAndClear()
|
||||||
startDownload(context, false, *info.toTypedArray())
|
runSwallowingWithUI {
|
||||||
|
startDownload(implicit<Context>(), false, *info.toTypedArray())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onClick(Icons.Default.Delete) {
|
onClick(Icons.Default.Delete) {
|
||||||
val info = checkedInfoMap.run { toMap().values.also { clear() } }
|
val info = checkedInfoMap.takeAndClear()
|
||||||
awaitPermissionOrCancel(title = R.string.delete_favorites_dialog_title) {
|
awaitPermissionOrCancel(title = R.string.delete_favorites_dialog_title) {
|
||||||
Text(text = stringResource(R.string.delete_favorites_dialog_message, info.size))
|
Text(text = stringResource(R.string.delete_favorites_dialog_message, info.size))
|
||||||
}
|
}
|
||||||
val srcCat = urlBuilder.favCat
|
val srcCat = urlBuilder.favCat
|
||||||
if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Delete local fav
|
runSwallowingWithUI {
|
||||||
EhDB.removeLocalFavorites(info)
|
if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Delete local fav
|
||||||
} else {
|
EhDB.removeLocalFavorites(info)
|
||||||
val delList = info.mapToLongArray(BaseGalleryInfo::gid)
|
} else {
|
||||||
EhEngine.modifyFavorites(delList, srcCat, -1)
|
val delList = info.mapToLongArray(BaseGalleryInfo::gid)
|
||||||
|
EhEngine.modifyFavorites(delList, srcCat, -1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// We refresh anyway as cloud data maybe partially modified
|
||||||
data.refresh()
|
data.refresh()
|
||||||
}
|
}
|
||||||
onClick(Icons.AutoMirrored.Default.DriveFileMove) {
|
onClick(Icons.AutoMirrored.Default.DriveFileMove) {
|
||||||
// First is local favorite, the other 10 is cloud favorite
|
// First is local favorite, the other 10 is cloud favorite
|
||||||
val items = buildList {
|
val items = buildList {
|
||||||
add(localFavName)
|
add(localFavName)
|
||||||
if (EhCookieStore.hasSignedIn()) {
|
if (hasSignedIn) {
|
||||||
addAll(Settings.favCat)
|
addAll(Settings.favCat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val index = showSelectItem(items, R.string.move_favorites_dialog_title)
|
val index = awaitSelectItem(items, R.string.move_favorites_dialog_title)
|
||||||
val srcCat = urlBuilder.favCat
|
val srcCat = urlBuilder.favCat
|
||||||
val dstCat = if (index == 0) FavListUrlBuilder.FAV_CAT_LOCAL else index - 1
|
val dstCat = if (index == 0) FavListUrlBuilder.FAV_CAT_LOCAL else index - 1
|
||||||
val info = checkedInfoMap.run { toMap().values.also { clear() } }
|
val info = checkedInfoMap.takeAndClear()
|
||||||
if (srcCat != dstCat) {
|
if (srcCat != dstCat) {
|
||||||
runSuspendCatching {
|
runSwallowingWithUI {
|
||||||
if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
|
if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
|
||||||
// Move from local to cloud
|
// Move from local to cloud
|
||||||
val galleryList = info.map { it.gid to it.token }
|
val galleryList = info.map { it.gid to it.token }
|
||||||
@@ -419,11 +424,9 @@ fun FavouritesScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
val gidArray = info.mapToLongArray(BaseGalleryInfo::gid)
|
val gidArray = info.mapToLongArray(BaseGalleryInfo::gid)
|
||||||
EhEngine.modifyFavorites(gidArray, srcCat, dstCat)
|
EhEngine.modifyFavorites(gidArray, srcCat, dstCat)
|
||||||
}
|
}
|
||||||
}.onSuccess {
|
|
||||||
data.refresh()
|
|
||||||
}.onFailure {
|
|
||||||
showSnackbar(it.displayString())
|
|
||||||
}
|
}
|
||||||
|
// We refresh anyway as cloud data maybe partially modified
|
||||||
|
data.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -41,8 +40,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -76,7 +74,6 @@ import androidx.core.text.parseAsHtml
|
|||||||
import com.hippo.ehviewer.EhApplication
|
import com.hippo.ehviewer.EhApplication
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.client.EhFilter.remember
|
import com.hippo.ehviewer.client.EhFilter.remember
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
@@ -85,11 +82,11 @@ import com.hippo.ehviewer.client.data.ListUrlBuilder
|
|||||||
import com.hippo.ehviewer.collectAsState
|
import com.hippo.ehviewer.collectAsState
|
||||||
import com.hippo.ehviewer.dao.Filter
|
import com.hippo.ehviewer.dao.Filter
|
||||||
import com.hippo.ehviewer.dao.FilterMode
|
import com.hippo.ehviewer.dao.FilterMode
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.ui.composing
|
import com.hippo.ehviewer.ui.composing
|
||||||
import com.hippo.ehviewer.ui.jumpToReaderByPage
|
import com.hippo.ehviewer.ui.jumpToReaderByPage
|
||||||
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
|
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
|
||||||
import com.hippo.ehviewer.ui.main.GalleryCommentCard
|
import com.hippo.ehviewer.ui.main.GalleryCommentCard
|
||||||
|
import com.hippo.ehviewer.ui.main.plus
|
||||||
import com.hippo.ehviewer.ui.openBrowser
|
import com.hippo.ehviewer.ui.openBrowser
|
||||||
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
|
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
|
||||||
import com.hippo.ehviewer.ui.tools.normalizeSpan
|
import com.hippo.ehviewer.ui.tools.normalizeSpan
|
||||||
@@ -106,7 +103,6 @@ import com.ramcosta.composedestinations.annotation.Destination
|
|||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -152,7 +148,6 @@ private val MinimumContentPaddingEditText = 88.dp
|
|||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composing(navigator) {
|
fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composing(navigator) {
|
||||||
LockDrawer(true)
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
var commenting by rememberSaveable { mutableStateOf(false) }
|
var commenting by rememberSaveable { mutableStateOf(false) }
|
||||||
val animationProgress by animateFloatMergePredictiveBackAsState(enable = commenting) { commenting = false }
|
val animationProgress by animateFloatMergePredictiveBackAsState(enable = commenting) { commenting = false }
|
||||||
@@ -246,7 +241,7 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val hasSignedIn = remember { EhCookieStore.hasSignedIn() }
|
val hasSignedIn by Settings.hasSignedIn.collectAsState()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -279,20 +274,28 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
|||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
val keylineMargin = dimensionResource(id = R.dimen.keyline_margin)
|
val keylineMargin = dimensionResource(id = R.dimen.keyline_margin)
|
||||||
var editTextMeasured by remember { mutableStateOf(MinimumContentPaddingEditText) }
|
var editTextMeasured by remember { mutableStateOf(MinimumContentPaddingEditText) }
|
||||||
val refreshState = rememberPullToRefreshState()
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
if (refreshState.isRefreshing) {
|
PullToRefreshBox(
|
||||||
LaunchedEffect(Unit) {
|
isRefreshing = isRefreshing,
|
||||||
runSuspendCatching {
|
onRefresh = {
|
||||||
withIOContext { refreshComment(true) }
|
isRefreshing = true
|
||||||
|
launchIO {
|
||||||
|
runSuspendCatching {
|
||||||
|
refreshComment(true)
|
||||||
|
}
|
||||||
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
refreshState.endRefresh()
|
},
|
||||||
}
|
modifier = Modifier.fillMaxSize().imePadding(),
|
||||||
}
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize().imePadding().nestedScroll(refreshState.nestedScrollConnection)) {
|
|
||||||
val additionalPadding = if (commenting) {
|
val additionalPadding = if (commenting) {
|
||||||
editTextMeasured
|
editTextMeasured
|
||||||
} else {
|
} else {
|
||||||
0.dp
|
if (!comments.hasMore) {
|
||||||
|
MinimumContentPaddingEditText
|
||||||
|
} else {
|
||||||
|
0.dp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val voteUpSucceed = stringResource(R.string.vote_up_successfully)
|
val voteUpSucceed = stringResource(R.string.vote_up_successfully)
|
||||||
val cancelVoteUpSucceed = stringResource(R.string.cancel_vote_up_successfully)
|
val cancelVoteUpSucceed = stringResource(R.string.cancel_vote_up_successfully)
|
||||||
@@ -302,14 +305,8 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.padding(horizontal = keylineMargin),
|
modifier = Modifier.padding(horizontal = keylineMargin),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
contentPadding = PaddingValues(
|
contentPadding = paddingValues + PaddingValues(bottom = additionalPadding),
|
||||||
// top = paddingValues.calculateTopPadding(),
|
|
||||||
bottom = paddingValues.calculateBottomPadding() + additionalPadding,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding()))
|
|
||||||
}
|
|
||||||
items(
|
items(
|
||||||
items = comments.comments,
|
items = comments.comments,
|
||||||
key = { it.id },
|
key = { it.id },
|
||||||
@@ -332,7 +329,7 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun doCommentAction(comment: GalleryComment) = showSelectActions {
|
suspend fun doCommentAction(comment: GalleryComment) = awaitSelectAction {
|
||||||
onSelect(copyComment) {
|
onSelect(copyComment) {
|
||||||
addTextToClipboard(comment.comment.parseAsHtml())
|
addTextToClipboard(comment.comment.parseAsHtml())
|
||||||
}
|
}
|
||||||
@@ -361,7 +358,7 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
|||||||
showCommentVoteStatus(comment)
|
showCommentVoteStatus(comment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
|
|
||||||
GalleryCommentCard(
|
GalleryCommentCard(
|
||||||
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
||||||
@@ -406,18 +403,8 @@ fun GalleryCommentsScreen(gid: Long, navigator: DestinationsNavigator) = composi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Workaround for comment list lagging when reaching the bottom
|
|
||||||
// https://github.com/FooIbar/EhViewer/issues/994
|
|
||||||
item {
|
|
||||||
Spacer(modifier = Modifier.height(MinimumContentPaddingEditText))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PullToRefreshContainer(
|
|
||||||
state = refreshState,
|
|
||||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = paddingValues.calculateTopPadding()),
|
|
||||||
)
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.align(Alignment.BottomCenter).layout { measurable, constraints ->
|
modifier = Modifier.align(Alignment.BottomCenter).layout { measurable, constraints ->
|
||||||
val origin = measurable.measure(constraints)
|
val origin = measurable.measure(constraints)
|
||||||
|
|||||||
@@ -56,13 +56,10 @@ import androidx.compose.material3.SnackbarResult
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
|
||||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -78,8 +75,9 @@ import androidx.compose.ui.res.dimensionResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.core.text.parseAsHtml
|
import androidx.core.text.parseAsHtml
|
||||||
import arrow.core.left
|
import androidx.window.core.layout.WindowWidthSizeClass
|
||||||
import arrow.core.partially1
|
import arrow.core.partially1
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
|
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
|
||||||
@@ -105,10 +103,6 @@ import com.hippo.ehviewer.client.data.findBaseInfo
|
|||||||
import com.hippo.ehviewer.client.exception.EhException
|
import com.hippo.ehviewer.client.exception.EhException
|
||||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||||
import com.hippo.ehviewer.client.getImageKey
|
import com.hippo.ehviewer.client.getImageKey
|
||||||
import com.hippo.ehviewer.client.parser.ArchiveParser
|
|
||||||
import com.hippo.ehviewer.client.parser.HomeParser
|
|
||||||
import com.hippo.ehviewer.client.parser.TorrentResult
|
|
||||||
import com.hippo.ehviewer.client.parser.format
|
|
||||||
import com.hippo.ehviewer.coil.justDownload
|
import com.hippo.ehviewer.coil.justDownload
|
||||||
import com.hippo.ehviewer.collectAsState
|
import com.hippo.ehviewer.collectAsState
|
||||||
import com.hippo.ehviewer.dao.DownloadInfo
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
@@ -118,7 +112,6 @@ import com.hippo.ehviewer.download.DownloadManager as EhDownloadManager
|
|||||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||||
import com.hippo.ehviewer.spider.SpiderDen
|
import com.hippo.ehviewer.spider.SpiderDen
|
||||||
import com.hippo.ehviewer.ui.GalleryInfoBottomSheet
|
import com.hippo.ehviewer.ui.GalleryInfoBottomSheet
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.ui.MainActivity
|
import com.hippo.ehviewer.ui.MainActivity
|
||||||
import com.hippo.ehviewer.ui.composing
|
import com.hippo.ehviewer.ui.composing
|
||||||
import com.hippo.ehviewer.ui.confirmRemoveDownload
|
import com.hippo.ehviewer.ui.confirmRemoveDownload
|
||||||
@@ -127,11 +120,13 @@ import com.hippo.ehviewer.ui.destinations.GalleryPreviewScreenDestination
|
|||||||
import com.hippo.ehviewer.ui.getFavoriteIcon
|
import com.hippo.ehviewer.ui.getFavoriteIcon
|
||||||
import com.hippo.ehviewer.ui.jumpToReaderByPage
|
import com.hippo.ehviewer.ui.jumpToReaderByPage
|
||||||
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
|
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
|
||||||
|
import com.hippo.ehviewer.ui.main.ArchiveList
|
||||||
import com.hippo.ehviewer.ui.main.EhPreviewItem
|
import com.hippo.ehviewer.ui.main.EhPreviewItem
|
||||||
import com.hippo.ehviewer.ui.main.GalleryCommentCard
|
import com.hippo.ehviewer.ui.main.GalleryCommentCard
|
||||||
import com.hippo.ehviewer.ui.main.GalleryDetailErrorTip
|
import com.hippo.ehviewer.ui.main.GalleryDetailErrorTip
|
||||||
import com.hippo.ehviewer.ui.main.GalleryDetailHeaderCard
|
import com.hippo.ehviewer.ui.main.GalleryDetailHeaderCard
|
||||||
import com.hippo.ehviewer.ui.main.GalleryTags
|
import com.hippo.ehviewer.ui.main.GalleryTags
|
||||||
|
import com.hippo.ehviewer.ui.main.TorrentList
|
||||||
import com.hippo.ehviewer.ui.modifyFavorites
|
import com.hippo.ehviewer.ui.modifyFavorites
|
||||||
import com.hippo.ehviewer.ui.navToReader
|
import com.hippo.ehviewer.ui.navToReader
|
||||||
import com.hippo.ehviewer.ui.openBrowser
|
import com.hippo.ehviewer.ui.openBrowser
|
||||||
@@ -141,6 +136,7 @@ import com.hippo.ehviewer.ui.tools.FilledTertiaryIconButton
|
|||||||
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconToggleButton
|
import com.hippo.ehviewer.ui.tools.FilledTertiaryIconToggleButton
|
||||||
import com.hippo.ehviewer.ui.tools.GalleryDetailRating
|
import com.hippo.ehviewer.ui.tools.GalleryDetailRating
|
||||||
import com.hippo.ehviewer.ui.tools.GalleryRatingBar
|
import com.hippo.ehviewer.ui.tools.GalleryRatingBar
|
||||||
|
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
|
||||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||||
import com.hippo.ehviewer.ui.tools.rememberLambda
|
import com.hippo.ehviewer.ui.tools.rememberLambda
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
@@ -164,6 +160,10 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
|
|||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
@@ -214,7 +214,6 @@ private fun List<GalleryTagGroup>.getArtistTag(): String? {
|
|||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNavigator) = composing(navigator) {
|
fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNavigator) = composing(navigator) {
|
||||||
LockDrawer(true)
|
|
||||||
var galleryInfo by remember {
|
var galleryInfo by remember {
|
||||||
val casted = args as? GalleryInfoArgs
|
val casted = args as? GalleryInfoArgs
|
||||||
mutableStateOf<GalleryInfo?>(casted?.galleryInfo)
|
mutableStateOf<GalleryInfo?>(casted?.galleryInfo)
|
||||||
@@ -293,13 +292,13 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mArchiveFormParamOr by remember { mutableStateOf<String?>(null) }
|
val archiveResult = remember(galleryInfo) {
|
||||||
var mArchiveList by remember { mutableStateOf<List<ArchiveParser.Archive>?>(null) }
|
async(Dispatchers.IO + Job(), CoroutineStart.LAZY) {
|
||||||
var mCurrentFunds by remember { mutableStateOf<HomeParser.Funds?>(null) }
|
val detail = galleryInfo as GalleryDetail
|
||||||
|
EhEngine.getArchiveList(detail.archiveUrl!!, gid, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val archiveFree = stringResource(R.string.archive_free)
|
|
||||||
val archiveOriginal = stringResource(R.string.archive_original)
|
|
||||||
val archiveResample = stringResource(R.string.archive_resample)
|
|
||||||
val failureNoHath = stringResource(R.string.download_archive_failure_no_hath)
|
val failureNoHath = stringResource(R.string.download_archive_failure_no_hath)
|
||||||
val noArchive = stringResource(R.string.no_archives)
|
val noArchive = stringResource(R.string.no_archives)
|
||||||
val downloadStarted = stringResource(R.string.download_archive_started)
|
val downloadStarted = stringResource(R.string.download_archive_started)
|
||||||
@@ -311,42 +310,19 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
if (galleryDetail.apiUid < 0) {
|
if (galleryDetail.apiUid < 0) {
|
||||||
showSnackbar(signInFirst)
|
showSnackbar(signInFirst)
|
||||||
} else {
|
} else {
|
||||||
Unit.runSuspendCatching {
|
runSuspendCatching {
|
||||||
if (mArchiveList == null) {
|
val (paramOr, archiveList, funds) = bgWork { archiveResult.await() }
|
||||||
val result = bgWork {
|
if (archiveList.isEmpty()) {
|
||||||
withIOContext {
|
|
||||||
EhEngine.getArchiveList(galleryDetail.archiveUrl!!, gid, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mArchiveFormParamOr = result.paramOr
|
|
||||||
mArchiveList = result.archiveList
|
|
||||||
mCurrentFunds = result.funds
|
|
||||||
}
|
|
||||||
if (mArchiveList!!.isEmpty()) {
|
|
||||||
showSnackbar(noArchive)
|
showSnackbar(noArchive)
|
||||||
} else {
|
} else {
|
||||||
val items = mArchiveList!!.map {
|
val selected = showNoButton {
|
||||||
it.run {
|
ArchiveList(
|
||||||
if (isHAtH) {
|
funds = funds,
|
||||||
val costStr = if (cost == "Free") archiveFree else cost
|
items = archiveList,
|
||||||
"[H@H] $name [$size] [$costStr]"
|
onItemClick = { dismissWith(it) },
|
||||||
} else {
|
)
|
||||||
val nameStr = if (res == "org") archiveOriginal else archiveResample
|
|
||||||
val costStr = if (cost == "Free!") archiveFree else cost
|
|
||||||
"$nameStr [$size] [$costStr]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var fundsGP = mCurrentFunds!!.fundsGP.toString()
|
EhEngine.downloadArchive(gid, token, paramOr, selected.res, selected.isHAtH)?.let {
|
||||||
// Ex GP numbers are rounded down to the nearest thousand
|
|
||||||
if (EhUtils.isExHentai) {
|
|
||||||
fundsGP += "+"
|
|
||||||
}
|
|
||||||
val title = getString(R.string.current_funds, fundsGP, mCurrentFunds!!.fundsC)
|
|
||||||
val selected = showSelectItem(items, title.left())
|
|
||||||
val res = mArchiveList!![selected].res
|
|
||||||
val isHAtH = mArchiveList!![selected].isHAtH
|
|
||||||
EhEngine.downloadArchive(gid, token, mArchiveFormParamOr!!, res, isHAtH)?.let {
|
|
||||||
val uri = Uri.parse(it)
|
val uri = Uri.parse(it)
|
||||||
val intent = Intent().apply {
|
val intent = Intent().apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
@@ -477,7 +453,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
val items = galleryDetail.newerVersions.map {
|
val items = galleryDetail.newerVersions.map {
|
||||||
getString(R.string.newer_version_title, it.title, it.posted)
|
getString(R.string.newer_version_title, it.title, it.posted)
|
||||||
}
|
}
|
||||||
val selected = showSelectItem(items)
|
val selected = awaitSelectItem(items)
|
||||||
val info = galleryDetail.newerVersions[selected]
|
val info = galleryDetail.newerVersions[selected]
|
||||||
withUIContext {
|
withUIContext {
|
||||||
// Can't use GalleryInfoArgs as thumbKey is null
|
// Can't use GalleryInfoArgs as thumbKey is null
|
||||||
@@ -583,29 +559,30 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val torrentText = stringResource(R.string.torrents)
|
val torrentText = stringResource(R.string.torrent_count, galleryDetail.torrentCount)
|
||||||
val permissionDenied = stringResource(R.string.permission_denied)
|
val permissionDenied = stringResource(R.string.permission_denied)
|
||||||
val downloadTorrentFailed = stringResource(R.string.download_torrent_failure)
|
val downloadTorrentFailed = stringResource(R.string.download_torrent_failure)
|
||||||
val downloadTorrentStarted = stringResource(R.string.download_torrent_started)
|
val downloadTorrentStarted = stringResource(R.string.download_torrent_started)
|
||||||
val noTorrents = stringResource(R.string.no_torrents)
|
val noTorrents = stringResource(R.string.no_torrents)
|
||||||
val noCurrentTorrents = stringResource(R.string.no_current_torrents)
|
val torrentResult = remember(galleryDetail) {
|
||||||
var mTorrentList by remember { mutableStateOf<TorrentResult?>(null) }
|
async(Dispatchers.IO + Job(), CoroutineStart.LAZY) {
|
||||||
suspend fun showTorrentDialog() {
|
EhEngine.getTorrentList(galleryDetail.torrentUrl!!, gid, token)
|
||||||
if (mTorrentList == null) {
|
|
||||||
mTorrentList = bgWork {
|
|
||||||
withIOContext {
|
|
||||||
EhEngine.getTorrentList(galleryDetail.torrentUrl!!, gid, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (mTorrentList!!.isEmpty()) {
|
}
|
||||||
showSnackbar(noCurrentTorrents)
|
suspend fun showTorrentDialog() {
|
||||||
|
val torrentList = bgWork { torrentResult.await() }
|
||||||
|
if (torrentList.isEmpty()) {
|
||||||
|
showSnackbar(noTorrents)
|
||||||
} else {
|
} else {
|
||||||
val items = mTorrentList!!.map { it.format() }
|
val selected = showNoButton(false) {
|
||||||
val selected = showSelectItem(items, R.string.torrents, respectDefaultWidth = false)
|
TorrentList(
|
||||||
val url = mTorrentList!![selected].url
|
items = torrentList,
|
||||||
val name = "${mTorrentList!![selected].name}.torrent"
|
onItemClick = { dismissWith(it) },
|
||||||
val r = DownloadManager.Request(Uri.parse(url))
|
)
|
||||||
|
}
|
||||||
|
val url = selected.url
|
||||||
|
val name = "${selected.name}.torrent"
|
||||||
|
val r = DownloadManager.Request(url.toUri())
|
||||||
r.setDestinationInExternalPublicDir(
|
r.setDestinationInExternalPublicDir(
|
||||||
Environment.DIRECTORY_DOWNLOADS,
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
AppConfig.APP_DIRNAME + "/" + FileUtils.sanitizeFilename(name),
|
AppConfig.APP_DIRNAME + "/" + FileUtils.sanitizeFilename(name),
|
||||||
@@ -660,23 +637,22 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
showSnackbar(signInFirst)
|
showSnackbar(signInFirst)
|
||||||
return@launchIO
|
return@launchIO
|
||||||
}
|
}
|
||||||
awaitPermissionOrCancel(title = R.string.rate) {
|
val pendingRating = awaitResult(galleryDetail.rating.coerceAtLeast(.5f), title = R.string.rate) {
|
||||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
var rating by remember { mutableFloatStateOf(galleryDetail.rating.coerceAtLeast(.5f)) }
|
var text by remember { mutableIntStateOf(getRatingText(expectedValue)) }
|
||||||
var text by remember { mutableIntStateOf(getRatingText(rating)) }
|
|
||||||
Text(text = stringResource(id = text), style = MaterialTheme.typography.bodyLarge)
|
Text(text = stringResource(id = text), style = MaterialTheme.typography.bodyLarge)
|
||||||
Spacer(modifier = Modifier.size(keylineMargin))
|
Spacer(modifier = Modifier.size(keylineMargin))
|
||||||
GalleryRatingBar(
|
GalleryRatingBar(
|
||||||
rating = rating,
|
rating = expectedValue,
|
||||||
onRatingChange = {
|
onRatingChange = {
|
||||||
rating = it.coerceAtLeast(.5f)
|
expectedValue = it.coerceAtLeast(.5f)
|
||||||
text = getRatingText(rating)
|
text = getRatingText(expectedValue)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
galleryDetail.runSuspendCatching {
|
galleryDetail.runSuspendCatching {
|
||||||
EhEngine.rateGallery(apiUid, apiKey, gid, token, rating)
|
EhEngine.rateGallery(apiUid, apiKey, gid, token, pendingRating)
|
||||||
}.onSuccess { result ->
|
}.onSuccess { result ->
|
||||||
galleryInfo = galleryDetail.apply {
|
galleryInfo = galleryDetail.apply {
|
||||||
rating = result.rating
|
rating = result.rating
|
||||||
@@ -733,7 +709,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
tag
|
tag
|
||||||
}
|
}
|
||||||
launchIO {
|
launchIO {
|
||||||
showSelectActions {
|
awaitSelectAction {
|
||||||
with(activity) {
|
with(activity) {
|
||||||
onSelect(copy) {
|
onSelect(copy) {
|
||||||
addTextToClipboard(tag)
|
addTextToClipboard(tag)
|
||||||
@@ -756,7 +732,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
onSelect(downTag) { galleryDetail.voteTag(tag, -1) }
|
onSelect(downTag) { galleryDetail.voteTag(tag, -1) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -775,7 +751,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
val galleryDetail = galleryInfo.asGalleryDetail()
|
val galleryDetail = galleryInfo.asGalleryDetail()
|
||||||
val windowSizeClass = calculateWindowSizeClass(activity)
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
val thumbColumns by Settings.thumbColumns.collectAsState()
|
val thumbColumns by Settings.thumbColumns.collectAsState()
|
||||||
val readText = stringResource(R.string.read)
|
val readText = stringResource(R.string.read)
|
||||||
val startPage by rememberInVM {
|
val startPage by rememberInVM {
|
||||||
@@ -822,7 +798,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
if (showBottomSheet && galleryDetail != null) {
|
if (showBottomSheet && galleryDetail != null) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { showBottomSheet = false },
|
onDismissRequest = { showBottomSheet = false },
|
||||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
|
||||||
) {
|
) {
|
||||||
GalleryInfoBottomSheet(galleryDetail, navigator)
|
GalleryInfoBottomSheet(galleryDetail, navigator)
|
||||||
}
|
}
|
||||||
@@ -856,8 +832,8 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (windowSizeClass.widthSizeClass) {
|
when (windowSizeClass.windowWidthSizeClass) {
|
||||||
WindowWidthSizeClass.Medium, WindowWidthSizeClass.Compact -> LazyVerticalGrid(
|
WindowWidthSizeClass.MEDIUM, WindowWidthSizeClass.COMPACT -> LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(thumbColumns),
|
columns = GridCells.Fixed(thumbColumns),
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
modifier = modifier.padding(horizontal = keylineMargin),
|
modifier = modifier.padding(horizontal = keylineMargin),
|
||||||
@@ -908,7 +884,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowWidthSizeClass.Expanded -> LazyVerticalGrid(
|
WindowWidthSizeClass.EXPANDED -> LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(thumbColumns),
|
columns = GridCells.Fixed(thumbColumns),
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
modifier = modifier.padding(horizontal = keylineMargin),
|
modifier = modifier.padding(horizontal = keylineMargin),
|
||||||
@@ -1098,7 +1074,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa
|
|||||||
} else {
|
} else {
|
||||||
val info = galleryInfo!!
|
val info = galleryInfo!!
|
||||||
val uri = awaitActivityResult(
|
val uri = awaitActivityResult(
|
||||||
CreateDocument("application/x-cbz"),
|
CreateDocument("application/vnd.comicbook+zip"),
|
||||||
EhUtils.getSuitableTitle(info) + ".cbz",
|
EhUtils.getSuitableTitle(info) + ".cbz",
|
||||||
)
|
)
|
||||||
val dirname = downloadInfo?.dirname
|
val dirname = downloadInfo?.dirname
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.hippo.ehviewer.ui.screen
|
package com.hippo.ehviewer.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.ViewConfiguration
|
import android.view.ViewConfiguration
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -10,12 +11,16 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
@@ -28,21 +33,23 @@ import androidx.compose.material.icons.automirrored.filled.Help
|
|||||||
import androidx.compose.material.icons.automirrored.filled.LastPage
|
import androidx.compose.material.icons.automirrored.filled.LastPage
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.Reorder
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Shuffle
|
import androidx.compose.material.icons.filled.Shuffle
|
||||||
import androidx.compose.material.icons.outlined.Bookmarks
|
import androidx.compose.material.icons.outlined.Bookmarks
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
||||||
import androidx.compose.material3.SwipeToDismissBoxState
|
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
import androidx.compose.material3.fork.SwipeToDismissBox
|
||||||
|
import androidx.compose.material3.fork.SwipeToDismissBoxState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
@@ -65,7 +72,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -75,7 +81,6 @@ import androidx.compose.ui.util.fastAny
|
|||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import androidx.compose.ui.util.lerp
|
import androidx.compose.ui.util.lerp
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.LoadState
|
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
@@ -104,7 +109,9 @@ import com.hippo.ehviewer.collectAsState
|
|||||||
import com.hippo.ehviewer.dao.QuickSearch
|
import com.hippo.ehviewer.dao.QuickSearch
|
||||||
import com.hippo.ehviewer.icons.EhIcons
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
import com.hippo.ehviewer.icons.filled.GoTo
|
import com.hippo.ehviewer.icons.filled.GoTo
|
||||||
|
import com.hippo.ehviewer.ui.DrawerHandle
|
||||||
import com.hippo.ehviewer.ui.LocalSideSheetState
|
import com.hippo.ehviewer.ui.LocalSideSheetState
|
||||||
|
import com.hippo.ehviewer.ui.awaitSelectDate
|
||||||
import com.hippo.ehviewer.ui.composing
|
import com.hippo.ehviewer.ui.composing
|
||||||
import com.hippo.ehviewer.ui.destinations.ProgressScreenDestination
|
import com.hippo.ehviewer.ui.destinations.ProgressScreenDestination
|
||||||
import com.hippo.ehviewer.ui.doGalleryInfoAction
|
import com.hippo.ehviewer.ui.doGalleryInfoAction
|
||||||
@@ -116,15 +123,14 @@ import com.hippo.ehviewer.ui.main.GalleryInfoListItem
|
|||||||
import com.hippo.ehviewer.ui.main.GalleryList
|
import com.hippo.ehviewer.ui.main.GalleryList
|
||||||
import com.hippo.ehviewer.ui.main.ImageSearch
|
import com.hippo.ehviewer.ui.main.ImageSearch
|
||||||
import com.hippo.ehviewer.ui.main.SearchFilter
|
import com.hippo.ehviewer.ui.main.SearchFilter
|
||||||
import com.hippo.ehviewer.ui.showDatePicker
|
|
||||||
import com.hippo.ehviewer.ui.tools.Deferred
|
import com.hippo.ehviewer.ui.tools.Deferred
|
||||||
import com.hippo.ehviewer.ui.tools.DragHandle
|
import com.hippo.ehviewer.ui.tools.HapticFeedbackType
|
||||||
import com.hippo.ehviewer.ui.tools.SwipeToDismissBox2
|
|
||||||
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
|
import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState
|
||||||
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate
|
||||||
import com.hippo.ehviewer.ui.tools.draggingHapticFeedback
|
|
||||||
import com.hippo.ehviewer.ui.tools.foldToLoadResult
|
import com.hippo.ehviewer.ui.tools.foldToLoadResult
|
||||||
|
import com.hippo.ehviewer.ui.tools.rememberHapticFeedback
|
||||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||||
|
import com.hippo.ehviewer.ui.tools.rememberMutableStateInDataStore
|
||||||
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
import com.hippo.ehviewer.ui.tools.snackBarPadding
|
||||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||||
import com.hippo.ehviewer.util.pickVisualMedia
|
import com.hippo.ehviewer.util.pickVisualMedia
|
||||||
@@ -144,7 +150,7 @@ import kotlinx.coroutines.launch
|
|||||||
import moe.tarsin.coroutines.onEachLatest
|
import moe.tarsin.coroutines.onEachLatest
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import sh.calvin.reorderable.ReorderableItem
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
import sh.calvin.reorderable.rememberReorderableLazyColumnState
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
@@ -167,16 +173,17 @@ fun ToplistScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlB
|
|||||||
fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = composing(navigator) {
|
fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = composing(navigator) {
|
||||||
val searchFieldState = rememberTextFieldState()
|
val searchFieldState = rememberTextFieldState()
|
||||||
var urlBuilder by rememberSaveable(lub) { mutableStateOf(lub) }
|
var urlBuilder by rememberSaveable(lub) { mutableStateOf(lub) }
|
||||||
|
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
||||||
var showSearchLayout by rememberSaveable { mutableStateOf(false) }
|
var showSearchLayout by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
var category by rememberSaveable { mutableIntStateOf(Settings.searchCategory) }
|
var category by rememberMutableStateInDataStore("SearchCategory") { EhUtils.ALL_CATEGORY }
|
||||||
var searchMethod by rememberSaveable { mutableIntStateOf(1) }
|
var advancedSearchOption by rememberMutableStateInDataStore("AdvancedSearchOption") { AdvancedSearchOption() }
|
||||||
var advancedSearchOption by rememberSaveable { mutableStateOf(AdvancedSearchOption()) }
|
|
||||||
var imageUri by rememberSaveable { mutableStateOf<Uri?>(null) }
|
var imageUri by rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
|
DrawerHandle(!searchBarExpanded)
|
||||||
|
|
||||||
LaunchedEffect(urlBuilder) {
|
LaunchedEffect(urlBuilder) {
|
||||||
if (urlBuilder.mode == MODE_SUBSCRIPTION) searchMethod = 2
|
|
||||||
if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category
|
if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category
|
||||||
if (urlBuilder.mode != MODE_TOPLIST) {
|
if (urlBuilder.mode != MODE_TOPLIST) {
|
||||||
var keyword = urlBuilder.keyword.orEmpty()
|
var keyword = urlBuilder.keyword.orEmpty()
|
||||||
@@ -359,25 +366,27 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
},
|
},
|
||||||
windowInsets = WindowInsets(0),
|
windowInsets = WindowInsets(0),
|
||||||
)
|
)
|
||||||
val view = LocalView.current
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
val quickSearchListState = rememberLazyListState()
|
val quickSearchListState = rememberLazyListState()
|
||||||
val reorderableLazyListState = rememberReorderableLazyColumnState(quickSearchListState) { from, to ->
|
val hapticFeedback = rememberHapticFeedback()
|
||||||
|
val reorderableLazyListState = rememberReorderableLazyListState(quickSearchListState) { from, to ->
|
||||||
val fromIndex = from.index - 1
|
val fromIndex = from.index - 1
|
||||||
val toIndex = to.index - 1
|
val toIndex = to.index - 1
|
||||||
quickSearchList.apply { add(toIndex, removeAt(fromIndex)) }
|
quickSearchList.apply { add(toIndex, removeAt(fromIndex)) }
|
||||||
view.performHapticFeedback(draggingHapticFeedback)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
|
||||||
}
|
}
|
||||||
var fromIndex by remember { mutableIntStateOf(-1) }
|
var fromIndex by remember { mutableIntStateOf(-1) }
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = quickSearchListState,
|
state = quickSearchListState,
|
||||||
// Workaround for https://issuetracker.google.com/332939169
|
contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
|
||||||
// contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
|
|
||||||
) {
|
) {
|
||||||
// Fix the first item's reorder animation
|
// Fix the first item's reorder animation
|
||||||
item {}
|
stickyHeader {
|
||||||
itemsIndexed(quickSearchList, key = { _, item -> item.id!! }) { index, item ->
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
itemsIndexed(quickSearchList, key = { _, item -> item.id!! }) { itemIndex, item ->
|
||||||
|
val index by rememberUpdatedState(itemIndex)
|
||||||
ReorderableItem(reorderableLazyListState, key = item.id!!) { isDragging ->
|
ReorderableItem(reorderableLazyListState, key = item.id!!) { isDragging ->
|
||||||
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
|
||||||
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
|
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
|
||||||
@@ -391,11 +400,10 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
EhDB.deleteQuickSearch(item)
|
EhDB.deleteQuickSearch(item)
|
||||||
with(quickSearchList) {
|
with(quickSearchList) {
|
||||||
val removeIndex = indexOf(item)
|
subList(index + 1, size).forEach {
|
||||||
subList(removeIndex + 1, size).forEach {
|
|
||||||
it.position--
|
it.position--
|
||||||
}
|
}
|
||||||
removeAt(removeIndex)
|
removeAt(index)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
dismissState.reset()
|
dismissState.reset()
|
||||||
@@ -403,9 +411,10 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SwipeToDismissBox2(
|
SwipeToDismissBox(
|
||||||
state = dismissState,
|
state = dismissState,
|
||||||
backgroundContent = {},
|
backgroundContent = {},
|
||||||
|
enableDismissFromStartToEnd = false,
|
||||||
) {
|
) {
|
||||||
val elevation by animateDpAsState(
|
val elevation by animateDpAsState(
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
@@ -433,22 +442,29 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
Text(text = item.name)
|
Text(text = item.name)
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
DragHandle(
|
IconButton(
|
||||||
onDragStarted = {
|
onClick = {},
|
||||||
fromIndex = index
|
modifier = Modifier.draggableHandle(
|
||||||
},
|
onDragStarted = {
|
||||||
onDragStopped = {
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.START)
|
||||||
if (fromIndex != -1) {
|
fromIndex = index
|
||||||
if (fromIndex != index) {
|
},
|
||||||
val range = if (fromIndex < index) fromIndex..index else index..fromIndex
|
onDragStopped = {
|
||||||
val toUpdate = quickSearchList.slice(range)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.END)
|
||||||
toUpdate.zip(range).forEach { it.first.position = it.second }
|
if (fromIndex != -1) {
|
||||||
launchIO { EhDB.updateQuickSearch(toUpdate) }
|
if (fromIndex != index) {
|
||||||
|
val range = if (fromIndex < index) fromIndex..index else index..fromIndex
|
||||||
|
val toUpdate = quickSearchList.slice(range)
|
||||||
|
toUpdate.zip(range).forEach { it.first.position = it.second }
|
||||||
|
launchIO { EhDB.updateQuickSearch(toUpdate) }
|
||||||
|
}
|
||||||
|
fromIndex = -1
|
||||||
}
|
}
|
||||||
fromIndex = -1
|
},
|
||||||
}
|
),
|
||||||
},
|
) {
|
||||||
)
|
Icon(imageVector = Icons.Default.Reorder, contentDescription = null)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -467,10 +483,6 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val refreshState = rememberPullToRefreshState {
|
|
||||||
data.loadState.refresh is LoadState.NotLoading
|
|
||||||
}
|
|
||||||
|
|
||||||
var fabExpanded by remember { mutableStateOf(false) }
|
var fabExpanded by remember { mutableStateOf(false) }
|
||||||
var fabHidden by remember { mutableStateOf(false) }
|
var fabHidden by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -530,13 +542,15 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
}
|
}
|
||||||
|
|
||||||
SearchBarScreen(
|
SearchBarScreen(
|
||||||
|
onApplySearch = ::onApplySearch,
|
||||||
|
expanded = searchBarExpanded,
|
||||||
|
onExpandedChange = {
|
||||||
|
searchBarExpanded = it
|
||||||
|
fabHidden = it
|
||||||
|
},
|
||||||
title = suitableTitle,
|
title = suitableTitle,
|
||||||
searchFieldState = searchFieldState,
|
searchFieldState = searchFieldState,
|
||||||
searchFieldHint = searchBarHint,
|
searchFieldHint = searchBarHint,
|
||||||
onApplySearch = ::onApplySearch,
|
|
||||||
onSearchExpanded = { fabHidden = true },
|
|
||||||
onSearchHidden = { fabHidden = false },
|
|
||||||
refreshState = refreshState,
|
|
||||||
suggestionProvider = {
|
suggestionProvider = {
|
||||||
GalleryDetailUrlParser.parse(it, false)?.run {
|
GalleryDetailUrlParser.parse(it, false)?.run {
|
||||||
GalleryDetailUrlSuggestion(gid, token)
|
GalleryDetailUrlSuggestion(gid, token)
|
||||||
@@ -564,12 +578,11 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
filter = {
|
filter = {
|
||||||
SearchFilter(
|
SearchFilter(
|
||||||
category = category,
|
category = category,
|
||||||
onCategoryChanged = {
|
onCategoryChanged = { category = it },
|
||||||
Settings.searchCategory = it
|
|
||||||
category = it
|
|
||||||
},
|
|
||||||
advancedOption = advancedSearchOption,
|
advancedOption = advancedSearchOption,
|
||||||
onAdvancedOptionChanged = { advancedSearchOption = it },
|
onAdvancedOptionChanged = {
|
||||||
|
advancedSearchOption = it
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -632,9 +645,8 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
|
|
||||||
val height by collectListThumbSizeAsState()
|
val height by collectListThumbSizeAsState()
|
||||||
val showPages by Settings.showGalleryPages.collectAsState()
|
val showPages by Settings.showGalleryPages.collectAsState()
|
||||||
val context = LocalContext.current
|
|
||||||
val searchBarConnection = remember {
|
val searchBarConnection = remember {
|
||||||
val slop = ViewConfiguration.get(context).scaledTouchSlop
|
val slop = ViewConfiguration.get(implicit<Context>()).scaledTouchSlop
|
||||||
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
@@ -675,9 +687,10 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
onClick = { navigate(info.asDst()) },
|
onClick = { navigate(info.asDst()) },
|
||||||
onLongClick = { launch { doGalleryInfoAction(info) } },
|
onLongClick = { launch { doGalleryInfoAction(info) } },
|
||||||
info = info,
|
info = info,
|
||||||
|
showPages = showPages,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
refreshState = refreshState,
|
searchBarOffsetY = { searchBarOffsetY },
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
urlBuilder.setRange(0)
|
urlBuilder.setRange(0)
|
||||||
data.refresh()
|
data.refresh()
|
||||||
@@ -722,32 +735,26 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c
|
|||||||
val page = urlBuilder.mJumpTo?.toIntOrNull() ?: 0
|
val page = urlBuilder.mJumpTo?.toIntOrNull() ?: 0
|
||||||
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
|
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
|
||||||
val text = awaitInputText(title = gotoTitle, hint = hint, isNumber = true) { oriText ->
|
val text = awaitInputText(title = gotoTitle, hint = hint, isNumber = true) { oriText ->
|
||||||
val text = oriText.trim()
|
when (oriText.trim().toIntOrNull()?.let { it - 1 }) {
|
||||||
val goTo = runCatching {
|
null -> invalidNum
|
||||||
text.toInt() - 1
|
!in 0..<TOPLIST_PAGES -> outOfRange
|
||||||
}.onFailure {
|
else -> null
|
||||||
return@awaitInputText invalidNum
|
}
|
||||||
}.getOrThrow()
|
|
||||||
if (goTo !in 0..<TOPLIST_PAGES) outOfRange else null
|
|
||||||
}.trim().toInt() - 1
|
}.trim().toInt() - 1
|
||||||
urlBuilder.setJumpTo(text)
|
urlBuilder.setJumpTo(text)
|
||||||
data.refresh()
|
|
||||||
} else {
|
} else {
|
||||||
launch {
|
val date = awaitSelectDate()
|
||||||
val date = showDatePicker()
|
urlBuilder.mJumpTo = date
|
||||||
urlBuilder.mJumpTo = date
|
|
||||||
data.refresh()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
data.refresh()
|
||||||
}
|
}
|
||||||
onClick(Icons.AutoMirrored.Default.LastPage) {
|
onClick(Icons.AutoMirrored.Default.LastPage) {
|
||||||
if (isTopList) {
|
if (isTopList) {
|
||||||
urlBuilder.setJumpTo(TOPLIST_PAGES - 1)
|
urlBuilder.setJumpTo(TOPLIST_PAGES - 1)
|
||||||
data.refresh()
|
|
||||||
} else {
|
} else {
|
||||||
urlBuilder.setIndex("1", false)
|
urlBuilder.setIndex("1", false)
|
||||||
data.refresh()
|
|
||||||
}
|
}
|
||||||
|
data.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import com.hippo.ehviewer.coil.justDownload
|
|||||||
import com.hippo.ehviewer.collectAsState
|
import com.hippo.ehviewer.collectAsState
|
||||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||||
import com.hippo.ehviewer.ktbuilder.launchIn
|
import com.hippo.ehviewer.ktbuilder.launchIn
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.ui.composing
|
import com.hippo.ehviewer.ui.composing
|
||||||
import com.hippo.ehviewer.ui.main.EhPreviewItem
|
import com.hippo.ehviewer.ui.main.EhPreviewItem
|
||||||
import com.hippo.ehviewer.ui.navToReader
|
import com.hippo.ehviewer.ui.navToReader
|
||||||
@@ -59,7 +58,6 @@ import moe.tarsin.coroutines.runSuspendCatching
|
|||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun GalleryPreviewScreen(detail: GalleryDetail, toNextPage: Boolean, navigator: DestinationsNavigator) = composing(navigator) {
|
fun GalleryPreviewScreen(detail: GalleryDetail, toNextPage: Boolean, navigator: DestinationsNavigator) = composing(navigator) {
|
||||||
LockDrawer(true)
|
|
||||||
val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
val pgSize = detail.previewList.size
|
val pgSize = detail.previewList.size
|
||||||
val state = rememberLazyGridState(initialFirstVisibleItemIndex = if (toNextPage) pgSize else 0)
|
val state = rememberLazyGridState(initialFirstVisibleItemIndex = if (toNextPage) pgSize else 0)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
import androidx.compose.material3.fork.SwipeToDismissBox
|
||||||
|
import androidx.compose.material3.fork.rememberSwipeToDismissBoxState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
@@ -46,13 +47,13 @@ import com.hippo.ehviewer.Settings
|
|||||||
import com.hippo.ehviewer.collectAsState
|
import com.hippo.ehviewer.collectAsState
|
||||||
import com.hippo.ehviewer.icons.EhIcons
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
import com.hippo.ehviewer.icons.big.History
|
import com.hippo.ehviewer.icons.big.History
|
||||||
|
import com.hippo.ehviewer.ui.DrawerHandle
|
||||||
import com.hippo.ehviewer.ui.composing
|
import com.hippo.ehviewer.ui.composing
|
||||||
import com.hippo.ehviewer.ui.doGalleryInfoAction
|
import com.hippo.ehviewer.ui.doGalleryInfoAction
|
||||||
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
|
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
|
||||||
import com.hippo.ehviewer.ui.main.plus
|
import com.hippo.ehviewer.ui.main.plus
|
||||||
import com.hippo.ehviewer.ui.tools.Deferred
|
import com.hippo.ehviewer.ui.tools.Deferred
|
||||||
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
|
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
|
||||||
import com.hippo.ehviewer.ui.tools.SwipeToDismissBox2
|
|
||||||
import com.hippo.ehviewer.ui.tools.rememberInVM
|
import com.hippo.ehviewer.ui.tools.rememberInVM
|
||||||
import com.hippo.ehviewer.ui.tools.thenIf
|
import com.hippo.ehviewer.ui.tools.thenIf
|
||||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||||
@@ -70,9 +71,12 @@ fun HistoryScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
val hint = stringResource(R.string.search_bar_hint, title)
|
val hint = stringResource(R.string.search_bar_hint, title)
|
||||||
val animateItems by Settings.animateItems.collectAsState()
|
val animateItems by Settings.animateItems.collectAsState()
|
||||||
|
|
||||||
|
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
var searchBarOffsetY by remember { mutableIntStateOf(0) }
|
||||||
var keyword by rememberSaveable { mutableStateOf("") }
|
var keyword by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
|
DrawerHandle(!searchBarExpanded)
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val historyData = rememberInVM {
|
val historyData = rememberInVM {
|
||||||
Pager(config = PagingConfig(pageSize = 20, jumpThreshold = 40)) {
|
Pager(config = PagingConfig(pageSize = 20, jumpThreshold = 40)) {
|
||||||
@@ -85,14 +89,14 @@ fun HistoryScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
}.collectAsLazyPagingItems()
|
}.collectAsLazyPagingItems()
|
||||||
FavouriteStatusRouter.Observe(historyData)
|
FavouriteStatusRouter.Observe(historyData)
|
||||||
SearchBarScreen(
|
SearchBarScreen(
|
||||||
title = title,
|
|
||||||
searchFieldHint = hint,
|
|
||||||
onApplySearch = {
|
onApplySearch = {
|
||||||
keyword = it
|
keyword = it
|
||||||
historyData.refresh()
|
historyData.refresh()
|
||||||
},
|
},
|
||||||
onSearchExpanded = {},
|
expanded = searchBarExpanded,
|
||||||
onSearchHidden = {},
|
onExpandedChange = { searchBarExpanded = it },
|
||||||
|
title = title,
|
||||||
|
searchFieldHint = hint,
|
||||||
searchBarOffsetY = { searchBarOffsetY },
|
searchBarOffsetY = { searchBarOffsetY },
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
@@ -141,10 +145,11 @@ fun HistoryScreen(navigator: DestinationsNavigator) = composing(navigator) {
|
|||||||
true
|
true
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
SwipeToDismissBox2(
|
SwipeToDismissBox(
|
||||||
state = dismissState,
|
state = dismissState,
|
||||||
backgroundContent = {},
|
backgroundContent = {},
|
||||||
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
modifier = Modifier.thenIf(animateItems) { animateItem() },
|
||||||
|
enableDismissFromStartToEnd = false,
|
||||||
) {
|
) {
|
||||||
GalleryInfoListItem(
|
GalleryInfoListItem(
|
||||||
onClick = { navigate(info.asDst()) },
|
onClick = { navigate(info.asDst()) },
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.hippo.ehviewer.ui.screen
|
package com.hippo.ehviewer.ui.screen
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||||
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
|
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
|
||||||
@@ -34,19 +33,9 @@ fun DestinationsNavigator.navWithUrl(url: String): Boolean {
|
|||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun DestinationsNavigator.popNavigate(direction: Direction) = navigate(direction) {
|
fun DestinationsNavigator.popNavigate(direction: Direction) = navigate(direction) {
|
||||||
popUpTo(NavGraphs.root.route)
|
popUpTo(NavGraphs.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun NavController.navWithUrl(url: String): Boolean {
|
|
||||||
val dest = urlToDestination(url) ?: return false
|
|
||||||
navigateTo(dest)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun NavController.navigateTo(direction: Direction) = navigate(direction.route)
|
|
||||||
|
|
||||||
fun BaseGalleryInfo.asDst() = GalleryDetailScreenDestination(GalleryInfoArgs(this))
|
fun BaseGalleryInfo.asDst() = GalleryDetailScreenDestination(GalleryInfoArgs(this))
|
||||||
|
|
||||||
infix fun Long.asDstWith(token: String) = GalleryDetailScreenDestination(TokenArgs(this, token))
|
infix fun Long.asDstWith(token: String) = GalleryDetailScreenDestination(TokenArgs(this, token))
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import com.hippo.ehviewer.R
|
|||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.icons.EhIcons
|
import com.hippo.ehviewer.icons.EhIcons
|
||||||
import com.hippo.ehviewer.icons.big.SadAndroid
|
import com.hippo.ehviewer.icons.big.SadAndroid
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.util.displayString
|
import com.hippo.ehviewer.util.displayString
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
@@ -35,7 +34,6 @@ import moe.tarsin.coroutines.runSuspendCatching
|
|||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun ProgressScreen(gid: Long, token: String, page: Int, navigator: DestinationsNavigator) {
|
fun ProgressScreen(gid: Long, token: String, page: Int, navigator: DestinationsNavigator) {
|
||||||
LockDrawer(true)
|
|
||||||
val wrong = stringResource(id = R.string.error_something_wrong_happened)
|
val wrong = stringResource(id = R.string.error_something_wrong_happened)
|
||||||
var error by rememberSaveable { mutableStateOf("") }
|
var error by rememberSaveable { mutableStateOf("") }
|
||||||
LaunchedEffect(error) {
|
LaunchedEffect(error) {
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
package com.hippo.ehviewer.ui.screen
|
package com.hippo.ehviewer.ui.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.displayCutoutPadding
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.ime
|
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.layout.union
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -41,15 +41,12 @@ import androidx.compose.material3.SearchBarDefaults
|
|||||||
import androidx.compose.material3.SearchBarHorizontalPadding
|
import androidx.compose.material3.SearchBarHorizontalPadding
|
||||||
import androidx.compose.material3.SearchBarInputField
|
import androidx.compose.material3.SearchBarInputField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -69,7 +66,6 @@ import com.hippo.ehviewer.collectAsState
|
|||||||
import com.hippo.ehviewer.dao.Search
|
import com.hippo.ehviewer.dao.Search
|
||||||
import com.hippo.ehviewer.dao.SearchDao
|
import com.hippo.ehviewer.dao.SearchDao
|
||||||
import com.hippo.ehviewer.ui.LocalNavDrawerState
|
import com.hippo.ehviewer.ui.LocalNavDrawerState
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||||
import com.hippo.ehviewer.ui.tools.rememberCompositionActiveState
|
import com.hippo.ehviewer.ui.tools.rememberCompositionActiveState
|
||||||
import com.hippo.ehviewer.ui.tools.thenIf
|
import com.hippo.ehviewer.ui.tools.thenIf
|
||||||
@@ -97,28 +93,26 @@ abstract class Suggestion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun SearchDao.suggestions(prefix: String, limit: Int) =
|
suspend fun SearchDao.suggestions(prefix: String, limit: Int) =
|
||||||
(if (prefix.isBlank()) list(limit) else rawSuggestions(prefix, limit)).map { it.query }
|
(if (prefix.isBlank()) list(limit) else rawSuggestions(prefix, limit))
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBarScreen(
|
fun SearchBarScreen(
|
||||||
|
onApplySearch: (String) -> Unit,
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
searchFieldState: TextFieldState = rememberTextFieldState(),
|
searchFieldState: TextFieldState = rememberTextFieldState(),
|
||||||
searchFieldHint: String? = null,
|
searchFieldHint: String? = null,
|
||||||
onApplySearch: (String) -> Unit,
|
|
||||||
onSearchExpanded: () -> Unit,
|
|
||||||
onSearchHidden: () -> Unit,
|
|
||||||
refreshState: PullToRefreshState? = null,
|
|
||||||
suggestionProvider: SuggestionProvider? = null,
|
suggestionProvider: SuggestionProvider? = null,
|
||||||
tagNamespace: Boolean = false,
|
tagNamespace: Boolean = false,
|
||||||
searchBarOffsetY: () -> Int,
|
searchBarOffsetY: () -> Int = { 0 },
|
||||||
trailingIcon: @Composable () -> Unit,
|
trailingIcon: @Composable () -> Unit = {},
|
||||||
filter: @Composable (() -> Unit)? = null,
|
filter: @Composable (() -> Unit)? = null,
|
||||||
floatingActionButton: @Composable () -> Unit = {},
|
floatingActionButton: @Composable () -> Unit = {},
|
||||||
content: @Composable (PaddingValues) -> Unit,
|
content: @Composable (PaddingValues) -> Unit,
|
||||||
) {
|
) {
|
||||||
var mSuggestionList by remember { mutableStateOf(emptyList<Suggestion>()) }
|
var mSuggestionList by remember { mutableStateOf(emptyList<Suggestion>()) }
|
||||||
val mSearchDatabase = searchDatabase.searchDao()
|
val mSearchDatabase = searchDatabase.searchDao()
|
||||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
|
||||||
val scope = rememberCoroutineScope { Dispatchers.IO }
|
val scope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val dialogState = LocalDialogState.current
|
val dialogState = LocalDialogState.current
|
||||||
@@ -169,19 +163,6 @@ fun SearchBarScreen(
|
|||||||
mSuggestionList = mergedSuggestionFlow().toList()
|
mSuggestionList = mergedSuggestionFlow().toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldLockDrawer by remember { mutableStateOf(false) }
|
|
||||||
LockDrawer(shouldLockDrawer)
|
|
||||||
|
|
||||||
fun onSearchViewExpanded() {
|
|
||||||
onSearchExpanded()
|
|
||||||
shouldLockDrawer = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSearchViewHidden() {
|
|
||||||
shouldLockDrawer = false
|
|
||||||
onSearchHidden()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
snapshotFlow { searchFieldState.text }.collectLatest {
|
snapshotFlow { searchFieldState.text }.collectLatest {
|
||||||
@@ -191,12 +172,12 @@ fun SearchBarScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun hideSearchView() {
|
fun hideSearchView() {
|
||||||
onSearchViewHidden()
|
onExpandedChange(false)
|
||||||
expanded = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onApplySearch() {
|
fun onApplySearch() {
|
||||||
val query = searchFieldState.text.trim().toString()
|
// May have invalid whitespaces if pasted from clipboard, replace them with spaces
|
||||||
|
val query = searchFieldState.text.trim().replace(WhitespaceRegex, " ")
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
mSearchDatabase.deleteQuery(query)
|
mSearchDatabase.deleteQuery(query)
|
||||||
@@ -219,42 +200,21 @@ fun SearchBarScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val scrollAwayModifier = if (!expanded) {
|
|
||||||
Modifier.offset { IntOffset(0, searchBarOffsetY()) }
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
// Placeholder, fill immutable SearchBar padding
|
// Placeholder, fill immutable SearchBar padding
|
||||||
Spacer(
|
Spacer(modifier = Modifier.statusBarsPadding().height(SearchBarDefaults.InputFieldHeight + 16.dp))
|
||||||
modifier = Modifier.statusBarsPadding().displayCutoutPadding()
|
|
||||||
.height(SearchBarDefaults.InputFieldHeight + 16.dp),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
floatingActionButton = floatingActionButton,
|
floatingActionButton = floatingActionButton,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
if (refreshState != null) {
|
// https://issuetracker.google.com/337191298
|
||||||
PullToRefreshContainer(
|
// Workaround for can't exit SearchBar due to refocus in non-touch mode
|
||||||
state = refreshState,
|
Box(Modifier.size(1.dp).focusable())
|
||||||
modifier = Modifier.align(Alignment.TopCenter).safeDrawingPadding()
|
|
||||||
.padding(top = 48.dp) then scrollAwayModifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val onExpandedChange = { v: Boolean ->
|
|
||||||
if (v) {
|
|
||||||
onSearchViewExpanded()
|
|
||||||
} else {
|
|
||||||
onSearchViewHidden()
|
|
||||||
}
|
|
||||||
expanded = v
|
|
||||||
}
|
|
||||||
val activeState = rememberCompositionActiveState()
|
val activeState = rememberCompositionActiveState()
|
||||||
SearchBar(
|
SearchBar(
|
||||||
modifier = Modifier.align(Alignment.TopCenter) then scrollAwayModifier
|
modifier = Modifier.align(Alignment.TopCenter).thenIf(!expanded) { offset { IntOffset(0, searchBarOffsetY()) } }
|
||||||
.windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)),
|
.windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)),
|
||||||
inputField = {
|
inputField = {
|
||||||
SearchBarInputField(
|
SearchBarInputField(
|
||||||
@@ -306,11 +266,8 @@ fun SearchBarScreen(
|
|||||||
activeState.Anchor()
|
activeState.Anchor()
|
||||||
filter?.invoke()
|
filter?.invoke()
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize(),
|
||||||
.windowInsetsPadding(WindowInsets.navigationBars.union(WindowInsets.ime).only(WindowInsetsSides.Bottom)),
|
contentPadding = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom).asPaddingValues(),
|
||||||
// Workaround for https://issuetracker.google.com/332939169
|
|
||||||
// contentPadding = WindowInsets.navigationBars.union(WindowInsets.ime)
|
|
||||||
// .only(WindowInsetsSides.Bottom).asPaddingValues(),
|
|
||||||
) {
|
) {
|
||||||
// Workaround for prepending before the first item
|
// Workaround for prepending before the first item
|
||||||
item {}
|
item {}
|
||||||
@@ -359,3 +316,5 @@ fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val WhitespaceRegex = Regex("\\s+")
|
||||||
@@ -51,7 +51,6 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
|
||||||
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}"
|
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}"
|
||||||
@@ -59,7 +58,7 @@ private const val RELEASE_URL = "$REPO_URL/releases"
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Stable
|
@Stable
|
||||||
private fun versionCode() = "${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA})\n" + stringResource(R.string.settings_about_build_time, BuildConfig.BUILD_TIME)
|
private fun versionCode() = "${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA})\n" + stringResource(R.string.settings_about_commit_time, AppConfig.commitTime)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Stable
|
@Stable
|
||||||
@@ -154,7 +153,7 @@ suspend fun DialogState.showNewVersion(context: Context, release: Release) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Settings.backupBeforeUpdate) {
|
if (Settings.backupBeforeUpdate) {
|
||||||
val time = ReadableTime.getFilenamableTime(Clock.System.now().toEpochMilliseconds())
|
val time = ReadableTime.getFilenamableTime()
|
||||||
downloadLocation.createFile("$time.db")?.let {
|
downloadLocation.createFile("$time.db")?.let {
|
||||||
EhDB.exportDB(context, it)
|
EhDB.exportDB(context, it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -35,15 +36,20 @@ import com.hippo.ehviewer.R
|
|||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.asMutableState
|
import com.hippo.ehviewer.asMutableState
|
||||||
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||||
|
import com.hippo.ehviewer.collectAsState
|
||||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||||
import com.hippo.ehviewer.ui.tools.observed
|
import com.hippo.ehviewer.ui.tools.observed
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.Crash
|
import com.hippo.ehviewer.util.Crash
|
||||||
import com.hippo.ehviewer.util.ReadableTime
|
import com.hippo.ehviewer.util.ReadableTime
|
||||||
|
import com.hippo.ehviewer.util.getAppLanguage
|
||||||
|
import com.hippo.ehviewer.util.getLanguages
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastV
|
||||||
|
import com.hippo.ehviewer.util.setAppLanguage
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.unifile.asUniFile
|
||||||
|
import com.jamal.composeprefs3.ui.prefs.DropDownPref
|
||||||
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
@@ -81,10 +87,12 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
|
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
|
||||||
// SwitchPreference(
|
if (AppConfig.isSnapshot) {
|
||||||
// title = "New Compose Reader [WIP!!!]",
|
SwitchPreference(
|
||||||
// value = Settings::newReader,
|
title = "New Compose Reader [WIP!!!]",
|
||||||
// )
|
value = Settings::newReader,
|
||||||
|
)
|
||||||
|
}
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = stringResource(id = R.string.settings_advanced_save_parse_error_body),
|
title = stringResource(id = R.string.settings_advanced_save_parse_error_body),
|
||||||
summary = stringResource(id = R.string.settings_advanced_save_parse_error_body_summary),
|
summary = stringResource(id = R.string.settings_advanced_save_parse_error_body_summary),
|
||||||
@@ -100,7 +108,7 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
title = stringResource(id = R.string.settings_advanced_dump_logcat),
|
title = stringResource(id = R.string.settings_advanced_dump_logcat),
|
||||||
summary = stringResource(id = R.string.settings_advanced_dump_logcat_summary),
|
summary = stringResource(id = R.string.settings_advanced_dump_logcat_summary),
|
||||||
contract = ActivityResultContracts.CreateDocument("application/zip"),
|
contract = ActivityResultContracts.CreateDocument("application/zip"),
|
||||||
key = "log-" + ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".zip",
|
key = "log-" + ReadableTime.getFilenamableTime() + ".zip",
|
||||||
) { uri ->
|
) { uri ->
|
||||||
uri?.run {
|
uri?.run {
|
||||||
context.runCatching {
|
context.runCatching {
|
||||||
@@ -116,7 +124,7 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
zipOs.putNextEntry(entry)
|
zipOs.putNextEntry(entry)
|
||||||
file.inputStream().use { it.copyTo(zipOs) }
|
file.inputStream().use { it.copyTo(zipOs) }
|
||||||
}
|
}
|
||||||
val logcatEntry = ZipEntry("logcat-" + ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt")
|
val logcatEntry = ZipEntry("logcat-" + ReadableTime.getFilenamableTime() + ".txt")
|
||||||
zipOs.putNextEntry(logcatEntry)
|
zipOs.putNextEntry(logcatEntry)
|
||||||
Crash.collectInfo(zipOs.writer())
|
Crash.collectInfo(zipOs.writer())
|
||||||
Runtime.getRuntime().exec("logcat -d").inputStream.use { it.copyTo(zipOs) }
|
Runtime.getRuntime().exec("logcat -d").inputStream.use { it.copyTo(zipOs) }
|
||||||
@@ -135,11 +143,17 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
entryValueRes = R.array.read_cache_size_entry_values,
|
entryValueRes = R.array.read_cache_size_entry_values,
|
||||||
value = Settings::readCacheSize.observed,
|
value = Settings::readCacheSize.observed,
|
||||||
)
|
)
|
||||||
SimpleMenuPreference(
|
var currentLanguage by remember { mutableStateOf(getAppLanguage()) }
|
||||||
|
val languages = remember { context.getLanguages() }
|
||||||
|
DropDownPref(
|
||||||
title = stringResource(id = R.string.settings_advanced_app_language_title),
|
title = stringResource(id = R.string.settings_advanced_app_language_title),
|
||||||
entry = R.array.app_language_entries,
|
defaultValue = currentLanguage,
|
||||||
entryValueRes = R.array.app_language_entry_values,
|
onValueChange = {
|
||||||
value = Settings::language,
|
setAppLanguage(it)
|
||||||
|
currentLanguage = it
|
||||||
|
},
|
||||||
|
useSelectedAsSummary = true,
|
||||||
|
entries = languages,
|
||||||
)
|
)
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = stringResource(id = R.string.preload_thumb_aggressively),
|
title = stringResource(id = R.string.preload_thumb_aggressively),
|
||||||
@@ -152,13 +166,6 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
title = stringResource(id = R.string.animate_items),
|
title = stringResource(id = R.string.animate_items),
|
||||||
summary = stringResource(id = R.string.animate_items_summary),
|
summary = stringResource(id = R.string.animate_items_summary),
|
||||||
)
|
)
|
||||||
IntSliderPreference(
|
|
||||||
maxValue = 5,
|
|
||||||
minValue = 1,
|
|
||||||
title = stringResource(id = R.string.settings_advanced_touch_slop),
|
|
||||||
summary = stringResource(id = R.string.settings_advanced_touch_slop_summary),
|
|
||||||
value = Settings::touchSlopFactor,
|
|
||||||
)
|
|
||||||
var userAgent by Settings::userAgent.observed
|
var userAgent by Settings::userAgent.observed
|
||||||
val userAgentTitle = stringResource(id = R.string.user_agent)
|
val userAgentTitle = stringResource(id = R.string.user_agent)
|
||||||
Preference(
|
Preference(
|
||||||
@@ -169,15 +176,16 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
userAgent = dialogState.awaitInputText(
|
userAgent = dialogState.awaitInputText(
|
||||||
initial = userAgent,
|
initial = userAgent,
|
||||||
title = userAgentTitle,
|
title = userAgentTitle,
|
||||||
).trim().takeUnless { it.isBlank() } ?: CHROME_USER_AGENT
|
).trim().ifBlank { null } ?: CHROME_USER_AGENT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val exportFailed = stringResource(id = R.string.settings_advanced_export_data_failed)
|
val exportFailed = stringResource(id = R.string.settings_advanced_export_data_failed)
|
||||||
|
val now = ReadableTime.getFilenamableTime()
|
||||||
LauncherPreference(
|
LauncherPreference(
|
||||||
title = stringResource(id = R.string.settings_advanced_export_data),
|
title = stringResource(id = R.string.settings_advanced_export_data),
|
||||||
summary = stringResource(id = R.string.settings_advanced_export_data_summary),
|
summary = stringResource(id = R.string.settings_advanced_export_data_summary),
|
||||||
contract = ActivityResultContracts.CreateDocument("application/vnd.sqlite3"),
|
contract = ActivityResultContracts.CreateDocument("application/vnd.sqlite3"),
|
||||||
key = ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".db",
|
key = if (isAtLeastV) now else "$now.db",
|
||||||
) { uri ->
|
) { uri ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
context.runCatching {
|
context.runCatching {
|
||||||
@@ -196,7 +204,7 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
title = stringResource(id = R.string.settings_advanced_import_data),
|
title = stringResource(id = R.string.settings_advanced_import_data),
|
||||||
summary = stringResource(id = R.string.settings_advanced_import_data_summary),
|
summary = stringResource(id = R.string.settings_advanced_import_data_summary),
|
||||||
contract = ActivityResultContracts.GetContent(),
|
contract = ActivityResultContracts.GetContent(),
|
||||||
key = "application/octet-stream",
|
key = if (isAtLeastV) "application/vnd.sqlite3" else "application/octet-stream",
|
||||||
) { uri ->
|
) { uri ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
context.runCatching {
|
context.runCatching {
|
||||||
@@ -209,7 +217,8 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (EhCookieStore.hasSignedIn()) {
|
val hasSignedIn by Settings.hasSignedIn.collectAsState()
|
||||||
|
if (hasSignedIn) {
|
||||||
val backupNothing = stringResource(id = R.string.settings_advanced_backup_favorite_nothing)
|
val backupNothing = stringResource(id = R.string.settings_advanced_backup_favorite_nothing)
|
||||||
val backupFailed = stringResource(id = R.string.settings_advanced_backup_favorite_failed)
|
val backupFailed = stringResource(id = R.string.settings_advanced_backup_favorite_failed)
|
||||||
val backupSucceed = stringResource(id = R.string.settings_advanced_backup_favorite_success)
|
val backupSucceed = stringResource(id = R.string.settings_advanced_backup_favorite_success)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import arrow.fx.coroutines.parMap
|
import arrow.fx.coroutines.parMap
|
||||||
|
import arrow.fx.coroutines.parMapNotNull
|
||||||
import com.hippo.ehviewer.EhDB
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
@@ -36,7 +37,10 @@ import com.hippo.ehviewer.client.EhUrl
|
|||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
|
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
|
||||||
|
import com.hippo.ehviewer.client.parser.ParserUtils
|
||||||
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
|
import com.hippo.ehviewer.download.downloadDir
|
||||||
import com.hippo.ehviewer.download.downloadLocation
|
import com.hippo.ehviewer.download.downloadLocation
|
||||||
import com.hippo.ehviewer.spider.COMIC_INFO_FILE
|
import com.hippo.ehviewer.spider.COMIC_INFO_FILE
|
||||||
import com.hippo.ehviewer.spider.SpiderDen
|
import com.hippo.ehviewer.spider.SpiderDen
|
||||||
@@ -46,6 +50,7 @@ import com.hippo.ehviewer.spider.readCompatFromUniFile
|
|||||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||||
import com.hippo.ehviewer.ui.tools.observed
|
import com.hippo.ehviewer.ui.tools.observed
|
||||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||||
|
import com.hippo.ehviewer.util.displayString
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.hippo.unifile.asUniFile
|
import com.hippo.unifile.asUniFile
|
||||||
import com.hippo.unifile.displayPath
|
import com.hippo.unifile.displayPath
|
||||||
@@ -157,6 +162,34 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
summary = stringResource(id = R.string.settings_download_archive_metadata_summary),
|
summary = stringResource(id = R.string.settings_download_archive_metadata_summary),
|
||||||
value = Settings::archiveMetadata,
|
value = Settings::archiveMetadata,
|
||||||
)
|
)
|
||||||
|
WorkPreference(
|
||||||
|
title = stringResource(id = R.string.settings_download_reload_metadata),
|
||||||
|
summary = stringResource(id = R.string.settings_download_reload_metadata_summary),
|
||||||
|
) {
|
||||||
|
fun DownloadInfo.isStable(): Boolean {
|
||||||
|
val downloadTime = downloadDir?.findFile(COMIC_INFO_FILE)?.lastModified() ?: return false
|
||||||
|
val postedTime = posted?.let { ParserUtils.parseDate(it) } ?: return false
|
||||||
|
// stable 30 days after posted
|
||||||
|
val stableTime = postedTime + 30L * 24L * 60L * 60L * 1000L
|
||||||
|
return downloadTime > stableTime
|
||||||
|
}
|
||||||
|
|
||||||
|
runSuspendCatching {
|
||||||
|
DownloadManager.downloadInfoList.parMapNotNull {
|
||||||
|
if (it.state == DownloadInfo.STATE_FINISH && !it.isStable()) it else null
|
||||||
|
}.apply {
|
||||||
|
fillGalleryListByApi(this, EhUrl.referer)
|
||||||
|
val toUpdate = parMap { di ->
|
||||||
|
di.galleryInfo.also { SpiderDen(it, di.dirname!!).writeComicInfo(false) }
|
||||||
|
}
|
||||||
|
EhDB.updateGalleryInfo(toUpdate)
|
||||||
|
launchSnackBar(context.getString(R.string.settings_download_reload_metadata_successfully, toUpdate.size))
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
launchSnackBar(context.getString(R.string.settings_download_reload_metadata_failed, it.displayString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val restoreFailed = stringResource(id = R.string.settings_download_restore_failed)
|
||||||
WorkPreference(
|
WorkPreference(
|
||||||
title = stringResource(id = R.string.settings_download_restore_download_items),
|
title = stringResource(id = R.string.settings_download_restore_download_items),
|
||||||
summary = stringResource(id = R.string.settings_download_restore_download_items_summary),
|
summary = stringResource(id = R.string.settings_download_restore_download_items_summary),
|
||||||
@@ -164,7 +197,7 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
var restoreDirCount = 0
|
var restoreDirCount = 0
|
||||||
suspend fun getRestoreItem(file: UniFile): RestoreItem? {
|
suspend fun getRestoreItem(file: UniFile): RestoreItem? {
|
||||||
if (!file.isDirectory) return null
|
if (!file.isDirectory) return null
|
||||||
return runCatching {
|
return runSuspendCatching {
|
||||||
val (gid, token) = file.findFile(SPIDER_INFO_FILENAME)?.let {
|
val (gid, token) = file.findFile(SPIDER_INFO_FILENAME)?.let {
|
||||||
readCompatFromUniFile(it)?.run {
|
readCompatFromUniFile(it)?.run {
|
||||||
GalleryDetailUrlParser.Result(gid, token)
|
GalleryDetailUrlParser.Result(gid, token)
|
||||||
@@ -190,12 +223,8 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
val result = downloadLocation.listFiles().mapNotNull { getRestoreItem(it) }.apply {
|
val result = downloadLocation.listFiles().parMapNotNull { getRestoreItem(it) }.also {
|
||||||
runSuspendCatching {
|
fillGalleryListByApi(it, EhUrl.referer)
|
||||||
fillGalleryListByApi(this, EhUrl.referer)
|
|
||||||
}.onFailure {
|
|
||||||
logcat(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
launchSnackBar(RESTORE_COUNT_MSG(restoreDirCount))
|
launchSnackBar(RESTORE_COUNT_MSG(restoreDirCount))
|
||||||
@@ -211,6 +240,7 @@ fun DownloadScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
logcat(it)
|
logcat(it)
|
||||||
|
launchSnackBar(restoreFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkPreference(
|
WorkPreference(
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.hippo.ehviewer.ui.settings
|
package com.hippo.ehviewer.ui.settings
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -14,6 +17,7 @@ import androidx.compose.material.icons.filled.ContentCopy
|
|||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
@@ -39,6 +43,7 @@ import androidx.compose.ui.res.stringArrayResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.fromHtml
|
import androidx.compose.ui.text.fromHtml
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.asMutableState
|
import com.hippo.ehviewer.asMutableState
|
||||||
@@ -47,10 +52,12 @@ import com.hippo.ehviewer.client.EhEngine
|
|||||||
import com.hippo.ehviewer.client.EhTagDatabase
|
import com.hippo.ehviewer.client.EhTagDatabase
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
import com.hippo.ehviewer.client.parser.HomeParser
|
import com.hippo.ehviewer.client.parser.HomeParser
|
||||||
|
import com.hippo.ehviewer.collectAsState
|
||||||
import com.hippo.ehviewer.ui.destinations.FilterScreenDestination
|
import com.hippo.ehviewer.ui.destinations.FilterScreenDestination
|
||||||
import com.hippo.ehviewer.ui.destinations.MyTagsScreenDestination
|
import com.hippo.ehviewer.ui.destinations.MyTagsScreenDestination
|
||||||
import com.hippo.ehviewer.ui.destinations.SignInScreenDestination
|
import com.hippo.ehviewer.ui.destinations.SignInScreenDestination
|
||||||
import com.hippo.ehviewer.ui.destinations.UConfigScreenDestination
|
import com.hippo.ehviewer.ui.destinations.UConfigScreenDestination
|
||||||
|
import com.hippo.ehviewer.ui.main.FundsItem
|
||||||
import com.hippo.ehviewer.ui.screen.popNavigate
|
import com.hippo.ehviewer.ui.screen.popNavigate
|
||||||
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
import com.hippo.ehviewer.ui.tools.LocalDialogState
|
||||||
import com.hippo.ehviewer.ui.tools.observed
|
import com.hippo.ehviewer.ui.tools.observed
|
||||||
@@ -77,7 +84,7 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
|||||||
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
|
fun launchSnackBar(content: String) = coroutineScope.launch { snackbarHostState.showSnackbar(content) }
|
||||||
val signin = EhCookieStore.hasSignedIn()
|
val hasSignedIn by Settings.hasSignedIn.collectAsState()
|
||||||
val dialogState = LocalDialogState.current
|
val dialogState = LocalDialogState.current
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -101,9 +108,10 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(paddingValues),
|
.padding(paddingValues),
|
||||||
) {
|
) {
|
||||||
|
val displayName by Settings.displayName.collectAsState()
|
||||||
Preference(
|
Preference(
|
||||||
title = stringResource(id = R.string.account_name),
|
title = stringResource(id = R.string.account_name),
|
||||||
summary = Settings.displayName ?: guestMode,
|
summary = displayName ?: guestMode,
|
||||||
) {
|
) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val cookies = EhCookieStore.getIdentityCookies()
|
val cookies = EhCookieStore.getIdentityCookies()
|
||||||
@@ -112,8 +120,9 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
|||||||
dismissText = R.string.settings_eh_clear_igneous,
|
dismissText = R.string.settings_eh_clear_igneous,
|
||||||
showCancelButton = cookies.last().second != null,
|
showCancelButton = cookies.last().second != null,
|
||||||
onCancelButtonClick = { EhCookieStore.clearIgneous() },
|
onCancelButtonClick = { EhCookieStore.clearIgneous() },
|
||||||
|
secure = hasSignedIn,
|
||||||
) {
|
) {
|
||||||
if (signin) {
|
if (hasSignedIn) {
|
||||||
Column {
|
Column {
|
||||||
val warning = stringResource(id = R.string.settings_eh_identity_cookies_signed)
|
val warning = stringResource(id = R.string.settings_eh_identity_cookies_signed)
|
||||||
val str = cookies.joinToString("\n") { (k, v) -> "$k: $v" }
|
val str = cookies.joinToString("\n") { (k, v) -> "$k: $v" }
|
||||||
@@ -144,17 +153,15 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (signin) {
|
if (hasSignedIn) {
|
||||||
val placeholder = stringResource(id = R.string.please_wait)
|
val placeholder = stringResource(id = R.string.please_wait)
|
||||||
val resetImageLimitSucceed = stringResource(id = R.string.reset_limits_succeed)
|
val resetImageLimitSucceed = stringResource(id = R.string.reset_limits_succeed)
|
||||||
var result by rememberSaveable { mutableStateOf<HomeParser.Result?>(null) }
|
var result by rememberSaveable { mutableStateOf<HomeParser.Result?>(null) }
|
||||||
var error by rememberSaveable { mutableStateOf<String?>(null) }
|
var error by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
val summary by rememberUpdatedState(
|
val summary by rememberUpdatedState(
|
||||||
stringResource(
|
result?.run {
|
||||||
id = R.string.image_limits_summary,
|
stringResource(id = R.string.image_limits_summary, limits.current, limits.maximum)
|
||||||
result?.run { limits.current } ?: 0,
|
} ?: placeholder,
|
||||||
result?.run { limits.maximum } ?: 0,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
suspend fun getImageLimits() {
|
suspend fun getImageLimits() {
|
||||||
result = EhEngine.getImageLimits()
|
result = EhEngine.getImageLimits()
|
||||||
@@ -182,10 +189,28 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
|||||||
error?.let {
|
error?.let {
|
||||||
Text(text = it)
|
Text(text = it)
|
||||||
} ?: result?.let { (limits, funds) ->
|
} ?: result?.let { (limits, funds) ->
|
||||||
Text(
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
text = stringResource(id = R.string.current_limits, summary, limits.resetCost) +
|
LinearProgressIndicator(
|
||||||
"\n" + stringResource(id = R.string.current_funds, "${funds.fundsGP}+", funds.fundsC),
|
progress = { limits.current.toFloat() / limits.maximum },
|
||||||
)
|
modifier = Modifier.height(12.dp).fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Text(text = summary)
|
||||||
|
Text(text = stringResource(id = R.string.reset_cost, limits.resetCost))
|
||||||
|
Text(text = stringResource(id = R.string.current_funds))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceAround,
|
||||||
|
) {
|
||||||
|
FundsItem(
|
||||||
|
type = "GP",
|
||||||
|
amount = funds.gp,
|
||||||
|
)
|
||||||
|
FundsItem(
|
||||||
|
type = "C",
|
||||||
|
amount = funds.credit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -238,11 +263,11 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
|||||||
val items = buildList {
|
val items = buildList {
|
||||||
add(disabled)
|
add(disabled)
|
||||||
add(localFav)
|
add(localFav)
|
||||||
if (signin) {
|
if (hasSignedIn) {
|
||||||
addAll(Settings.favCat)
|
addAll(Settings.favCat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defaultFavSlot = dialogState.showSelectItem(
|
defaultFavSlot = dialogState.awaitSelectItem(
|
||||||
items = items,
|
items = items,
|
||||||
title = R.string.default_favorites_collection,
|
title = R.string.default_favorites_collection,
|
||||||
selected = defaultFavSlot + 2,
|
selected = defaultFavSlot + 2,
|
||||||
@@ -355,7 +380,7 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
|||||||
title = stringResource(id = R.string.settings_eh_metered_network_warning),
|
title = stringResource(id = R.string.settings_eh_metered_network_warning),
|
||||||
value = Settings.meteredNetworkWarning::value,
|
value = Settings.meteredNetworkWarning::value,
|
||||||
)
|
)
|
||||||
if (signin) {
|
if (hasSignedIn) {
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = stringResource(id = R.string.settings_eh_show_jpn_title),
|
title = stringResource(id = R.string.settings_eh_show_jpn_title),
|
||||||
summary = stringResource(id = R.string.settings_eh_show_jpn_title_summary),
|
summary = stringResource(id = R.string.settings_eh_show_jpn_title_summary),
|
||||||
@@ -371,7 +396,7 @@ fun EhScreen(navigator: DestinationsNavigator) {
|
|||||||
Preference(title = pickerTitle) {
|
Preference(title = pickerTitle) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val time = LocalTime.fromSecondOfDay(Settings.requestNewsTime)
|
val time = LocalTime.fromSecondOfDay(Settings.requestNewsTime)
|
||||||
val (hour, minute) = dialogState.showTimePicker(pickerTitle, time.hour, time.minute)
|
val (hour, minute) = dialogState.awaitSelectTime(pickerTitle, time.hour, time.minute)
|
||||||
Settings.requestNewsTime = LocalTime(hour, minute).toSecondOfDay()
|
Settings.requestNewsTime = LocalTime(hour, minute).toSecondOfDay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,9 +215,8 @@ fun FilterScreen(navigator: DestinationsNavigator) {
|
|||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Deferred({ allFilterMap.await() }) { filters ->
|
Deferred({ allFilterMap.await() }) { filters ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).padding(paddingValues),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
// Workaround for https://issuetracker.google.com/332939169
|
contentPadding = paddingValues,
|
||||||
// contentPadding = paddingValues,
|
|
||||||
) {
|
) {
|
||||||
var showTip = true
|
var showTip = true
|
||||||
filters.forEach { (filterMode, filters) ->
|
filters.forEach { (filterMode, filters) ->
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import com.google.accompanist.web.WebView
|
|||||||
import com.google.accompanist.web.rememberWebViewState
|
import com.google.accompanist.web.rememberWebViewState
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.util.setDefaultSettings
|
import com.hippo.ehviewer.util.setDefaultSettings
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
@@ -26,7 +25,6 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
@Composable
|
@Composable
|
||||||
fun MyTagsScreen(navigator: DestinationsNavigator) {
|
fun MyTagsScreen(navigator: DestinationsNavigator) {
|
||||||
val url = EhUrl.myTagsUrl
|
val url = EhUrl.myTagsUrl
|
||||||
LockDrawer(true)
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import com.google.accompanist.web.rememberWebViewState
|
|||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
import com.hippo.ehviewer.ui.LockDrawer
|
|
||||||
import com.hippo.ehviewer.util.setDefaultSettings
|
import com.hippo.ehviewer.util.setDefaultSettings
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
@@ -36,7 +35,6 @@ private const val APPLY_JS = "javascript:(function(){var apply = document.getEle
|
|||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun UConfigScreen(navigator: DestinationsNavigator) {
|
fun UConfigScreen(navigator: DestinationsNavigator) {
|
||||||
LockDrawer(true)
|
|
||||||
val url = EhUrl.uConfigUrl
|
val url = EhUrl.uConfigUrl
|
||||||
val webview = remember { Atomic<WebView?>(null) }
|
val webview = remember { Atomic<WebView?>(null) }
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.hippo.ehviewer.ui.tools
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.autofill.AutofillNode
|
||||||
|
import androidx.compose.ui.autofill.AutofillType
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalAutofill
|
||||||
|
import androidx.compose.ui.platform.LocalAutofillTree
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Modifier.autofill(
|
||||||
|
autofillTypes: List<AutofillType>,
|
||||||
|
onFill: ((String) -> Unit),
|
||||||
|
): Modifier {
|
||||||
|
val autofill = LocalAutofill.current
|
||||||
|
val autofillNode = AutofillNode(onFill = onFill, autofillTypes = autofillTypes)
|
||||||
|
LocalAutofillTree.current += autofillNode
|
||||||
|
|
||||||
|
return onGloballyPositioned {
|
||||||
|
autofillNode.boundingBox = it.boundsInWindow()
|
||||||
|
}.onFocusChanged { focusState ->
|
||||||
|
autofill?.run {
|
||||||
|
if (focusState.isFocused) {
|
||||||
|
requestAutofillForNode(autofillNode)
|
||||||
|
} else {
|
||||||
|
cancelAutofillForNode(autofillNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,14 +6,11 @@ import androidx.compose.foundation.gestures.draggable2D
|
|||||||
import androidx.compose.foundation.gestures.rememberDraggable2DState
|
import androidx.compose.foundation.gestures.rememberDraggable2DState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
import com.hippo.ehviewer.Settings
|
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.math.atan2
|
import kotlin.math.atan2
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
@@ -38,7 +35,7 @@ fun CircularLayout(
|
|||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var delta by remember { mutableFloatStateOf(Settings.favDialogTheta) }
|
var delta by rememberMutableStateInDataStore("FavDialogTheta") { 0F }
|
||||||
val state = rememberDraggable2DState {
|
val state = rememberDraggable2DState {
|
||||||
val prevOfs = currentOfs
|
val prevOfs = currentOfs
|
||||||
val prevTheta = currentTheta
|
val prevTheta = currentTheta
|
||||||
@@ -72,12 +69,10 @@ fun CircularLayout(
|
|||||||
animateDecay(delta, omega, rotateDecay) { d, _ ->
|
animateDecay(delta, omega, rotateDecay) { d, _ ->
|
||||||
delta = d
|
delta = d
|
||||||
}
|
}
|
||||||
Settings.favDialogTheta = delta
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentOfs = null
|
currentOfs = null
|
||||||
currentTheta = null
|
currentTheta = null
|
||||||
Settings.favDialogTheta = delta
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
content = content,
|
content = content,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.hippo.ehviewer.ui.tools
|
package com.hippo.ehviewer.ui.tools
|
||||||
|
|
||||||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
|
|
||||||
val LocalWindowSizeClass = compositionLocalOf<WindowSizeClass> { error("CompositionLocal LocalWindowSizeClass not present!") }
|
val LocalWindowSizeClass = compositionLocalOf<WindowSizeClass> { error("CompositionLocal LocalWindowSizeClass not present!") }
|
||||||
@@ -68,6 +68,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.compose.ui.window.SecureFlagPolicy
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import com.jamal.composeprefs3.ui.ifNotNullThen
|
import com.jamal.composeprefs3.ui.ifNotNullThen
|
||||||
@@ -99,15 +100,10 @@ class DialogState {
|
|||||||
content = null
|
content = null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <R> dialog(crossinline block: @Composable (CancellableContinuation<R>) -> Unit) = suspendCancellableCoroutine { cont ->
|
suspend inline fun <R> dialog(crossinline block: @Composable (CancellableContinuation<R>) -> Unit) = try {
|
||||||
cont.invokeOnCancellation { dismiss() }
|
suspendCancellableCoroutine { cont -> content = { block(cont) } }
|
||||||
val realContinuation = object : CancellableContinuation<R> by cont {
|
} finally {
|
||||||
override fun resumeWith(result: Result<R>) {
|
dismiss()
|
||||||
dismiss()
|
|
||||||
cont.resumeWith(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content = { block(realContinuation) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <R> awaitResult(
|
suspend fun <R> awaitResult(
|
||||||
@@ -279,6 +275,7 @@ class DialogState {
|
|||||||
confirmButtonEnabled: Boolean = true,
|
confirmButtonEnabled: Boolean = true,
|
||||||
showCancelButton: Boolean = true,
|
showCancelButton: Boolean = true,
|
||||||
onCancelButtonClick: () -> Unit = {},
|
onCancelButtonClick: () -> Unit = {},
|
||||||
|
secure: Boolean = false,
|
||||||
text: @Composable (() -> Unit)? = null,
|
text: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
return dialog { cont ->
|
return dialog { cont ->
|
||||||
@@ -299,11 +296,16 @@ class DialogState {
|
|||||||
},
|
},
|
||||||
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
|
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
|
||||||
text = text,
|
text = text,
|
||||||
|
properties = if (secure) {
|
||||||
|
DialogProperties(securePolicy = SecureFlagPolicy.SecureOn)
|
||||||
|
} else {
|
||||||
|
DialogProperties()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showDatePicker(
|
suspend fun awaitSelectDate(
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
initialSelectedDateMillis: Long? = null,
|
initialSelectedDateMillis: Long? = null,
|
||||||
initialDisplayedMonthMillis: Long? = initialSelectedDateMillis,
|
initialDisplayedMonthMillis: Long? = initialSelectedDateMillis,
|
||||||
@@ -370,7 +372,7 @@ class DialogState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showTimePicker(
|
suspend fun awaitSelectTime(
|
||||||
title: String,
|
title: String,
|
||||||
initialHour: Int,
|
initialHour: Int,
|
||||||
initialMinute: Int,
|
initialMinute: Int,
|
||||||
@@ -412,7 +414,7 @@ class DialogState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showSingleChoice(
|
suspend fun awaitSingleChoice(
|
||||||
items: List<String>,
|
items: List<String>,
|
||||||
selected: Int,
|
selected: Int,
|
||||||
@StringRes title: Int? = null,
|
@StringRes title: Int? = null,
|
||||||
@@ -438,14 +440,14 @@ class DialogState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showSelectItem(
|
suspend fun awaitSelectItem(
|
||||||
items: List<String>,
|
items: List<String>,
|
||||||
@StringRes title: Int? = null,
|
@StringRes title: Int? = null,
|
||||||
selected: Int = -1,
|
selected: Int = -1,
|
||||||
respectDefaultWidth: Boolean = true,
|
respectDefaultWidth: Boolean = true,
|
||||||
) = showSelectItem(items, title?.right(), selected, respectDefaultWidth)
|
) = awaitSelectItem(items, title?.right(), selected, respectDefaultWidth)
|
||||||
|
|
||||||
suspend fun showSelectItem(
|
suspend fun awaitSelectItem(
|
||||||
items: List<String>,
|
items: List<String>,
|
||||||
title: Either<String, Int>?,
|
title: Either<String, Int>?,
|
||||||
selected: Int = -1,
|
selected: Int = -1,
|
||||||
@@ -471,17 +473,17 @@ class DialogState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun showSelectActions(
|
suspend inline fun awaitSelectAction(
|
||||||
@StringRes title: Int? = null,
|
@StringRes title: Int? = null,
|
||||||
selected: Int = -1,
|
selected: Int = -1,
|
||||||
builder: ActionScope.() -> Unit,
|
builder: ActionScope.() -> Unit,
|
||||||
) {
|
): suspend () -> Unit {
|
||||||
val (items, actions) = buildList { builder(ActionScope { action, that -> add(action to that) }) }.unzip()
|
val (items, actions) = buildList { builder(ActionScope { action, that -> add(action to that) }) }.unzip()
|
||||||
val index = showSelectItem(items, title, selected)
|
val index = awaitSelectItem(items, title, selected)
|
||||||
actions[index].invoke()
|
return actions[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showSelectItemWithCheckBox(
|
suspend fun awaitSelectItemWithCheckBox(
|
||||||
items: List<String>,
|
items: List<String>,
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
@StringRes checkBoxText: Int,
|
@StringRes checkBoxText: Int,
|
||||||
@@ -513,7 +515,7 @@ class DialogState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showSelectItemWithIcon(
|
suspend fun awaitSelectItemWithIcon(
|
||||||
items: List<Pair<ImageVector, Int>>,
|
items: List<Pair<ImageVector, Int>>,
|
||||||
title: String,
|
title: String,
|
||||||
): Int = showNoButton {
|
): Int = showNoButton {
|
||||||
@@ -536,7 +538,7 @@ class DialogState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun showSelectItemWithIconAndTextField(
|
suspend fun awaitSelectItemWithIconAndTextField(
|
||||||
items: List<Pair<ImageVector, String>>,
|
items: List<Pair<ImageVector, String>>,
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
@StringRes hint: Int,
|
@StringRes hint: Int,
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
package com.hippo.ehviewer.ui.tools
|
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Reorder
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import com.hippo.ehviewer.util.isAtLeastOMR1
|
|
||||||
import com.hippo.ehviewer.util.isAtLeastR
|
|
||||||
import com.hippo.ehviewer.util.isAtLeastU
|
|
||||||
import sh.calvin.reorderable.ReorderableItemScope
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ReorderableItemScope.DragHandle(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onDragStarted: () -> Unit = {},
|
|
||||||
onDragStopped: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
val view = LocalView.current
|
|
||||||
IconButton(
|
|
||||||
onClick = {},
|
|
||||||
modifier = modifier.draggableHandle(
|
|
||||||
onDragStarted = {
|
|
||||||
val feedbackConstant = if (isAtLeastU) {
|
|
||||||
HapticFeedbackConstants.DRAG_START
|
|
||||||
} else if (isAtLeastR) {
|
|
||||||
HapticFeedbackConstants.GESTURE_START
|
|
||||||
} else if (isAtLeastOMR1) {
|
|
||||||
HapticFeedbackConstants.KEYBOARD_PRESS
|
|
||||||
} else {
|
|
||||||
HapticFeedbackConstants.VIRTUAL_KEY
|
|
||||||
}
|
|
||||||
view.performHapticFeedback(feedbackConstant)
|
|
||||||
onDragStarted()
|
|
||||||
},
|
|
||||||
onDragStopped = {
|
|
||||||
val feedbackConstant = if (isAtLeastR) {
|
|
||||||
HapticFeedbackConstants.GESTURE_END
|
|
||||||
} else if (isAtLeastOMR1) {
|
|
||||||
HapticFeedbackConstants.KEYBOARD_RELEASE
|
|
||||||
} else {
|
|
||||||
HapticFeedbackConstants.VIRTUAL_KEY
|
|
||||||
}
|
|
||||||
view.performHapticFeedback(feedbackConstant)
|
|
||||||
onDragStopped()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Reorder,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val draggingHapticFeedback = if (isAtLeastU) {
|
|
||||||
HapticFeedbackConstants.SEGMENT_FREQUENT_TICK
|
|
||||||
} else if (isAtLeastOMR1) {
|
|
||||||
HapticFeedbackConstants.TEXT_HANDLE_MOVE
|
|
||||||
} else {
|
|
||||||
HapticFeedbackConstants.CLOCK_TICK
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.hippo.ehviewer.ui.tools
|
||||||
|
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.MenuAnchorType
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DropdownFilterChip(
|
||||||
|
label: String,
|
||||||
|
menuItems: List<String>,
|
||||||
|
selectedItemIndex: Int,
|
||||||
|
onSelectedItemIndexChange: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }, modifier = modifier) {
|
||||||
|
FilterChip(
|
||||||
|
selected = selectedItemIndex != 0,
|
||||||
|
onClick = {},
|
||||||
|
label = {
|
||||||
|
Text(text = if (selectedItemIndex == 0) label else menuItems[selectedItemIndex])
|
||||||
|
},
|
||||||
|
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }, matchTextFieldWidth = false) {
|
||||||
|
menuItems.forEachIndexed { index, item ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = item) },
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
onSelectedItemIndexChange(index)
|
||||||
|
},
|
||||||
|
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.hippo.ehviewer.ui.tools
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.View
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastOMR1
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastR
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastU
|
||||||
|
|
||||||
|
enum class HapticFeedbackType {
|
||||||
|
START,
|
||||||
|
MOVE,
|
||||||
|
END,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HapticFeedback {
|
||||||
|
fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberHapticFeedback(): HapticFeedback {
|
||||||
|
val view = LocalView.current
|
||||||
|
return remember(view) { AndroidHapticFeedback(view) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidHapticFeedback(private val view: View) : HapticFeedback {
|
||||||
|
override fun performHapticFeedback(hapticFeedbackType: HapticFeedbackType) {
|
||||||
|
val feedbackConstant = when (hapticFeedbackType) {
|
||||||
|
HapticFeedbackType.START -> {
|
||||||
|
if (isAtLeastU) {
|
||||||
|
HapticFeedbackConstants.DRAG_START
|
||||||
|
} else if (isAtLeastR) {
|
||||||
|
HapticFeedbackConstants.GESTURE_START
|
||||||
|
} else if (isAtLeastOMR1) {
|
||||||
|
HapticFeedbackConstants.KEYBOARD_PRESS
|
||||||
|
} else {
|
||||||
|
HapticFeedbackConstants.VIRTUAL_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HapticFeedbackType.MOVE -> {
|
||||||
|
if (isAtLeastU) {
|
||||||
|
HapticFeedbackConstants.SEGMENT_FREQUENT_TICK
|
||||||
|
} else if (isAtLeastOMR1) {
|
||||||
|
HapticFeedbackConstants.TEXT_HANDLE_MOVE
|
||||||
|
} else {
|
||||||
|
HapticFeedbackConstants.CLOCK_TICK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HapticFeedbackType.END -> {
|
||||||
|
if (isAtLeastR) {
|
||||||
|
HapticFeedbackConstants.GESTURE_END
|
||||||
|
} else if (isAtLeastOMR1) {
|
||||||
|
HapticFeedbackConstants.KEYBOARD_RELEASE
|
||||||
|
} else {
|
||||||
|
HapticFeedbackConstants.VIRTUAL_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.performHapticFeedback(feedbackConstant)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user