Compare commits

...
138 Commits
Author SHA1 Message Date
FooIbar 71a4e84d7c Prepare for release 1.11.4
Releases / build (push) Has been cancelled
2024-04-21 22:45:54 +08:00
FooIbar d6c36146ba Fix losing reading progress on reading mode change 2024-04-21 22:35:22 +08:00
FooIbar 9013778f58 More workaround 2024-04-21 18:05:25 +08:00
renovate[bot] 1af03dd145 Update dependency sh.calvin.reorderable:reorderable to v1.5.1 2024-04-20 16:46:53 +00:00
FooIbar b83e0fd092 Workaround for lazy layout lagging (#1042) 2024-04-20 16:37:41 +00:00
GG Bond 1f8d786d05 Simplify to use Modifier.offset 2024-04-20 18:31:22 +08:00
GG Bond 5d87a4ada0 Never have a clipped shadow 2024-04-20 18:31:22 +08:00
GG Bond 4d4fb1e398 Eliminate unnecessary Column 2024-04-20 17:10:33 +08:00
GG Bond 05eea54f06 Fix primaryFab appear anim 2024-04-20 17:10:33 +08:00
GG Bond a00d9ef765 Mitigate shadow being clipped 2024-04-20 17:10:33 +08:00
GG Bond aa6ddac9c5 Avoid unnecessary zIndex 2024-04-20 17:10:33 +08:00
GG Bond 9a6f17c745 Image Search Card doesn't need to be scrollable now 2024-04-20 17:10:33 +08:00
GG Bond 980977aaf3 Ensure Image Search Card's shadow not being clipped 2024-04-20 17:10:33 +08:00
GG Bond 3ed0afaf91 Restore an alpha usage can be safely applied 2024-04-20 17:10:33 +08:00
GG Bond 250adb4b33 Fix shadow being clipped 2024-04-20 17:10:33 +08:00
GG Bond 0baf09cb53 Codestyle fix for DownloadCard 2024-04-20 17:10:33 +08:00
FooIbar d04a914b9b Tweak gallery card alignment and ensure min height 2024-04-19 19:08:21 +08:00
renovate[bot] 59775cb27c Update dependency com.android.application to v8.5.0-alpha06 2024-04-18 22:06:24 +00:00
GG Bond fb76cd7b1b Move compose out of snapshot 2024-04-18 11:25:52 +08:00
renovate[bot] 52fb5306b6 Update dependency dev.chrisbanes.compose:compose-bom to v2024.04.00-alpha02 2024-04-18 01:03:11 +00:00
renovate[bot] 74f7d95718 Update AndroidX & Jetpack Compose 2024-04-17 18:12:27 +00:00
renovate[bot] bcc1b78c7c Update compose snapshot 2024-04-18 00:00:56 +08:00
FooIbar 3978abea87 Fix UniFile.fromUri() for 2 path segments tree uri 2024-04-17 17:38:51 +08:00
FooIbar 556b726349 Update to follow the latest material slider specs
Tick size is 4 dp now.
2024-04-16 10:49:10 +08:00
FooIbar e0f16f31e6 Automatically backup data before updating 2024-04-16 00:08:30 +08:00
FooIbar b39e24467c Fix UniFile.fromUri for children of tree uri 2024-04-15 23:50:54 +08:00
FooIbar 65613f07ba Optimize DownloadCard interaction in select mode 2024-04-15 21:53:37 +08:00
FooIbar 666a7e4d77 Allow disable list item animations on API 29+ (#1024)
* Allow disable list item animations on API 29+

* Update list item animations setting description
2024-04-15 14:54:13 +08:00
renovate[bot] cf08bfdd9b Update dependency me.saket.telephoto:zoomable to v0.10.0 2024-04-14 21:58:40 +00:00
renovate[bot]andFooIbar 1f47d56c55 Update Kotlin, KSP and Compose Compiler (#1009)
* Update Kotlin, KSP and Compose Compiler

* Migrate to new compose compiler gradle plugin

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2024-04-14 14:38:22 +00:00
FooIbar 1a7dde848f Don't treat warnings as errors on debug build 2024-04-14 17:56:55 +08:00
GG Bond c8207308a8 Update predictive back animations to use interpolation curve of (0.1, 0.1, 0, 1) 2024-04-14 17:38:03 +08:00
renovate[bot] f23020cc6f Update dependency sh.calvin.reorderable:reorderable to v1.5.0 2024-04-12 18:46:12 +00:00
renovate[bot] 555e487f1e Update dependency com.android.application to v8.5.0-alpha05 2024-04-11 18:20:21 +00:00
renovate[bot] 107bb0b15d Update aboutlibraries to v11.1.3 2024-04-09 19:14:59 +00:00
FooIbar 87a2de393e Tweak select item dialog style 2024-04-10 03:04:49 +08:00
renovate[bot] a7e4412454 Update dependency com.mikepenz.aboutlibraries.plugin to v11.1.2 2024-04-09 14:42:54 +00:00
FooIbar 7dce4cfa8e Update Compose snapshot 2024-04-09 22:12:49 +08:00
FooIbar 2b30aca47c Update R8 and remove workaround 2024-04-09 21:44:57 +08:00
FooIbar 0073d2862b Fixed crash on deleting a label after renaming it 2024-04-09 18:39:25 +08:00
FooIbar b182c97185 Fix titleless select item dialog top padding 2024-04-09 10:46:44 +08:00
renovate[bot] a32119bada Update dependency com.android.application to v8.5.0-alpha04 2024-04-08 19:37:35 +00:00
FooIbar a928cfd066 Update libarchive to v3.7.3 2024-04-09 00:08:01 +08:00
FooIbar 8c7a371336 Highlight selected item in select item dialog 2024-04-07 23:32:47 +08:00
GG Bond 6f61eb4946 Update outdated comments 2024-04-07 21:21:37 +08:00
GG Bond 1096f05344 Move navigation out of snapshot 2024-04-07 21:21:37 +08:00
GG Bond 26f96d3e38 Move to telephoto 0.10.0-SNAPSHOT 2024-04-07 21:21:37 +08:00
FooIbar 4a02189a59 Rearrange code to move plurals resources together 2024-04-07 17:13:35 +08:00
FooIbar 7a5bab41f6 Add option to disable list item animations (#997)
It may crash on some devices.
2024-04-07 13:57:27 +08:00
FooIbar 84c32fb28d Prepare for next iteration 2024-04-06 17:10:28 +08:00
FooIbar a3cb6d2782 Prepare for release 1.11.3
Releases / build (push) Has been cancelled
2024-04-06 17:09:38 +08:00
FooIbar 45be957df5 Workaround for comment list lagging at the bottom 2024-04-06 15:25:26 +08:00
FooIbar 8e45014e3f Align load comments button and progress indicator 2024-04-06 15:24:54 +08:00
FooIbar d84e948352 Add padding for progress indicator in DetailScreen 2024-04-06 15:24:54 +08:00
FooIbar 68bd835038 Fallback to transferTo if sendfile failed 2024-04-06 10:52:25 +08:00
FooIbar c297bebea1 Fix resource leak when decode job is canceled 2024-04-05 20:29:04 +08:00
FooIbar 6c0a823216 Fix typo (#991)
* Fix typo

* Fix lint
2024-04-05 06:57:34 +00:00
renovate[bot]andFooIbar 067a701bdc Update KSP to v2.0.0-Beta5-1.0.20 (#989)
* Update Kotlin, KSP and Compose Compiler

* Fix compose compiler version

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2024-04-05 05:03:58 +00:00
GG Bond c3fb008c92 Introduce HttpStatement.executeSafely 2024-04-05 13:03:39 +08:00
GG Bond bc4321da2e Cleanup CronetEngine 2024-04-05 13:03:39 +08:00
FooIbar 8dec040803 Fix FileNotFoundException on saving download thumb 2024-04-05 00:04:02 +08:00
FooIbar 29e4e77c44 Open file descriptor in "wt" mode for overwriting 2024-04-05 00:04:02 +08:00
FooIbar 11429ef7fe Log image loading errors in release build 2024-04-05 00:04:02 +08:00
FooIbar f5fcd96a1e Fix DownloadService stopped during onFinish call 2024-04-04 19:17:35 +08:00
renovate[bot]andFooIbar 19120038c5 Update AndroidX & Jetpack Compose (#985)
* Update AndroidX & Jetpack Compose

* Fix nullability

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2024-04-04 06:19:08 +00:00
FooIbar 9021f28ead Allow update to unstable AndroidX libraries
Most of them are already transitively updated.
2024-04-04 13:52:50 +08:00
renovate[bot] 62934f72e3 Update dependency dev.chrisbanes.compose:compose-bom to v2024.04.00-alpha01 2024-04-03 23:36:15 +00:00
renovate[bot] 6c534e1049 Update dependency androidx.paging:paging-compose to v3.3.0-beta01 2024-04-03 19:00:38 +00:00
renovate[bot] f80cd90792 Update dependency androidx.core:core-splashscreen to v1.1.0-rc01 2024-04-03 19:00:00 +00:00
renovate[bot] 3999c0c114 Update dependency io.arrow-kt:arrow-stack to v1.2.4 2024-04-03 13:21:01 +00:00
FooIbar 59399453c4 Fix IP ban error message and parse error logging (#979) 2024-04-03 08:56:40 +00:00
renovate[bot] 694bb19e2a Update dependency sh.calvin.reorderable:reorderable to v1.4.0 2024-04-03 06:25:14 +00:00
FooIbar 9928ecd4d9 Refactor database export 2024-04-03 11:54:06 +08:00
renovate[bot] 2c5293af6e Update dependency com.android.application to v8.5.0-alpha03 2024-04-02 13:39:05 +00:00
FooIbar 9569ca774d Fix unable to refresh images due to resource leak 2024-04-02 00:38:38 +08:00
FooIbar 3a04460926 Abort if no image entries found in archive 2024-04-01 18:18:19 +08:00
FooIbar acb4fb6166 Don't run decode job if fetch job failed
Fix fetch error being suppressed by decode job result.
2024-03-31 20:39:27 +08:00
renovate[bot]andGG Bond 7d137bb016 Fast move to compose destinations v2.1 (#965)
* Fast move to compose destinations v2.1

* Remove default parameters

* Cleanup

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2024-03-31 16:07:06 +08:00
FooIbar 8c2ed37b72 Update build script due to xz Github repo shutdown 2024-03-31 01:31:22 +08:00
FooIbar 749b230250 Fix download label not being draggable 2024-03-30 10:19:08 +08:00
renovate[bot]andGG Bond 23531cb6e0 Upstream SearchBar (#960)
* Upstream SearchBar

* Fix SearchBar label

* Fix SearchBar padding

* Use BoxWithConstraints

* Refine

* Refine

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2024-03-29 10:01:42 +00:00
FooIbar 0ac11dbe0b Fix SpiderQueen not respecting FIFO order
And optimize preloading strategy.
2024-03-29 17:50:53 +08:00
FooIbar 9b0e017542 Use Compose UI implementation of parseAsHtml 2024-03-28 15:48:28 +08:00
FooIbar f2f5fcacf9 Use upstream navigation drawer 2024-03-28 15:48:16 +08:00
FooIbar 52ded265f3 Update Compose to snapshot and remove workaround 2024-03-28 08:41:01 +08:00
FooIbar e77f905c4a Revert "Downgrade to stable AGP"
This reverts commit 2d14d34d53.

Reason for revert: Workaround added
2024-03-28 08:39:15 +08:00
FooIbar 2d14d34d53 Downgrade to stable AGP 2024-03-27 13:26:34 +08:00
renovate[bot] 78facda18a Crossinline (#952) 2024-03-26 22:32:42 +08:00
renovate[bot]andrenovate[bot] 1fe1b43015 Update Kotlin, KSP and Compose Compiler (#950)
* Update Kotlin, KSP and Compose Compiler

* Update libs.versions.toml

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <99822064+revonateB0T@users.noreply.github.com>
2024-03-26 14:19:00 +00:00
FooIbar 63164dc70a Scale wide images (banners) to fit target width
And don't scale tall images (webtoons).
2024-03-26 15:50:45 +08:00
renovate[bot] 0d39092a7f Update dependency me.saket.telephoto:zoomable to v0.9.0 2024-03-26 00:34:29 +00:00
renovate[bot] c50db8a327 Update plugin composeCompilerReportGenerator to v1.2.0 2024-03-25 11:58:03 +00:00
renovate[bot] 7e107cc5e2 Update dependency com.mikepenz.aboutlibraries.plugin to v11.1.1 2024-03-23 15:15:45 +00:00
renovate[bot] 260949e3a7 Update dependency gradle to v8.7 2024-03-22 20:28:33 +00:00
renovate[bot] d9ec108721 Update dependency com.android.application to v8.5.0-alpha02 2024-03-22 20:25:51 +00:00
FooIbar 1e97407936 Fix recycled item's height being 0 in webtoon mode
Refactor reader page holder and miscellaneous cleanups
2024-03-22 22:10:00 +08:00
FooIbar 73afe05a18 Manually migrate from schema 17 to 18 for non-null 2024-03-22 20:59:45 +08:00
FooIbar 23475de932 Fix layout inspector not working 2024-03-21 20:31:10 +08:00
FooIbar 6fb2172c2c Drop obsolete snapshot 2024-03-21 19:02:34 +08:00
renovate[bot] 9226b71119 Update dependency dev.chrisbanes.compose:compose-bom to v2024.03.00-alpha02 2024-03-21 10:42:27 +00:00
FooIbar 58e38b58bf Fix renovate configuration
[no ci]
2024-03-21 18:18:29 +08:00
renovate[bot] db0d110627 Update dependency androidx.paging:paging-compose to v3.3.0-alpha05 2024-03-21 10:17:09 +00:00
FooIbar 08812bf7ff Tweak renovate configuration
[no ci]
2024-03-21 18:09:13 +08:00
renovate[bot] daf7f8ffca Update dependency com.android.application to v8.5.0-alpha01 2024-03-21 09:52:03 +00:00
FooIbar 74564fa07f Tweak renovate configuration
[no ci]
2024-03-21 17:41:35 +08:00
FooIbar fc51d3fc81 Allow update to unstable AGP and Compose BOM
[no ci]
2024-03-21 17:32:50 +08:00
renovate[bot]andFooIbar c5d5b8abd2 Update dependency androidx.navigation:navigation-compose to v2.8.0-alpha05 (#929)
* Update dependency androidx.navigation:navigation-compose to v2.8.0-alpha05

* Fix deprecated

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2024-03-21 17:31:54 +08:00
FooIbar 193badccb8 Fix crash when reading galleries from gallery list 2024-03-21 16:52:17 +08:00
FooIbar 2653d0568a Fix false positive update notifications 2024-03-21 16:31:48 +08:00
FooIbar adf5a65371 Group Kotlin, KSP and Compose Compiler
[no ci]
2024-03-21 15:54:08 +08:00
FooIbar 5b59dabc66 Fix generating simple language from title 2024-03-21 15:19:46 +08:00
GG Bond f7259c5b40 Individual zoomableState per pager item 2024-03-17 14:56:44 +08:00
FooIbar 6f9fceaef8 Prepare for next iteration 2024-03-17 14:45:19 +08:00
FooIbar 6a64ff51ed Prepare for release 1.11.2
Releases / build (push) Has been cancelled
2024-03-17 14:44:16 +08:00
FooIbar ea9d9cfba7 Prepare changelog for 1.11.2 2024-03-17 14:41:43 +08:00
FooIbar 75c6561896 Fix app updater getting wrong download link 2024-03-16 20:13:42 +08:00
FooIbar 8f8db82e9d Fix disowned gallery uploader parsing (#918)
* Fix disowned gallery uploader parsing

* Run rustfmt

* Refine

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

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

* Remove WriteChannelContent as no user now

* Cleanup

* Cleanup

* Remove unnecessary invokeOnCancellation

* Refine

* Refine

* Format

---------

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

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

No files matched your search

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