Compare commits

...
190 Commits
Author SHA1 Message Date
FooIbar 0195f1c9b8 Prepare for release 1.14.6
Releases / build (push) Has been cancelled
2025-12-17 22:12:05 +08:00
renovate[bot] 18d2d992a4 Update dependency io.arrow-kt:arrow-stack to v2.2.1.1 (#2800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 13:44:04 +00:00
FooIbar 5582353c44 Enable group key based Compose stack traces 2025-12-17 15:08:04 +08:00
renovate[bot] 464568f29f Update dependency com.google.devtools.ksp to v2.3.4 (#2798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 20:09:01 +00:00
renovate[bot]andFooIbar 213a9d56d2 Update kotlin monorepo to v2.3.0 (#2797)
* Update kotlin monorepo to v2.3.0

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-12-16 11:15:39 +00:00
FooIbar f672462bcd Use WebView version for UA to bypass Cloudflare 2025-12-16 00:29:36 +08:00
renovate[bot] 9a0b0d2c65 Lock file maintenance (#2795)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 02:04:38 +00:00
renovate[bot] fcaf4e3182 Update actions/upload-artifact action to v6 (#2792)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 20:59:34 +00:00
renovate[bot] ea40fa9b05 Update dessant/lock-threads action to v6 (#2791)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 16:39:18 +00:00
renovate[bot] 2e3436dbe7 Update actions/cache action to v5 (#2790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 04:02:53 +00:00
renovate[bot] bd759dedea Update AGP to v8.13.2 (#2788)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 09:09:24 +00:00
FooIbar bac78ce018 Improve category text contrast 2025-12-05 21:41:29 +08:00
FooIbar dd95d7aaa2 Reapply "Use the upstream PaddingValues.plus"
This reverts commit e8b5fd0425.
2025-12-04 21:13:57 +08:00
renovate[bot] d1bb5b1d87 Cleanup snapshot workaround (#2782)
* Update settings.gradle.kts

* Update libs.versions.toml
2025-12-04 11:22:31 +08:00
renovate[bot] 67a3252b05 Update dependency androidx.compose:compose-bom-alpha to v2025.12.00 (#2781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 21:02:24 +00:00
renovate[bot] 2df08f022f Update dependency androidx.test.uiautomator:uiautomator to v2.4.0-alpha07 (#2780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 21:01:00 +00:00
renovate[bot] 65673a0ed9 Update dependency androidx.compose.foundation:foundation to v1.10.0 (#2779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 18:24:57 +00:00
renovate[bot] aef20e2e04 Update dependency androidx.activity:activity-compose to v1.12.1 (#2778)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 18:23:46 +00:00
FooIbar 3bbfc8ac3a Use different icon for finished download items 2025-12-03 11:05:21 +08:00
renovate[bot] 02e85a8c4d Update dependency chrome to v143 (#2776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 21:57:07 +00:00
FooIbar 8cc531ccec Fix database export 2025-12-02 10:00:48 +08:00
FooIbar 30fc750e17 Show tag content on long click to avoid misclicks 2025-12-02 10:00:32 +08:00
AdiEcho 0ff40d3d38 Add show reading progress function (#2768)
* Add reading progress

* Optimize reading progress state collection and display logic

* Optimize reading progress state collection and add page display to downloads

* Enhance download progress display and settings

* Optimize state collection for read progress, revert arrangement

* Revert arrangement changement
2025-12-02 01:01:25 +08:00
renovate[bot] 9fa80db783 Update dependency libarchive to v3.8.4 (#2773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 15:12:35 +00:00
renovate[bot] 357503b403 Lock file maintenance (#2770)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 01:06:56 +00:00
FooIbar 10f7b2a7a9 KMP EhCookieStore 2025-11-29 15:08:32 +08:00
FooIbar 99696b11db Migrate to SQLite Driver APIs 2025-11-29 15:08:24 +08:00
FooIbar 71291b8800 Fix download state on archive failure 2025-11-27 20:48:52 +08:00
renovate[bot] 3532dbdcd6 Update dependency io.ktor:ktor-client-okhttp to v3.3.3 (#2765)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 15:32:05 +00:00
renovate[bot] 589ba8432c Update dependency org.jetbrains.kotlinx:kotlinx-io-core to v0.8.2 (#2764)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 21:09:00 +00:00
renovate[bot] 273196bf5d Update dependency Corrosion to v0.6 (#2763)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 13:53:02 +00:00
renovate[bot] a5fadeee3c Update dependency org.jetbrains.kotlinx:kotlinx-io-core to v0.8.1 (#2762)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-21 22:49:16 +00:00
FooIbar 3f784ec1de Refine FFI error handling 2025-11-21 09:18:11 +08:00
renovate[bot] f7a70cdfc3 Update dependency androidx.datastore:datastore-preferences to v1.2.0 (#2760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-21 01:02:00 +00:00
renovate[bot] 030100280f Update dependency com.materialkolor:material-kolor to v4.0.5 (#2759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 22:38:28 +00:00
renovate[bot] 0d76ee43fd Update dependency com.google.devtools.ksp to v2.3.3 (#2758)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 22:38:07 +00:00
renovate[bot] 11a215f070 Update actions/checkout action to v6 (#2757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 17:12:09 +00:00
FooIbar a6e61e07fc Update Compose snaoshot (#2756) 2025-11-21 00:00:29 +08:00
renovate[bot] da94d298ce Update dependency androidx.activity:activity-compose to v1.12.0 (#2753)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 02:05:04 +00:00
renovate[bot] 9e74db0f02 Update dependency androidx.compose:compose-bom-alpha to v2025.11.01 (#2754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 02:04:47 +00:00
renovate[bot] 0d63c2fa3c Update androidx.room to v2.8.4 (#2752)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 21:01:03 +00:00
renovate[bot] bf44fde58e Update androidx.lifecycle to v2.10.0 (#2751)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 21:00:42 +00:00
renovate[bot] 4bc119138f Update moko.resources to v0.25.2 (#2750)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 17:57:46 +00:00
renovate[bot] c7f5b86843 Update spotless to v8.1.0 (#2749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 01:37:36 +00:00
renovate[bot] 2aa815dce1 Update dependency com.squareup.okhttp3:okhttp-bom to v5.3.2 (#2748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 01:36:52 +00:00
FooIbar cf7f18351d Simplify build script for Nettle 2025-11-18 23:45:22 +08:00
FooIbar 5ed27963d7 Block HyperOS 1 & 2 for Cronet 2025-11-18 23:45:11 +08:00
renovate[bot] 85e539f0ba Update Gradle to v9.2.1 (#2745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 08:17:05 +00:00
renovate[bot] 15ecb0e3f4 Update dependency libarchive to v3.8.3 (#2744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 23:56:04 +00:00
renovate[bot] db5055da6e Update dependency com.squareup.okio:okio to v3.16.4 (#2743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 23:55:24 +00:00
FooIbar 09e201f3f2 Fix CFLAGS changes not propagating to nettle build 2025-11-17 14:55:07 +08:00
renovate[bot] 6f34083880 Update dependency com.squareup.okhttp3:okhttp-bom to v5.3.1 (#2740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 18:10:47 +00:00
FooIbar 4e0c42c955 Use -ffile-prefix-map to improve reproducibility 2025-11-16 21:49:17 +08:00
FooIbar 935b7dec0c Reduce binary size and improve reproducibility 2025-11-16 15:34:49 +08:00
FooIbar d0b0651c7d Revert "Enable trim-paths to improve reproducibility"
This reverts commit e131a416e8.
2025-11-16 15:34:49 +08:00
FooIbar e131a416e8 Enable trim-paths to improve reproducibility 2025-11-15 23:47:01 +08:00
FooIbar dd526d33bd Update download URL for Nettle
The GitLab mirror has been archived.
2025-11-15 18:15:37 +08:00
renovate[bot]andFooIbar 4611a77e04 Update dependency com.pinterest.ktlint:ktlint-cli to v1.8.0 (#2729)
* Update dependency com.pinterest.ktlint:ktlint-cli to v1.8.0

* Fix spotless

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-11-15 08:13:23 +00:00
renovate[bot] 806560cf59 Update dependency rust-nightly to v2025-11-15 (#2733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 07:01:25 +00:00
renovate[bot] fd3f6eb124 Lock file maintenance (#2732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 01:03:09 +00:00
renovate[bot] 8e14c6e57b Update dependency com.squareup.okio:okio to v3.16.3 (#2731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 01:02:04 +00:00
FooIbar 20a4f4ab25 Remove trailing spaces and periods from filenames 2025-11-12 23:14:58 +08:00
renovate[bot] d4c64f6e66 Update dependency com.materialkolor:material-kolor to v4.0.4 (#2726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 01:49:24 +00:00
renovate[bot] cfb2f15a1e Update AGP to v8.13.1 (#2724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 18:53:27 +00:00
renovate[bot] 927a738f46 Update dependency com.google.devtools.ksp to v2.3.2 (#2722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 23:07:08 +00:00
renovate[bot] 19482dbd77 Update dependency org.jetbrains.compose to v1.9.3 (#2721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 18:20:14 +00:00
FooIbar 43b7fb611a Migrate CMP dependencies to version catalog
Gradle plugin dependency aliases are deprecated.
2025-11-06 18:20:11 +08:00
renovate[bot] c39338efb7 Update dependency androidx.compose:compose-bom-alpha to v2025.11.00 (#2719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 02:23:30 +00:00
renovate[bot] dbd952eebb Update dependency androidx.core:core-splashscreen to v1.2.0 (#2718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 02:23:04 +00:00
renovate[bot] 192039408c Update androidx.lifecycle to v2.10.0-rc01 (#2716)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 21:24:26 +00:00
renovate[bot] e38376d4b0 Update dependency androidx.activity:activity-compose to v1.12.0-rc01 (#2717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 21:23:25 +00:00
renovate[bot] a4062cf4d1 Update dependency io.ktor:ktor-client-okhttp to v3.3.2 (#2715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 10:15:16 +00:00
FooIbar 3c47d36525 Use snapshot Compose for the popup positioning fix 2025-11-05 18:14:37 +08:00
renovate[bot] 00090944d5 Update dependency com.google.devtools.ksp to v2.3.1 (#2712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 06:50:00 +00:00
renovate[bot] d2cafbb2a3 Update dependency io.arrow-kt:arrow-stack to v2.2.0 (#2710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 16:57:32 +00:00
renovate[bot] 9847b95582 Lock file maintenance (#2709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 01:54:57 +00:00
renovate[bot] 986412932b Update dependency com.squareup.okhttp3:okhttp-bom to v5.3.0 (#2707)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 00:21:38 +00:00
renovate[bot] f13831cb74 Update dependency com.materialkolor:material-kolor to v4.0.3 (#2706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 20:16:27 +00:00
renovate[bot] 5ed1203558 Update Gradle to v9.2.0 (#2705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 00:58:23 +00:00
renovate[bot] acf1fe43bf Update dependency org.jetbrains.compose.material3.adaptive:adaptive to v1.2.0 (#2704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 00:53:24 +00:00
renovate[bot] 4761ef7416 Update dependency org.jetbrains.compose to v1.9.2 (#2703)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 14:05:04 +00:00
renovate[bot] 70aaf38f94 Update xmlutil to v0.91.3 (#2701)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 22:53:18 +00:00
renovate[bot] a92aaf19c9 Update dependency chrome to v142 (#2702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 22:52:56 +00:00
renovate[bot] a5b408d316 Update dependency io.github.petertrr:kotlin-multiplatform-diff to v1.2.0 (#2699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 18:52:48 +00:00
renovate[bot] f96a2e63d1 Update actions/upload-artifact action to v5 (#2698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 23:17:26 +00:00
renovate[bot] a0a9da68da Update dependency com.materialkolor:material-kolor to v4.0.2 (#2697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 23:12:52 +00:00
FooIbar a83be94d1f Fix dropdown menu anchor calculation in dialog 2025-10-23 22:31:47 +08:00
FooIbar cc5dfbfa41 Update screenshots 2025-10-23 19:36:27 +08:00
renovate[bot] 57b79b0b06 Update kotlin monorepo to v2.2.21 (#2694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 09:49:38 +00:00
renovate[bot] 569130087f Update dependency androidx.work:work-runtime to v2.11.0 (#2693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 03:36:38 +00:00
renovate[bot] 1a7b36b488 Update dependency androidx.compose:compose-bom-alpha to v2025.10.01 (#2691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 03:36:18 +00:00
renovate[bot] e8b5fd0425 Revert "Use the upstream PaddingValues.plus"
This reverts commit 656d372612.
2025-10-23 11:25:39 +08:00
renovate[bot] 626d5e7a8c Update dependency androidx.activity:activity-compose to v1.12.0-beta01 (#2690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 22:38:35 +00:00
renovate[bot] 4d94e84c13 Update androidx.room to v2.8.3 (#2689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 19:08:50 +00:00
renovate[bot] 863ec659ff Update androidx.lifecycle to v2.10.0-beta01 (#2688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 19:07:45 +00:00
renovate[bot] 071fdfc0a3 Update dependency com.google.devtools.ksp to v2.3.0 (#2685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-22 10:18:07 +00:00
FooIbar a137e9715a Refactor search bar text handling
* Fix search bar text not cleared after searching in popular / toplist
* Fix search bar text not cleared when quick search keyword is empty
2025-10-22 18:05:02 +08:00
renovate[bot] c28d1d34ff Update dependency com.materialkolor:material-kolor to v4 (#2683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 20:49:43 +00:00
FooIbar 1230022d17 Reset FAB state when gallery list changes 2025-10-20 21:11:47 +08:00
FooIbar 9a9f2a8d70 Prepare for next iteration 2025-10-19 21:19:02 +08:00
FooIbar f51da1a948 Prepare for release 1.14.5
Releases / build (push) Has been cancelled
2025-10-19 21:18:11 +08:00
FooIbar c1ac592855 [Reader] Show gallery title & fix status bar color 2025-10-19 12:41:17 +08:00
FooIbar 371103908b Fix flickering loops in reader
Opt out activity recreation for size-based config changes as they are
handled by Compose.
2025-10-16 19:19:40 +08:00
FooIbar a2a029bb28 Fix unnecessary recompositions
`BaseGalleryInfo` is used as part of remember keys.
2025-10-16 19:19:40 +08:00
renovate[bot] 9266fba415 Update dependency libarchive to v3.8.2 (#2678)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 00:58:36 +00:00
FooIbar 0963e2f564 Fix reader settings crash with custom orientation 2025-10-15 18:11:29 +08:00
renovate[bot] 72a5906a9b Lock file maintenance (#2676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 02:13:59 +00:00
renovate[bot] 4dbd54c495 Update dependency com.squareup.okio:okio to v3.16.2 (#2675)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 20:06:47 +00:00
renovate[bot] 6219acb15c Update dependency org.jetbrains.compose to v1.9.1 (#2672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 13:53:25 +00:00
renovate[bot] c4179bef5d Update aboutlibraries to v13.1.0 (#2670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 20:52:02 +00:00
renovate[bot] 117a1eaab8 Fix -Ofast deprecation 2025-10-13 17:31:15 +08:00
FooIbar 78e31e4fb9 Remove indent from compiler flags 2025-10-13 00:14:07 +08:00
FooIbar 3da86fd745 Prepare for next iteration 2025-10-12 21:32:38 +08:00
FooIbar e0c710ff5f Prepare for release 1.14.4
Releases / build (push) Has been cancelled
2025-10-12 21:32:02 +08:00
FooIbar 2946459f1d Prepare changelog for 1.14.4 (#2665)
[no ci]
2025-10-12 21:30:55 +08:00
renovate[bot] 5b5f328e06 Lock file maintenance (#2667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 13:22:31 +00:00
FooIbar b496e18e7a Revert "Use to the new panic-immediate-abort cargo feature"
This reverts commit cf10e2ff6d.

Reason for revert: Incompatible with renovate
2025-10-12 21:08:34 +08:00
renovate[bot] 4abb2ab0ce Update compose.destinations to v2.3.0 (#2664)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 09:40:33 +00:00
FooIbar 16edaacf37 Temporarily disable backupBeforeUpdate by default 2025-10-12 12:59:35 +08:00
FooIbar b69aea9d14 Update AndroidX WorkManager to 2.11.0-rc01 2025-10-12 12:07:02 +08:00
FooIbar 656d372612 Use the upstream PaddingValues.plus 2025-10-12 12:04:30 +08:00
FooIbar 09a2ebcc61 Workaround crash when 0-sized TextField is focused
It only affects API < 28
https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
2025-10-11 10:21:44 +08:00
renovate[bot]andFooIbar b74d0e9006 Update dependency com.mikepenz.aboutlibraries.plugin to v13 (#2658)
* Update dependency com.mikepenz.aboutlibraries.plugin to v13

* Fix build

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-10-10 14:10:30 +00:00
renovate[bot] a7c81130cd Update dependency com.squareup.okhttp3:okhttp-bom to v5.2.1 (#2657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 04:46:04 +00:00
renovate[bot] c0b75cdbe9 Update dependency com.squareup.okio:okio to v3.16.1 (#2656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 00:49:53 +00:00
FooIbar 2c15c74697 Fix visibility calculation with zoomable content 2025-10-09 21:52:39 +08:00
FooIbar ba9308bfbc Underline solid tags when opted in 2025-10-09 21:28:39 +08:00
renovate[bot] 21050b49d4 Update dependency androidx.compose:compose-bom-alpha to v2025.10.00 (#2651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 21:46:22 +00:00
renovate[bot] 842da49b08 Update androidx.lifecycle to v2.10.0-alpha05 (#2649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 19:28:37 +00:00
renovate[bot] 44fb2b70a5 Update androidx.room to v2.8.2 (#2650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 19:28:12 +00:00
renovate[bot] 9ef95f56cf Update dependency io.ktor:ktor-client-okhttp to v3.3.1 (#2648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 10:56:43 +00:00
renovate[bot] b5b08404bd Update dependency NDK to v29 (#2647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 05:04:11 +00:00
renovate[bot] 17abca7f7d Update dependency com.google.devtools.ksp to v2.2.20-2.0.4 (#2646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 05:02:34 +00:00
renovate[bot] c8d4782b2a Update dependency com.squareup.okhttp3:okhttp-bom to v5.2.0 (#2645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 17:53:41 +00:00
FooIbar e0af610c2c Clean up rust build-std flags 2025-10-06 22:36:17 +08:00
FooIbar ddc645c323 Speed up sorting download list by page count 2025-10-06 09:42:32 +08:00
FooIbar 779dd1f8fd Reduce DownloadManager initialization time 2025-10-06 09:42:32 +08:00
FooIbar cf10e2ff6d Use to the new panic-immediate-abort cargo feature 2025-10-05 19:38:52 +08:00
renovate[bot] 55d1094e55 Update dependency me.saket.telephoto:zoomable to v0.18.0 (#2642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 20:57:52 +00:00
FooIbar 8248ac6e2f Update rust-nightly to 2025-10-03
This reverts commit 085e0938cb.
2025-10-03 12:09:57 +08:00
renovate[bot] 4b2ac8ae5e Update moko.resources to v0.25.1 (#2639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 17:18:38 +00:00
FooIbar 085e0938cb Downgrade rust-nightly to 2025-09-23
2025-09-24 and later have a +25% (141KB) binary size regression

This reverts commit 9a2c0d7121.
2025-10-02 15:40:17 +08:00
renovate[bot]andFooIbar 9a2c0d7121 Update dependency rust-nightly to v2025-10-01 (#2635)
* Update dependency rust-nightly to v2025-10-01

* Fix build

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-10-01 16:11:15 +00:00
renovate[bot] aa8834b941 Update gradle/actions action to v5 (#2636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 13:24:49 +00:00
FooIbar d6b524bc47 Use RTL mode by default for non-webtoon galleries 2025-10-01 21:08:28 +08:00
FooIbar 1ca960995f Use webtoon mode by default for webtoon galleries 2025-10-01 21:08:28 +08:00
renovate[bot] 31be34f551 Update dependency chrome to v141 (#2633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 21:59:26 +00:00
FooIbar e0d1c42391 Extract common model and database code 2025-09-30 12:04:50 +08:00
FooIbar 1b9c3050aa Fix race condition in concurrent append/prepend 2025-09-28 13:31:21 +08:00
FooIbar 5ba0afdd24 Switch tl repository to avoid renovate updating it 2025-09-26 20:16:28 +08:00
FooIbar a8eb4ce47a Extract common utils (#2620)
* Extract common utils

* Fix lint
2025-09-25 17:49:06 +08:00
renovate[bot] feba6dfeb0 Update spotless to v8 (#2626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 01:27:59 +00:00
renovate[bot] 061f4a0ed0 Update dependency androidx.work:work-runtime to v2.10.5 (#2625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 01:25:06 +00:00
renovate[bot] f7c273a4f3 Update dependency androidx.activity:activity-compose to v1.12.0-alpha09 (#2623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 20:56:04 +00:00
renovate[bot] 096dae3cd0 Update dependency androidx.compose:compose-bom-alpha to v2025.09.01 (#2624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 20:55:24 +00:00
renovate[bot] eebf51f4a6 Update androidx.lifecycle to v2.10.0-alpha04 (#2621)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 18:50:38 +00:00
renovate[bot] 000ae02d47 Update androidx.room to v2.8.1 (#2622)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 18:50:21 +00:00
FooIbar 84c1ee3e88 Clean up CoroutineScope extensions 2025-09-24 13:15:30 +08:00
FooIbar 0fea2076d9 Fix a main thread disk read 2025-09-24 13:15:30 +08:00
FooIbar b7a90b171f Omit default parameters of getGalleryDetailUrl 2025-09-24 13:15:30 +08:00
FooIbar 7a68f331b0 Refactor haptic feedback type selection 2025-09-24 13:15:30 +08:00
renovate[bot] 9e7abd5a85 Update Gradle to v9.1.0 (#2618)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 14:43:54 +00:00
FooIbar 9bc3f1137e Fix ghost touch areas for links in comments 2025-09-17 22:28:32 +08:00
FooIbar 0c7e3ac8b4 Fix gallery availability check 2025-09-17 17:16:09 +08:00
renovate[bot] 715f863167 Update dependency org.jetbrains.compose to v1.9.0 (#2613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 15:44:37 +00:00
e062301d42 Disallow overzoom out (#2156)
Co-authored-by: Dude so hot <djohn@fbi.gov>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-09-15 15:22:02 +08:00
renovate[bot]andFooIbar 77396558d7 Update dependency me.saket.telephoto:zoomable to v0.17.0 (#2611)
* Update dependency me.saket.telephoto:zoomable to v0.17.0

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-09-15 05:40:17 +00:00
renovate[bot]andFooIbar d11c16c549 [no ci] Migrate renovate config (#2612)
* Migrate config renovate.json

* Keep comment

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-09-15 13:10:22 +08:00
FooIbar 6a90ac40be [no ci] Update lock file biweekly 2025-09-15 11:53:08 +08:00
FooIbar 25cc1d77b1 Stop downloads and notify the user on fatal errors 2025-09-15 11:31:30 +08:00
renovate[bot] fd489d93bb Lock file maintenance (#2610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 02:15:20 +00:00
FooIbar e80487e379 Work around filename length limit on some devices 2025-09-14 11:30:58 +08:00
renovate[bot] 3b4c2981ac Update dependency com.google.devtools.ksp to v2.2.20-2.0.3 (#2606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 03:24:24 +00:00
FooIbar 348d45f2f5 Treat search warnings as errors 2025-09-12 21:16:16 +08:00
renovate[bot] 4db96c7bdb Update dependency io.ktor:ktor-client-okhttp to v3.3.0 (#2603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 13:03:06 +00:00
renovate[bot]andFooIbar b1617c6996 Update dependency androidx.compose:compose-bom-alpha to v2025.09.00 (#2602)
* Update dependency androidx.compose:compose-bom-alpha to v2025.09.00

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-09-11 05:59:34 +00:00
renovate[bot] c078166e8d Update androidx.room to v2.8.0 (#2601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 00:32:19 +00:00
renovate[bot] 092936eda5 Update dependency androidx.work:work-runtime to v2.10.4 (#2599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 21:51:50 +00:00
renovate[bot] 257a999300 Update dependency com.google.devtools.ksp to v2.2.20-2.0.2 (#2600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 21:51:08 +00:00
renovate[bot] df6a5040a5 Update dependency androidx.activity:activity-compose to v1.12.0-alpha08 (#2598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 17:48:40 +00:00
renovate[bot] 3085ca2eeb Update benchmark to v1.4.1 (#2597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 17:48:17 +00:00
renovate[bot] fa4005cf9a Update kotlin monorepo to v2.2.20 (#2596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 10:18:21 +00:00
FooIbar c4cd375c04 Allow disabling page turning with volume keys 2025-09-09 22:04:41 +08:00
FooIbar 19d9bb198f Work around ANR on DownloadManager initialization 2025-09-05 00:29:28 +08:00
LuK1337 7016190ce5 Use LocalContentColor.current for tag text
Fixes text visibility on Android 16 QPR1.
2025-09-04 22:02:21 +08:00
renovate[bot] 8020ffa666 Update dependency com.google.devtools.ksp to v2.2.20-RC2-2.0.2 (#2590)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 04:25:38 +00:00
FooIbar bc9914ffa6 Add missing changelog entry
[no ci]
2025-09-03 11:54:41 +08:00
renovate[bot] 4ded4ff470 Update dependency chrome to v140 (#2589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 21:30:42 +00:00
renovate[bot] 7742299651 Update AGP to v8.13.0 (#2587)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 17:42:45 +00:00
FooIbar 0b348a59d2 Prepare for next iteration 2025-09-01 20:42:28 +08:00
265 changed files with 2503 additions and 2061 deletions

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
[*.{kt,kts}]
ij_kotlin_imports_layout = *
ij_kotlin_line_break_after_multiline_when_entry = false
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled
ktlint_standard_mixed-condition-operators = disabled
-1
View File
@@ -1 +0,0 @@
*.sh eol=lf
+4 -4
View File
@@ -25,7 +25,7 @@ jobs:
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
@@ -42,13 +42,13 @@ jobs:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: app/.cxx
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
@@ -59,7 +59,7 @@ jobs:
- name: Upload reports
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: reports
path: benchmark/build/reports
+18 -18
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
@@ -38,7 +38,7 @@ jobs:
run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: true
@@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
@@ -74,12 +74,12 @@ jobs:
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: app/.cxx
key: cmake-default-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
@@ -93,32 +93,32 @@ jobs:
run: ./gradlew assembleDefaultRelease
- name: Upload ARM64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: default-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: default-x86_64-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: default-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
- name: Upload mapping
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
@@ -127,7 +127,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
@@ -146,12 +146,12 @@ jobs:
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: app/.cxx
key: cmake-marshmallow-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
@@ -165,32 +165,32 @@ jobs:
run: ./gradlew assembleMarshmallowRelease
- name: Upload ARM64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: marshmallow-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: marshmallow-x86_64-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: marshmallow-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
- name: Upload mapping
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: marshmallow-mapping-${{ github.sha }}
path: app/build/outputs/mapping/marshmallowRelease
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: marshmallow-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
- uses: dessant/lock-threads@v6
with:
issue-inactive-days: '1'
process-only: 'issues'
+3 -3
View File
@@ -19,7 +19,7 @@ jobs:
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
@@ -36,13 +36,13 @@ jobs:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: app/.cxx
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
+59
View File
@@ -1,5 +1,63 @@
# 变更日志
## [1.14.6] - 2025-12-17
### 新功能
* 支持显示画廊阅读进度(于 EH 设置中开启) [#2277](https://github.com/FooIbar/EhViewer/issues/2277)
### 改进
* 画廊列表变化时重新显示悬浮操作按钮 [#2681](https://github.com/FooIbar/EhViewer/issues/2681)
* 移除文件名中的尾随空格和点以兼容 Windows [#2727](https://github.com/FooIbar/EhViewer/issues/2727)
* 长按画廊详情中的标签时显示标签名称 [#2769](https://github.com/FooIbar/EhViewer/issues/2769)
* 杂项优化
### Bug 修复
* 对话框中的下拉菜单显示错位
* HTTP 状态错误显示为“什么都没有找到”
* 某些老设备无法通过 Cloudflare 验证 [#2794](https://github.com/FooIbar/EhViewer/issues/2794)
* 杂项修复
## [1.14.5] - 2025-10-19
### 改进
* [阅读器] 显示画廊标题
* [阅读器] 显示菜单时改变标题栏颜色
### Bug 修复
* 横屏模式下打开阅读器设置会闪退 [#2673](https://github.com/FooIbar/EhViewer/issues/2673)
* 强制横屏时竖直拿取设备加载图片会横竖屏乱跳 [#2674](https://github.com/FooIbar/EhViewer/issues/2674)
## [1.14.4] - 2025-10-12
### 改进
* [阅读器] 支持禁用使用音量键翻页 [#2593](https://github.com/FooIbar/EhViewer/issues/2593)
* 视搜索警告为错误 [#2604](https://github.com/FooIbar/EhViewer/issues/2604)
* 余额不足时停止下载并显示通知 [#2479](https://github.com/FooIbar/EhViewer/issues/2479)
* [阅读器] 使用默认阅读模式时对带有 webtoon 标签的画廊自动使用条漫模式 [#2632](https://github.com/FooIbar/EhViewer/issues/2632)
* 优化按页数排序下载项目的速度
* 开启显示标签投票状态时为 power >= 100 的标签添加下划线
* 杂项优化
### Bug 修复
* Android 16 QPR1 上画廊详情中的标签文本对比度不足
* 初始化 DownloadManager 用时过长导致 ANR [#2588](https://github.com/FooIbar/EhViewer/issues/2588)
* 某些设备上无法下载名称较长的画廊 [#2607](https://github.com/FooIbar/EhViewer/issues/2607)
* 评论区链接实际可点击区域比画面显示上的略长 [#2615](https://github.com/FooIbar/EhViewer/issues/2615)
* 主页快速搜索再次打开出现以前的画廊重复出现在最上层 [#2628](https://github.com/FooIbar/EhViewer/issues/2628)
* 非条漫模式下原始大小较小的动图不播放 [#2652](https://github.com/FooIbar/EhViewer/issues/2652)
* Android 9 以下打开评论区切屏后回来会闪退 [#2655](https://github.com/FooIbar/EhViewer/issues/2655)
* 杂项修复
## [1.14.3] - 2025-09-01
### 新功能
@@ -9,6 +67,7 @@
### Bug 修复
* [阅读器] 关闭滤镜设置时崩溃 [#2573](https://github.com/FooIbar/EhViewer/issues/2573)
* 编辑评论时上下文菜单缺少格式化选项
* 某些情况下搜索相似画廊时出错
+4 -4
View File
@@ -15,7 +15,7 @@
</p>
<h1 align="center">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<img src="https://github.com/FooIbar/EhViewer-art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<br>EhViewer<br>
</h1>
@@ -78,13 +78,13 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
*Devices running Android 6 need to have the [ISRG Root X1](https://letsencrypt.org/certs/isrgrootx1.pem) certificate installed
<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"/>
<img alt="Get it on GitHub" src="https://github.com/FooIbar/EhViewer-art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
# Screenshot
![screenshot-01](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-01.png)
![screenshot-02](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-02.png)
![screenshots-01](https://github.com/FooIbar/EhViewer-art/blob/master/screenshots-01.webp)
![screenshots-02](https://github.com/FooIbar/EhViewer-art/blob/master/screenshots-02.webp)
# Thanks
+5 -14
View File
@@ -9,7 +9,8 @@ plugins {
alias(libs.plugins.ehviewer.android.application)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.aboutlibrariesPlugin)
alias(libs.plugins.aboutlibraries)
alias(libs.plugins.aboutlibrariesAndroid)
alias(libs.plugins.baselineprofile)
}
@@ -51,18 +52,15 @@ android {
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
.removeSuffix(".git")
val chromeVersion = rootProject.layout.projectDirectory.file("chrome.version").asFile.readText().trim()
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
versionCode = 180063
versionName = "1.14.3"
versionName = "1.14.6"
// versionNameSuffix = "-SNAPSHOT"
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("long", "COMMIT_TIME", commitTime)
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
buildConfigField("String", "CHROME_VERSION", "\"$chromeVersion\"")
ndk {
if (isRelease) {
abiFilters.addAll(supportedAbis)
@@ -178,10 +176,7 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.biometric)
implementation(libs.androidx.browser)
// https://developer.android.com/jetpack/androidx/releases/compose-material3
api(platform(libs.compose.bom))
implementation(libs.bundles.compose)
implementation(libs.androidx.webkit)
implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.compiler)
@@ -204,7 +199,6 @@ dependencies {
implementation(libs.androidx.paging.compose)
// https://developer.android.com/jetpack/androidx/releases/room
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.paging)
implementation(libs.androidx.work.runtime)
@@ -216,8 +210,6 @@ dependencies {
// https://square.github.io/okhttp/changelogs/changelog/
implementation(platform(libs.okhttp.bom))
implementation(platform(libs.okio.bom))
implementation(libs.logcat)
implementation(libs.diff)
@@ -258,13 +250,12 @@ kotlin {
optIn.addAll(
"coil3.annotation.ExperimentalCoilApi",
"androidx.paging.ExperimentalPagingApi",
"kotlinx.serialization.ExperimentalSerializationApi",
"me.saket.telephoto.ExperimentalTelephotoApi",
)
}
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
}
+1 -1
View File
@@ -39,7 +39,7 @@
<activity
android:name=".ui.MainActivity"
android:banner="@mipmap/ic_leanback_banner"
android:configChanges="screenSize"
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/AppTheme.Splash"
+33 -27
View File
@@ -4,17 +4,20 @@ include(ExternalProject)
include(FetchContent)
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
-mllvm -polly-isl-arg=--no-schedule-serialize-sccs \
-mllvm -polly-ast-use-context \
-mllvm -polly-detect-keep-going \
-mllvm -polly-position=before-vectorizer \
-mllvm -polly-vectorizer=stripmine \
-mllvm -polly-detect-profitability-min-per-loop-insts=40 \
-mllvm -polly-invariant-load-hoisting")
string(CONCAT CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -ffast-math -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto "
"-ffile-prefix-map=${ANDROID_TOOLCHAIN_ROOT}= "
"-ffile-prefix-map=${CMAKE_SOURCE_DIR}= "
"-ffile-prefix-map=${CMAKE_BINARY_DIR}= "
"-mllvm -polly "
"-mllvm -polly-run-dce "
"-mllvm -polly-run-inliner "
"-mllvm -polly-isl-arg=--no-schedule-serialize-sccs "
"-mllvm -polly-ast-use-context "
"-mllvm -polly-detect-keep-going "
"-mllvm -polly-position=before-vectorizer "
"-mllvm -polly-vectorizer=stripmine "
"-mllvm -polly-detect-profitability-min-per-loop-insts=40 "
"-mllvm -polly-invariant-load-hoisting")
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
option(BUILD_TESTING OFF)
@@ -38,23 +41,25 @@ include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
# Build GNUTLS libnettle
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
execute_process(
COMMAND wsl wslpath ${CMAKE_CURRENT_LIST_DIR}
OUTPUT_VARIABLE CURRENT_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}")
set(TOOLCHAIN "$ANDROID_HOME/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin")
set(WSL wsl)
else ()
set(CURRENT_DIR ${CMAKE_CURRENT_LIST_DIR})
set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin")
endif ()
set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}")
set(CONFIGURE_COMMAND ./configure --disable-public-key --disable-dependency-tracking
"--host=${CMAKE_C_COMPILER_TARGET}"
"CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments"
"AR=${TOOLCHAIN}/llvm-ar"
"CC=${TOOLCHAIN}/clang --target=${CMAKE_C_COMPILER_TARGET}"
"RANLIB=${TOOLCHAIN}/llvm-ranlib"
)
ExternalProject_Add(
nettle
GIT_REPOSITORY https://gitlab.com/gnutls/nettle.git
GIT_TAG nettle_3.10.2_release_20250626
GIT_SHALLOW 1
GIT_CONFIG core.autocrlf=input
CONFIGURE_COMMAND bash ${CURRENT_DIR}/configure_nettle.sh ${NDK_VERSION} ${CMAKE_C_COMPILER_TARGET}
BUILD_COMMAND bash -c "make -j4 libnettle.a CFLAGS='${CMAKE_C_FLAGS} -Qunused-arguments'"
URL https://ftpmirror.gnu.org/nettle/nettle-3.10.2.tar.gz
URL_MD5 b28bcbf6f045ff007940a9401673600d
CONFIGURE_COMMAND ${WSL} ${CONFIGURE_COMMAND}
BUILD_COMMAND ${WSL} make -j4 libnettle.a
INSTALL_COMMAND ""
BUILD_IN_SOURCE 1
BUILD_BYPRODUCTS <SOURCE_DIR>/libnettle.a
@@ -96,7 +101,7 @@ set(LIBARCHIVE_PATCH
FetchContent_Declare(
libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
GIT_TAG v3.8.1
GIT_TAG v3.8.4
GIT_SHALLOW 1
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
)
@@ -134,7 +139,7 @@ unset(Rust_TOOLCHAIN CACHE)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.5.2
GIT_TAG v0.6
GIT_SHALLOW 1
)
@@ -143,8 +148,9 @@ corrosion_import_crate(
MANIFEST_PATH ../rust/Cargo.toml
LOCKED
FEATURES $<IF:$<VERSION_GREATER_EQUAL:${ANDROID_NATIVE_API_LEVEL},26>,android-26,android>
FLAGS $<$<NOT:$<CONFIG:Debug>>:-Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort>
FLAGS $<$<NOT:$<CONFIG:Debug>>:-Z build-std -Z build-std-features=>
)
corrosion_add_target_rustflags(ehviewer_rust $<$<NOT:$<CONFIG:Debug>>:-Z unstable-options -C panic=immediate-abort -Z location-detail=none -Z fmt-debug=none>)
corrosion_link_libraries(ehviewer_rust android jnigraphics log webp webpdemux)
# Build and link our app's native lib
-13
View File
@@ -1,13 +0,0 @@
SUFFIX=toolchains/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64/bin
CI_NDK_LOCATION=$ANDROID_HOME/ndk/$1/$SUFFIX
ARCH_NDK_LOCATION=$ANDROID_HOME/ndk-bundle/$SUFFIX
if test -e $CI_NDK_LOCATION ;then
TOOLCHAIN=$CI_NDK_LOCATION
else
TOOLCHAIN=$ARCH_NDK_LOCATION
fi
TARGET=$2
./.bootstrap && ./configure --host=$TARGET --disable-public-key --disable-dependency-tracking \
AR=$TOOLCHAIN/llvm-ar \
CC="$TOOLCHAIN/clang -target $TARGET" \
RANLIB=$TOOLCHAIN/llvm-ranlib
@@ -1,161 +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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.systemuicontroller
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import android.view.Window
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
/**
* A class which provides easy-to-use utilities for updating the System UI bar
* colors within Jetpack Compose.
*/
@Stable
interface SystemUiController {
/**
* Control for the behavior of the system bars. This value should be one of the
* [WindowInsetsControllerCompat] behavior constants:
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH] (Deprecated),
* [WindowInsetsControllerCompat.BEHAVIOR_DEFAULT] and
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE].
*/
var systemBarsBehavior: Int
/**
* Property which holds the status bar visibility. If set to true, show the status bar,
* otherwise hide the status bar.
*/
var isStatusBarVisible: Boolean
/**
* Property which holds the navigation bar visibility. If set to true, show the navigation bar,
* otherwise hide the navigation bar.
*/
var isNavigationBarVisible: Boolean
/**
* Property which holds the status & navigation bar visibility. If set to true, show both bars,
* otherwise hide both bars.
*/
var isSystemBarsVisible: Boolean
get() = isNavigationBarVisible && isStatusBarVisible
set(value) {
isStatusBarVisible = value
isNavigationBarVisible = value
}
/**
* Property which holds whether the status bar icons + content are 'dark' or not.
*/
var statusBarDarkContentEnabled: Boolean
}
/**
* Remembers a [SystemUiController] for the given [window].
*
* If no [window] is provided, an attempt to find the correct [Window] is made.
*
* First, if the [LocalView]'s parent is a [DialogWindowProvider], then that dialog's [Window] will
* be used.
*
* Second, we attempt to find [Window] for the [Activity] containing the [LocalView].
*
* If none of these are found (such as may happen in a preview), then the functionality of the
* returned [SystemUiController] will be degraded, but won't throw an exception.
*/
@Composable
fun rememberSystemUiController(
window: Window? = findWindow(),
): SystemUiController {
val view = LocalView.current
return remember(view, window) { AndroidSystemUiController(view, window) }
}
@Composable
private fun findWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window
?: LocalView.current.context.findWindow()
private tailrec fun Context.findWindow(): Window? = when (this) {
is Activity -> window
is ContextWrapper -> baseContext.findWindow()
else -> null
}
/**
* A helper class for setting the navigation and status bar colors for a [View], gracefully
* degrading behavior based upon API level.
*
* Typically you would use [rememberSystemUiController] to remember an instance of this.
*/
internal class AndroidSystemUiController(
private val view: View,
window: Window?,
) : SystemUiController {
private val windowInsetsController = window?.let {
WindowCompat.getInsetsController(it, view)
}
override var systemBarsBehavior: Int
get() = windowInsetsController?.systemBarsBehavior ?: 0
set(value) {
windowInsetsController?.systemBarsBehavior = value
}
override var isStatusBarVisible: Boolean
get() {
return ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.statusBars()) == true
}
set(value) {
if (value) {
windowInsetsController?.show(WindowInsetsCompat.Type.statusBars())
} else {
windowInsetsController?.hide(WindowInsetsCompat.Type.statusBars())
}
}
override var isNavigationBarVisible: Boolean
get() {
return ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true
}
set(value) {
if (value) {
windowInsetsController?.show(WindowInsetsCompat.Type.navigationBars())
} else {
windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars())
}
}
override var statusBarDarkContentEnabled: Boolean
get() = windowInsetsController?.isAppearanceLightStatusBars == true
set(value) {
windowInsetsController?.isAppearanceLightStatusBars = value
}
}
@@ -180,7 +180,6 @@ fun WebView(
is WebContent.Url -> {
wv.loadUrl(content.url, content.additionalHttpHeaders)
}
is WebContent.Data -> {
wv.loadDataWithBaseURL(
content.baseUrl,
@@ -190,14 +189,12 @@ fun WebView(
content.historyUrl,
)
}
is WebContent.Post -> {
wv.postUrl(
content.url,
content.postData,
)
}
is WebContent.NavigatorOnly -> {
// NO-OP
}
@@ -502,11 +499,9 @@ class WebViewNavigator(private val coroutineScope: CoroutineScope) {
event.encoding,
event.historyUrl,
)
is NavigationEvent.LoadUrl -> {
loadUrl(event.url, event.additionalHttpHeaders)
}
is NavigationEvent.PostUrl -> {
postUrl(event.url, event.postData)
}
@@ -22,6 +22,7 @@ import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composer
import androidx.compose.runtime.tooling.ComposeStackTraceMode
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.coroutineScope
import coil3.EventListener
@@ -37,9 +38,15 @@ import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.serviceLoaderEnabled
import coil3.util.DebugLogger
import com.ehviewer.core.database.SearchDatabase
import com.ehviewer.core.database.roomDb
import com.ehviewer.core.files.deleteContent
import com.ehviewer.core.ui.util.initSETConnection
import com.ehviewer.core.util.isAtLeastO
import com.ehviewer.core.util.isAtLeastP
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.launchUI
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withUIContext
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.coil.AnimatedWebPDecoder
@@ -51,7 +58,6 @@ import com.hippo.ehviewer.coil.MapExtraInfoInterceptor
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.coil.QrCodeInterceptor
import com.hippo.ehviewer.dailycheck.checkDawn
import com.hippo.ehviewer.dao.SearchDatabase
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.ktbuilder.diskCache
@@ -59,6 +65,7 @@ import com.hippo.ehviewer.ktbuilder.imageLoader
import com.hippo.ehviewer.ktor.Cronet
import com.hippo.ehviewer.ktor.configureClient
import com.hippo.ehviewer.ktor.configureCommon
import com.hippo.ehviewer.ktor.isCronetAvailable
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.lockObserver
import com.hippo.ehviewer.ui.screen.detailCache
@@ -68,12 +75,6 @@ import com.hippo.ehviewer.util.CrashHandler
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.OSUtils
import com.hippo.ehviewer.util.isAtLeastO
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastS
import com.hippo.ehviewer.util.isAtLeastSExtension7
import com.hippo.files.deleteContent
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import kotlinx.coroutines.Dispatchers
@@ -83,19 +84,16 @@ import logcat.LogPriority
import logcat.LogcatLogger
import logcat.asLog
import okio.Path.Companion.toOkioPath
import splitties.arch.room.roomDb
import splitties.init.appCtx
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
private val lifecycleScope = lifecycle.coroutineScope
class EhApplication :
Application(),
SingletonImageLoader.Factory {
override fun onCreate() {
class EhApplication : Application(), SingletonImageLoader.Factory {
override fun onCreate() = with(lifecycleScope) {
initSETConnection()
// Initialize Settings on first access
lifecycleScope.launchIO {
launchIO {
val mode = Settings.theme.value
if (!isAtLeastS) {
withUIContext {
@@ -115,24 +113,25 @@ class EhApplication :
CrashHandler.install()
super.onCreate()
System.loadLibrary("ehviewer")
lifecycleScope.launchIO {
launchUI {
FavouriteStatusRouter.collect { info ->
detailCache[info.gid]?.apply {
favoriteSlot = info.favoriteSlot
favoriteName = info.favoriteName
favoriteNote = info.favoriteNote
}
launch {
FavouriteStatusRouter.collect { info ->
detailCache[info.gid]?.apply {
favoriteSlot = info.favoriteSlot
favoriteName = info.favoriteName
favoriteNote = info.favoriteNote
}
}
}
launchIO {
EhTagDatabase.launchUpdate()
launch { EhDB }
launchIO { dataStateFlow.value }
launchIO { OSUtils.totalMemory }
launch { dataStateFlow.value }
launch { OSUtils.totalMemory }
launch {
if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode !in Settings.snapshot()) {
Settings.downloadFilterMode.value = DownloadsFilterMode.CUSTOM.flag
}
initialized = true
DownloadManager.readMetadataFromLocal()
}
launch {
@@ -146,7 +145,9 @@ class EhApplication :
}
if (BuildConfig.DEBUG) {
StrictMode.enableDefaults()
Composer.setDiagnosticStackTraceEnabled(true)
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.SourceInformation)
} else {
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.Auto)
}
}
@@ -214,8 +215,12 @@ class EhApplication :
}
companion object {
@Volatile
var initialized = false
private set
val ktorClient by lazy {
if (isAtLeastSExtension7 && Settings.enableCronet.value) {
if (isCronetAvailable && Settings.enableCronet.value) {
HttpClient(Cronet) {
engine { configureClient(Settings.enableQuic.value) }
configureCommon()
+43 -37
View File
@@ -15,28 +15,31 @@
*/
package com.hippo.ehviewer
import android.content.Context
import android.net.Uri
import androidx.room.execSQL
import androidx.room.useWriterConnection
import arrow.fx.coroutines.resource
import arrow.fx.coroutines.resourceScope
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.dao.DownloadDirname
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.dao.EhDatabase
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.HistoryInfo
import com.hippo.ehviewer.dao.LocalFavoriteInfo
import com.hippo.ehviewer.dao.ProgressInfo
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.dao.Schema17to18
import com.ehviewer.core.data.model.asEntity
import com.ehviewer.core.database.EhDatabase
import com.ehviewer.core.database.Schema17to18
import com.ehviewer.core.database.getDatabasePath
import com.ehviewer.core.database.model.DownloadArtist
import com.ehviewer.core.database.model.DownloadDirname
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.database.model.DownloadLabel
import com.ehviewer.core.database.model.Filter
import com.ehviewer.core.database.model.GalleryEntity
import com.ehviewer.core.database.model.HistoryInfo
import com.ehviewer.core.database.model.LocalFavoriteInfo
import com.ehviewer.core.database.model.ProgressInfo
import com.ehviewer.core.database.model.QuickSearch
import com.ehviewer.core.database.roomDb
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.sendTo
import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.util.sendTo
import kotlinx.coroutines.flow.Flow
import okio.Path
import okio.Path.Companion.toOkioPath
import splitties.arch.room.roomDb
object EhDB {
private const val DB_NAME = "eh.db"
@@ -44,15 +47,15 @@ object EhDB {
addMigrations(Schema17to18())
}
suspend fun putGalleryInfo(galleryInfo: BaseGalleryInfo) {
suspend fun putGalleryInfo(galleryInfo: GalleryEntity) {
db.galleryDao().upsert(galleryInfo)
}
private suspend fun deleteGalleryInfo(galleryInfo: BaseGalleryInfo) {
private suspend fun deleteGalleryInfo(galleryInfo: GalleryEntity) {
runCatching { db.galleryDao().delete(galleryInfo) }
}
suspend fun updateGalleryInfo(galleryInfoList: List<BaseGalleryInfo>) {
suspend fun updateGalleryInfo(galleryInfoList: List<GalleryEntity>) {
db.galleryDao().update(galleryInfoList)
}
@@ -156,12 +159,12 @@ object EhDB {
}
}
suspend fun removeLocalFavorites(galleryInfo: BaseGalleryInfo) {
suspend fun removeLocalFavorites(galleryInfo: GalleryInfo) {
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
deleteGalleryInfo(galleryInfo)
deleteGalleryInfo(galleryInfo.asEntity())
}
suspend fun removeLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
suspend fun removeLocalFavorites(galleryInfoList: Collection<GalleryInfo>) {
galleryInfoList.forEach {
removeLocalFavorites(it)
}
@@ -172,12 +175,12 @@ object EhDB {
return dao.contains(gid)
}
suspend fun putLocalFavorites(galleryInfo: BaseGalleryInfo) {
putGalleryInfo(galleryInfo)
suspend fun putLocalFavorites(galleryInfo: GalleryInfo) {
putGalleryInfo(galleryInfo.asEntity())
db.localFavoritesDao().upsert(LocalFavoriteInfo(galleryInfo.gid))
}
suspend fun putLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
suspend fun putLocalFavorites(galleryInfoList: Collection<GalleryInfo>) {
galleryInfoList.forEach {
putLocalFavorites(it)
}
@@ -223,8 +226,8 @@ object EhDB {
fun searchLocalFav(keyword: String) = db.localFavoritesDao().joinListLazy("*$keyword*")
suspend fun putHistoryInfo(galleryInfo: BaseGalleryInfo) {
putGalleryInfo(galleryInfo)
suspend fun putHistoryInfo(galleryInfo: GalleryInfo) {
putGalleryInfo(galleryInfo.asEntity())
db.historyDao().upsert(HistoryInfo(galleryInfo.gid))
}
@@ -241,7 +244,7 @@ object EhDB {
dao.insertOrIgnore(historyInfoList)
}
suspend fun deleteHistoryInfo(galleryInfo: BaseGalleryInfo) {
suspend fun deleteHistoryInfo(galleryInfo: GalleryEntity) {
val dao = db.historyDao()
dao.deleteByKey(galleryInfo.gid)
deleteGalleryInfo(galleryInfo)
@@ -275,23 +278,26 @@ object EhDB {
db.filterDao().update(filter)
}
fun exportDB(context: Context, file: Path) {
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
val dbFile = context.getDatabasePath(DB_NAME)
dbFile.toOkioPath() sendTo file
suspend fun exportDB(file: Path) {
db.useWriterConnection { conn ->
conn.execSQL("PRAGMA wal_checkpoint(FULL)")
conn.execSQL("VACUUM")
}
val dbFile = getDatabasePath(DB_NAME)
dbFile sendTo file
}
suspend fun importDB(context: Context, uri: Uri) = resourceScope {
suspend fun importDB(file: Path) = resourceScope {
val tempDBName = "tmp.db"
val dbFile = getDatabasePath(tempDBName)
file sendTo dbFile
val oldDB = resource {
context.deleteDatabase(tempDBName)
roomDb<EhDatabase>(tempDBName) {
createFromInputStream { context.contentResolver.openInputStream(uri) }
addMigrations(Schema17to18())
}
} release { db ->
db.close()
context.deleteDatabase(tempDBName)
dbFile.delete()
}
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
@@ -9,10 +9,10 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.preferences.DataStorePreferences
import com.ehviewer.core.preferences.PrefDelegate
import com.ehviewer.core.preferences.edit
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.download.SortMode
@@ -82,6 +82,7 @@ object Settings : DataStorePreferences(null) {
val detailSize = intPref("detail_size_2", 0)
val thumbColumns = intPref("thumb_columns", 3)
val showGalleryPages = boolPref("show_gallery_pages", true)
val showReadingProgress = boolPref("show_reading_progress", false)
val showVoteStatus = boolPref("show_vote_status", false)
val showComments = boolPref("show_gallery_comments", true)
val commentThreshold = intPref("comment_threshold", -100)
@@ -118,7 +119,7 @@ object Settings : DataStorePreferences(null) {
val desktopSite = boolPref("desktop_site", true)
// About
val backupBeforeUpdate = boolPref("backup_before_update", true)
val backupBeforeUpdate = boolPref("backup_before_update", false)
val useCIUpdateChannel = boolPref("ci_update_channel", AppConfig.isSnapshot)
val updateIntervalDays = intPref("update_interval_days", 7)
@@ -158,8 +159,8 @@ object Settings : DataStorePreferences(null) {
val colorFilterMode = intPref("color_filter_mode", 0)
val customBrightness = boolPref("pref_custom_brightness_key", false)
val customBrightnessValue = intPref("custom_brightness_value", 0)
val readingMode = intPref("pref_default_reading_mode_key", ReadingModeType.LEFT_TO_RIGHT.flagValue)
val orientationMode = intPref("pref_default_orientation_type_key", OrientationType.FREE.flagValue)
val readingMode = intPref("pref_default_reading_mode_key", ReadingModeType.DEFAULT.prefValue)
val orientationMode = intPref("pref_default_orientation_type_key", OrientationType.DEFAULT.prefValue)
val showReaderSeekbar = boolPref("pref_show_reader_seekbar", true)
val showPageNumber = boolPref("pref_show_page_number_key", true)
val readerTheme = intPref("pref_reader_theme_key", 1)
@@ -169,7 +170,8 @@ object Settings : DataStorePreferences(null) {
val keepScreenOn = boolPref("pref_keep_screen_on_key", true)
val readerLongTapAction = boolPref("reader_long_tap", true)
val pageTransitions = boolPref("pref_enable_transitions_key", true)
val readerReverseControls = boolPref("reader_volume_keys_inverted", false)
val readWithVolumeKeys = boolPref("reader_volume_keys", false)
val readWithVolumeKeysInverted = boolPref("reader_volume_keys_inverted", false)
val grayScale = boolPref("pref_grayscale", false)
val invertedColors = boolPref("pref_inverted_colors", false)
val readerWebtoonNav = intPref("reader_navigation_mode_webtoon", 0)
@@ -193,6 +195,10 @@ object Settings : DataStorePreferences(null) {
if (pref[downloadFilterMode] == DownloadsFilterMode.ARTIST.flag && pref[recentDownloadLabel] == null) {
pref[recentDownloadLabel] = ""
}
val orientation = pref[orientationMode]
if (OrientationType.entries.none { it.prefValue == orientation }) {
pref.remove(orientationMode)
}
}
}
@@ -4,13 +4,13 @@ import android.app.UiModeManager
import androidx.appcompat.app.AppCompatDelegate
import arrow.core.Either.Companion.catch
import com.ehviewer.core.preferences.PrefDelegate
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.util.isAtLeastS
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -15,7 +15,7 @@
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.client.data.GalleryInfo
import com.ehviewer.core.model.GalleryInfo
// Normal Preview (v2): https://*.hath.network/c(m|1|2)/[timed token]/[gid]-[index].(jpg|webp)
// ExHentai Large Preview (v1 Cover): https://s.exhentai.org/t/***
@@ -22,14 +22,21 @@ import arrow.core.right
import arrow.fx.coroutines.parMap
import arrow.fx.coroutines.parZip
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.fillInfo
import com.hippo.ehviewer.client.data.filterComments
import com.hippo.ehviewer.client.exception.CloudflareBypassException
import com.hippo.ehviewer.client.exception.EhError
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.InsufficientGpException
import com.hippo.ehviewer.client.exception.IpBannedException
import com.hippo.ehviewer.client.exception.NoHathClientException
import com.hippo.ehviewer.client.exception.NoHitsFoundException
import com.hippo.ehviewer.client.exception.NotLoggedInException
@@ -41,6 +48,7 @@ import com.hippo.ehviewer.client.parser.FavoritesParser
import com.hippo.ehviewer.client.parser.GalleryApiParser
import com.hippo.ehviewer.client.parser.GalleryDetailParser
import com.hippo.ehviewer.client.parser.GalleryListParser
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
import com.hippo.ehviewer.client.parser.GalleryPageParser
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
import com.hippo.ehviewer.client.parser.HomeParser
@@ -58,10 +66,12 @@ import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.HttpStatement
import io.ktor.client.statement.bodyAsChannel
import io.ktor.client.statement.request
import io.ktor.http.Url
import io.ktor.util.moveToByteArray
import io.ktor.utils.io.pool.DirectByteBufferPool
import io.ktor.utils.io.pool.useInstance
import io.ktor.utils.io.readAvailable
@@ -72,6 +82,8 @@ import kotlin.system.measureTimeMillis
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.serialization.SerializationException
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonArray
import kotlinx.serialization.json.put
@@ -86,7 +98,9 @@ private const val MAX_REQUEST_SIZE = 25
private const val MAX_SEQUENTIAL_REQUESTS = 5
private const val REQUEST_INTERVAL = 5000L
fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
private val Url.isLogin get() = segments.firstOrNull() == "bounce_login.php"
private fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
val dir = AppConfig.externalParseErrorDir ?: return
val file = File(dir, ReadableTime.getFilenamableTime() + ".txt")
file.sink().buffer().use { sink ->
@@ -98,7 +112,7 @@ fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
}
}
fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuffer>, e: Throwable): Nothing {
private fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuffer>, e: Throwable): Nothing {
// Don't translate coroutine cancellation
if (e is CancellationException) throw e
@@ -110,36 +124,40 @@ fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuffer>, e:
{ !it.hasRemaining() },
)
if (empty) {
if (EhUtils.isExHentai) {
throw EhException("Sad Panda\n(without panda)")
val message = if (EhUtils.isExHentai) {
"Sad Panda\n(without panda)"
} else {
throw EhException("IP banned")
appCtx.getString(R.string.error_ip_banned)
}
throw IpBannedException(message)
}
// Check Gallery Not Available
body.onLeft {
if ("Gallery Not Available - " in it) throw e
}
// Parse error from rust
val error = body.getOrNull()?.runCatching {
val array = moveToByteArray()
rewind()
Cbor.decodeFromByteArray<EhError>(array)
}?.getOrNull()
// Check parse error thrown by rust
val cause = e.cause
if (e is ParseException && cause is RuntimeException) {
when (val message = cause.message) {
"0" -> throw NoHitsFoundException()
"1" -> throw EhException(R.string.gallery_list_empty_hit_subscription)
"2" -> throw NotLoggedInException()
"3" -> throw NoHathClientException()
"4" -> throw InsufficientFundsException()
is String if message.startsWith('5') -> throw EhException(message.drop(1))
}
}
// Check Gallery Not Available - 404
if (error is EhError.GalleryUnavailable) throw EhException(error.message)
// Check bad response code
response.status.ensureSuccess()
when (error) {
EhError.NoHits -> throw NoHitsFoundException()
EhError.NoWatched -> throw EhException(R.string.gallery_list_empty_hit_subscription)
EhError.NeedLogin -> throw NotLoggedInException()
EhError.NoHathClient -> throw NoHathClientException()
EhError.InsufficientFunds -> throw InsufficientFundsException()
is EhError.IpBanned -> throw IpBannedException(error.message)
is EhError.Error -> throw EhException(error.message)
else -> Unit
}
if (e is ParseException || e is SerializationException) {
body.onLeft { if ("<" !in it) throw EhException(it) }
body.onLeft { if ("<" !in it) throw IpBannedException(it) }
if (Settings.saveParseErrorBody.value) body.saveParseError(e)
throw EhException(appCtx.getString(R.string.error_parse_error), e)
}
@@ -148,9 +166,10 @@ fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuffer>, e:
throw e
}
val httpContentPool = DirectByteBufferPool(8, 0x80000)
private val httpContentPool = DirectByteBufferPool(8, 0x80000)
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = executeSafely { response ->
private suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = executeSafely { response ->
if (response.request.url.isLogin) throw NotLoggedInException()
val body = response.bodyAsUtf8Text()
runSuspendCatching {
block(body)
@@ -159,7 +178,8 @@ suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend
}.getOrThrow()
}
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = executeSafely { response ->
private suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = executeSafely { response ->
if (response.request.url.isLogin) throw NotLoggedInException()
httpContentPool.useInstance { buffer ->
with(response.bodyAsChannel()) { while (!isClosedForRead) readAvailable(buffer) }
buffer.flip()
@@ -174,10 +194,12 @@ suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: s
object EhEngine {
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()
response.headers["Location"]?.takeUnless { Url(it).isLogin } ?: throw InsufficientGpException()
}
suspend fun getPTokenListFromMpv(gid: Long, token: String) = ehRequest(EhUrl.getGalleryMultiPageViewerUrl(gid, token), EhUrl.referer)
.fetchUsingAsText(GalleryMultiPageViewerPTokenParser::parse)
suspend fun getTorrentList(gid: Long, token: String): TorrentResult {
val url = EhUrl.getTorrentUrl(gid, token)
val referer = EhUrl.getGalleryDetailUrl(gid, token)
@@ -1,10 +1,10 @@
package com.hippo.ehviewer.client
import arrow.core.memoize
import com.ehviewer.core.database.model.Filter
import com.ehviewer.core.database.model.FilterMode
import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.FilterMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -15,11 +15,11 @@
*/
package com.hippo.ehviewer.client
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.header
@@ -20,17 +20,17 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import arrow.core.Either
import com.ehviewer.core.files.metadataOrNull
import com.ehviewer.core.files.read
import com.ehviewer.core.files.write
import com.ehviewer.core.model.TagNamespace
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess
import com.hippo.ehviewer.util.utf8
import com.hippo.files.metadataOrNull
import com.hippo.files.read
import com.hippo.files.write
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.request.prepareGet
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
@@ -20,17 +20,21 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Environment
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.net.toUri
import arrow.core.memoize
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.util.withUIContext
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.parser.Archive
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.util.AppConfig
@@ -39,6 +43,7 @@ import com.hippo.ehviewer.util.addTextToClipboard
import com.materialkolor.hct.Hct
import com.materialkolor.ktx.from
import com.materialkolor.ktx.toColor
import com.materialkolor.utils.ColorUtils.lstarFromArgb
import splitties.systemservices.downloadManager
object EhUtils {
@@ -151,7 +156,13 @@ object EhUtils {
}
}
val categoryTextColor = Color(0xffe6e0e9)
@ReadOnlyComposable
@Composable
fun getCategoryTextColor(color: Color) = if (isSystemInDarkTheme() == lstarFromArgb(color.toArgb()) > 70) {
MaterialTheme.colorScheme.surface
} else {
LocalContentColor.current
}
val favoriteIconColor = Color(0xffff3040)
@@ -1,13 +0,0 @@
package com.hippo.ehviewer.client.data
interface AbstractDownloadInfo {
var state: Int
var legacy: Int
var time: Long
var label: String?
var speed: Long
var remaining: Long
var finished: Int
var downloaded: Int
var total: Int
}
@@ -1,96 +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.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "GALLERIES")
@TypeConverters(SimpleTagsConverter::class)
data class BaseGalleryInfo(
@PrimaryKey
@ColumnInfo(name = "GID")
override var gid: Long = 0,
@ColumnInfo(name = "TOKEN")
override var token: String = "",
@ColumnInfo(name = "TITLE")
override var title: String? = null,
@ColumnInfo(name = "TITLE_JPN")
override var titleJpn: String? = null,
@ColumnInfo(name = "THUMB")
override var thumbKey: String? = null,
@ColumnInfo(name = "CATEGORY")
override var category: Int = 0,
@ColumnInfo(name = "POSTED")
override var posted: String? = null,
@ColumnInfo(name = "UPLOADER")
override var uploader: String? = null,
@Ignore
override var disowned: Boolean = false,
@ColumnInfo(name = "RATING")
override var rating: Float = 0f,
@Ignore
override var rated: Boolean = false,
@ColumnInfo(name = "SIMPLE_TAGS")
override var simpleTags: List<String>? = null,
@ColumnInfo(name = "PAGES", defaultValue = "0")
override var pages: Int = 0,
@Ignore
override var thumbWidth: Int = 0,
@Ignore
override var thumbHeight: Int = 0,
@ColumnInfo(name = "SIMPLE_LANGUAGE")
override var simpleLanguage: String? = null,
@ColumnInfo(name = "FAVORITE_SLOT")
override var favoriteSlot: Int = NOT_FAVORITED,
@Ignore
override var favoriteName: String? = null,
@Ignore
override var favoriteNote: String? = null,
) : GalleryInfo
class SimpleTagsConverter {
@TypeConverter
fun fromString(value: String) = value.split(", ")
@TypeConverter
fun toString(value: List<String>) = value.joinToString()
}
@@ -25,9 +25,9 @@ import kotlinx.serialization.Serializable
data class FavListUrlBuilder(
val favCat: Int = FAV_CAT_ALL,
val keyword: String? = null,
var jumpTo: String? = null,
var prev: String? = null,
var next: String? = null,
private var jumpTo: String? = null,
private var prev: String? = null,
private var next: String? = null,
) {
val isLocal
get() = favCat == FAV_CAT_LOCAL
@@ -35,6 +35,7 @@ data class FavListUrlBuilder(
fun setIndex(index: String?, isNext: Boolean) {
next = index.takeIf { isNext }
prev = index.takeUnless { isNext }
jumpTo = null
}
fun build() = ehUrl(EhUrl.FAV_PATH) {
@@ -16,10 +16,13 @@
package com.hippo.ehviewer.client.data
import androidx.compose.ui.util.fastFlatMap
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryTagGroup
import com.ehviewer.core.model.PowerStatus
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhFilter
import kotlinx.serialization.Serializable
private val LANGUAGES = arrayOf(
"English",
@@ -38,45 +41,27 @@ private val LANGUAGES = arrayOf(
"Dutch",
)
@Serializable
data class GalleryDetail(
val galleryInfo: BaseGalleryInfo = BaseGalleryInfo(),
var apiUid: Long = -1L,
var apiKey: String? = null,
var torrentCount: Int = 0,
var parent: String? = null,
var newerVersions: List<BaseGalleryInfo> = emptyList(),
var visible: String? = null,
var language: String? = null,
var size: String? = null,
var favoriteCount: Int = 0,
var ratingCount: Int = 0,
val tagGroups: List<GalleryTagGroup>,
var comments: GalleryCommentList,
val previewList: List<GalleryPreview>,
) : GalleryInfo by galleryInfo {
suspend fun fillInfo() {
val index = LANGUAGES.indexOf(language)
if (index != -1) simpleLanguage = GalleryInfo.S_LANGS[index]
simpleTags = tagGroups.fastFlatMap(GalleryTagGroup::tags).map { (text, power, _) ->
if (power == PowerStatus.Weak) "_$text" else text
}
if (favoriteSlot == GalleryInfo.NOT_FAVORITED && EhDB.containLocalFavorites(gid)) {
favoriteSlot = GalleryInfo.LOCAL_FAVORITED
}
suspend fun GalleryDetail.fillInfo() {
val index = LANGUAGES.indexOf(language)
if (index != -1) simpleLanguage = GalleryInfo.S_LANGS[index]
simpleTags = tagGroups.fastFlatMap(GalleryTagGroup::tags).map { (text, power, _) ->
if (power == PowerStatus.Weak) "_$text" else text
}
if (favoriteSlot == GalleryInfo.NOT_FAVORITED && EhDB.containLocalFavorites(gid)) {
favoriteSlot = GalleryInfo.LOCAL_FAVORITED
}
}
suspend fun filterComments() {
comments = with(comments) {
val scoreThreshold = Settings.commentThreshold.value
copy(
comments = comments.filter {
it.uploader ||
it.score > scoreThreshold &&
!EhFilter.filterCommenter(it.user.orEmpty()) &&
!EhFilter.filterComment(it.comment)
},
)
}
suspend fun GalleryDetail.filterComments() {
comments = with(comments) {
val scoreThreshold = Settings.commentThreshold.value
copy(
comments = comments.filter {
it.uploader ||
it.score > scoreThreshold &&
!EhFilter.filterCommenter(it.user.orEmpty()) &&
!EhFilter.filterComment(it.comment)
},
)
}
}
@@ -15,25 +15,24 @@
*/
package com.hippo.ehviewer.client.data
import com.ehviewer.core.database.model.QuickSearch
import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.addQueryParameter
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
import com.hippo.ehviewer.client.ehUrl
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.ui.main.AdvanceTable
import io.ktor.http.Parameters
import kotlin.text.toIntOrNull
import kotlinx.serialization.Serializable
@Serializable
data class ListUrlBuilder(
var mode: Int = MODE_NORMAL,
private var prev: String? = null,
var next: String? = null,
// Reset to null after initial loading
var jumpTo: String? = null,
private var next: String? = null,
private var jumpTo: String? = null,
private var range: Int = 0,
var category: Int = EhUtils.NONE,
private var mKeyword: String? = null,
@@ -49,11 +48,14 @@ data class ListUrlBuilder(
next = index.takeIf { isNext }
prev = index.takeUnless { isNext }
range = 0
jumpTo = null
}
fun setJumpTo(to: Int) {
jumpTo = to.takeUnless { it == 0 }?.toString()
}
var page: Int
get() = (jumpTo?.toInt() ?: 0) + 1
set(value) {
jumpTo = (value - 1).takeUnless { it == 0 }?.toString()
}
fun setRange(to: Int) {
range = to
@@ -62,6 +64,11 @@ data class ListUrlBuilder(
jumpTo = null
}
fun setSeek(date: String) {
setIndex("2", true)
jumpTo = date
}
var keyword: String?
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
set(keyword) {
@@ -205,7 +212,6 @@ data class ListUrlBuilder(
}
}
}.buildString()
MODE_UPLOADER, MODE_TAG -> {
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
ehUrl(listOf(path, requireNotNull(mKeyword))) {
@@ -215,7 +221,6 @@ data class ListUrlBuilder(
addQueryParameterIfNotBlank("range", range.takeIf { it > 0 }?.toString())
}.buildString()
}
MODE_WHATS_HOT -> EhUrl.popularUrl
MODE_IMAGE_SEARCH -> ehUrl {
addQueryParameter("f_shash", requireNotNull(hash))
@@ -224,7 +229,6 @@ data class ListUrlBuilder(
addQueryParameter("tl", requireNotNull(mKeyword))
addQueryParameterIfNotBlank("p", jumpTo)
}.buildString()
else -> throw IllegalStateException("Unexpected value: $mode")
}
@@ -0,0 +1,48 @@
package com.hippo.ehviewer.client.exception
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.CborArray
@Serializable
sealed interface EhError {
@CborArray
@Serializable
@SerialName("NoHits")
data object NoHits : EhError
@CborArray
@Serializable
@SerialName("NoWatched")
data object NoWatched : EhError
@CborArray
@Serializable
@SerialName("NeedLogin")
data object NeedLogin : EhError
@CborArray
@Serializable
@SerialName("NoHathClient")
data object NoHathClient : EhError
@CborArray
@Serializable
@SerialName("InsufficientFunds")
data object InsufficientFunds : EhError
@CborArray
@Serializable
@SerialName("GalleryUnavailable")
data class GalleryUnavailable(val message: String) : EhError
@CborArray
@Serializable
@SerialName("IpBanned")
data class IpBanned(val message: String) : EhError
@CborArray
@Serializable
@SerialName("Error")
data class Error(val message: String) : EhError
}
@@ -0,0 +1,13 @@
package com.hippo.ehviewer.client.exception
import androidx.annotation.StringRes
import com.ehviewer.core.i18n.R
sealed class FatalException : EhException {
constructor(message: String) : super(message)
constructor(@StringRes messageId: Int) : super(messageId)
}
class IpBannedException(message: String) : FatalException(message)
class InsufficientGpException : FatalException(R.string.insufficient_gp)
class QuotaExceededException : FatalException(R.string.error_509)
@@ -1,5 +0,0 @@
package com.hippo.ehviewer.client.exception
import com.ehviewer.core.i18n.R
class QuotaExceededException : EhException(R.string.error_509)
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.client.parser
import com.ehviewer.core.util.unescapeXml
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml
import java.nio.ByteBuffer
import kotlinx.serialization.Serializable
import moe.tarsin.coroutines.runSuspendCatching
@@ -15,11 +15,11 @@
*/
package com.hippo.ehviewer.client.parser
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.unescapeXml
import com.hippo.ehviewer.client.EhUtils.getCategory
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.unescapeXml
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -17,9 +17,9 @@ package com.hippo.ehviewer.client.parser
import arrow.core.Either
import arrow.core.getOrElse
import com.hippo.ehviewer.client.data.GalleryCommentList
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryPreviewList
import com.ehviewer.core.model.GalleryCommentList
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryPreviewList
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlinx.serialization.Serializable
@@ -1,6 +1,6 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.ehviewer.core.model.BaseGalleryInfo
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlinx.serialization.Serializable
@@ -26,7 +26,7 @@ object GalleryListUrlParser {
fun parse(urlStr: String) = Either.catch {
val url = Url(urlStr)
if (url.host != EhUrl.DOMAIN_E && url.host != EhUrl.DOMAIN_EX) {
return null
return@catch null
}
val segments = url.segments
if (segments.isEmpty()) {
@@ -42,7 +42,6 @@ object GalleryListUrlParser {
mode = if (head == "uploader") ListUrlBuilder.MODE_UPLOADER else ListUrlBuilder.MODE_TAG
keyword = segments[1].decodeURLQueryComponent(plusIsSpace = true)
}
"toplist.php" -> {
val tl = url.parameters["tl"]
if (tl != null && tl in arrayOf("11", "12", "13", "15")) {
@@ -55,7 +54,6 @@ object GalleryListUrlParser {
null
}
}
else -> {
val category = EhUtils.getCategory(head)
if (category != EhUtils.UNKNOWN) {
@@ -15,8 +15,8 @@
*/
package com.hippo.ehviewer.client.parser
import com.ehviewer.core.util.unescapeXml
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml
object GalleryPageParser {
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
@@ -15,9 +15,9 @@
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.util.toEpochMillis
import com.hippo.ehviewer.util.toIntOrDefault
import com.hippo.ehviewer.util.toLocalDateTime
import com.ehviewer.core.util.toEpochMillis
import com.ehviewer.core.util.toIntOrDefault
import com.ehviewer.core.util.toLocalDateTime
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format.char
@@ -19,7 +19,7 @@ package com.hippo.ehviewer.client.parser
import arrow.core.Either.Companion.catch
import arrow.core.getOrElse
import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.ehviewer.core.model.GalleryTagGroup
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
@@ -9,7 +9,7 @@ import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.os.SystemClock
import androidx.core.graphics.createBitmap
import eu.kanade.tachiyomi.util.system.logcat
import com.ehviewer.core.util.logcat
import java.nio.ByteBuffer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -12,9 +12,9 @@ import coil3.asImage
import coil3.intercept.Interceptor
import coil3.request.ImageResult
import coil3.request.SuccessResult
import com.ehviewer.core.util.isAtLeastQ
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.image.copyBitmapToAHB
import com.hippo.ehviewer.util.isAtLeastQ
import eu.kanade.tachiyomi.util.system.logcat
import moe.tarsin.coroutines.runSuspendCatching
@RequiresApi(Build.VERSION_CODES.O)
@@ -6,16 +6,16 @@ import coil3.intercept.Interceptor
import coil3.request.ImageRequest
import coil3.request.ImageResult
import coil3.request.SuccessResult
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.isFile
import com.ehviewer.core.files.sendTo
import com.ehviewer.core.files.toUri
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.util.sendTo
import com.hippo.files.delete
import com.hippo.files.isDirectory
import com.hippo.files.isFile
import com.hippo.files.toUri
private val downloadInfoKey = Extras.Key<DownloadInfo?>(default = null)
@@ -35,7 +35,7 @@ object DownloadThumbInterceptor : Interceptor {
info.thumbKey = thumbKey
EhDB.putGalleryInfo(info.galleryInfo)
}
val dir = downloadLocation / info.dirname
val dir = downloadLocation / info.dirname!!
val format = thumbKey.substringAfterLast('.', "")
check(format.isNotBlank())
val thumb = dir / "thumb.$format"
@@ -7,10 +7,13 @@ import coil3.decode.BlackholeDecoder
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.size.SizeResolver
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryPreview
import com.ehviewer.core.model.V2GalleryPreview
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.client.getV2PreviewKey
import com.hippo.ehviewer.client.thumbUrl
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ktbuilder.execute
@@ -30,7 +33,7 @@ fun ImageRequest.Builder.ehUrl(info: GalleryInfo) = apply {
// Load in original size so the memory cache can be reused for preload requests
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
with(preview) {
val key = imageKey
val key = if (this is V2GalleryPreview) getV2PreviewKey(url) else getThumbKey(url)
data(url)
size(SizeResolver.ORIGINAL)
memoryCacheKey(key)
@@ -19,11 +19,11 @@ import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import eu.kanade.tachiyomi.util.system.logcat
import java.time.Duration
import java.util.concurrent.TimeUnit
import kotlin.time.Clock
@@ -22,18 +22,29 @@ import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.util.size
import arrow.fx.coroutines.parMapNotNull
import com.ehviewer.core.data.model.asEntity
import com.ehviewer.core.database.model.DownloadArtist
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.database.model.DownloadLabel
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.find
import com.ehviewer.core.files.toOkioPath
import com.ehviewer.core.files.toUri
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.preferences.edit
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.mapNotNull
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.client.exception.FatalException
import com.hippo.ehviewer.spider.COMIC_INFO_FILE
import com.hippo.ehviewer.spider.SpeedTracker
import com.hippo.ehviewer.spider.SpiderQueen
@@ -45,13 +56,7 @@ import com.hippo.ehviewer.spider.readCompatFromPath
import com.hippo.ehviewer.spider.toSimpleTags
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.insertWith
import com.hippo.ehviewer.util.mapNotNull
import com.hippo.ehviewer.util.runAssertingNotMainThread
import com.hippo.files.delete
import com.hippo.files.find
import com.hippo.files.toOkioPath
import com.hippo.files.toUri
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -70,9 +75,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
// All download info list
private val allInfoList = runAssertingNotMainThread {
(EhDB.getAllDownloadInfo() as MutableList).apply { sortWith(comparator()) }
}
private val allInfoList = runAssertingNotMainThread { EhDB.getAllDownloadInfo() as MutableList }
val downloadInfoList: List<DownloadInfo>
get() = allInfoList
@@ -97,6 +100,21 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
private val mutex = Mutex()
private val sortMutex = Mutex()
private val sortMode
get() = SortMode.from(Settings.downloadSortMode.value)
var isInitialized by mutableStateOf(false)
private set
init {
launch {
val mode = sortMode
if (mode != SortMode.Default) sortDownloads(mode)
isInitialized = true
}
}
fun containLabel(label: String?): Boolean {
if (label == null) {
return false
@@ -130,7 +148,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
mCurrentTask = info
with(info) {
archiveFile?.let {
return onFinish(pages, pages, pages)
return onFinish(pages, pages, pages, failed = false)
}
state = DownloadInfo.STATE_DOWNLOAD
speed = -1
@@ -178,11 +196,13 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
} else {
// It is new download info
info = DownloadInfo(galleryInfo, galleryInfo.downloadDirname())
info = DownloadInfo(galleryInfo.asEntity(), galleryInfo.downloadDirname())
info.label = label
info.state = DownloadInfo.STATE_WAIT
// Add to all download list and map
allInfoList.insertWith(info, comparator())
sortMutex.withLock {
allInfoList.insertWith(info, sortMode.comparator())
}
mAllInfoMap[galleryInfo.gid] = info
// Add to wait list
@@ -197,7 +217,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
ensureDownload()
// Add it to history
EhDB.putHistoryInfo(info.galleryInfo)
EhDB.putHistoryInfo(info)
}
}
@@ -239,10 +259,12 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}.collectAsState(transform(info)).value
suspend fun startAllDownload() {
val updateList = allInfoList.filter {
when (it.state) {
DownloadInfo.STATE_NONE, DownloadInfo.STATE_FAILED -> true
else -> false
val updateList = sortMutex.withLock {
allInfoList.filter {
when (it.state) {
DownloadInfo.STATE_NONE, DownloadInfo.STATE_FAILED -> true
else -> false
}
}
}
if (updateList.isNotEmpty()) {
@@ -256,8 +278,8 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
}
suspend fun addDownload(downloadInfoList: List<DownloadInfo>) {
val comparator = comparator()
suspend fun addDownload(downloadInfoList: List<DownloadInfo>) = sortMutex.withLock {
val comparator = sortMode.comparator()
downloadInfoList.forEach { info ->
if (containDownloadInfo(info.gid)) return@forEach
@@ -287,11 +309,13 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
suspend fun restoreDownload(galleryInfo: BaseGalleryInfo, dirname: String) {
val info = DownloadInfo(galleryInfo, dirname)
val info = DownloadInfo(galleryInfo.asEntity(), dirname)
info.state = DownloadInfo.STATE_NONE
// Add to all download list and map
allInfoList.insertWith(info, comparator())
sortMutex.withLock {
allInfoList.insertWith(info, sortMode.comparator())
}
mAllInfoMap[galleryInfo.gid] = info
// Save to
@@ -350,7 +374,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
EhDB.removeDownloadInfo(info)
// Remove all list and map
allInfoList.remove(info)
sortMutex.withLock {
allInfoList.remove(info)
}
mAllInfoMap.remove(info.gid)
// Update listener
@@ -373,7 +399,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
mAllInfoMap.remove(gid)
}
EhDB.removeDownloadInfo(list)
allInfoList.removeAll(list.toSet())
sortMutex.withLock {
allInfoList.removeAll(list.toSet())
}
// Update listener
list.forEach { mutableNotifyFlow.emit(it) }
@@ -382,7 +410,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
ensureDownload()
}
fun sortDownloads(mode: SortMode) {
suspend fun sortDownloads(mode: SortMode) = sortMutex.withLock {
allInfoList.sortWith(mode.comparator())
}
@@ -501,9 +529,11 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
val new = exist.copy(label = to)
labelList.add(index, new)
EhDB.updateDownloadLabel(new)
allInfoList.forEach {
if (it.label == from) {
it.label = to
sortMutex.withLock {
allInfoList.forEach {
if (it.label == from) {
it.label = to
}
}
}
}
@@ -519,21 +549,25 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
removeAt(index)
}
allInfoList.forEach {
if (it.label == label) {
it.label = null
sortMutex.withLock {
allInfoList.forEach {
if (it.label == label) {
it.label = null
}
}
}
}
suspend fun readMetadataFromLocal() {
val list = allInfoList.mapNotNull {
val updateGallery = it.pages == 0 || it.simpleTags == null
val updateArtist = it.artistInfoList.isEmpty()
if (updateGallery || updateArtist) {
Triple(it, updateGallery, updateArtist)
} else {
null
val list = sortMutex.withLock {
allInfoList.mapNotNull {
val updateGallery = it.pages == 0 || it.simpleTags == null
val updateArtist = it.artistInfoList.isEmpty()
if (updateGallery || updateArtist) {
Triple(it, updateGallery, updateArtist)
} else {
null
}
}
}.parMapNotNull(concurrency = 5) { (info, updateGallery, updateArtist) ->
info.downloadDir?.run {
@@ -574,13 +608,14 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
}
val galleryInfoList = mutableListOf<BaseGalleryInfo>()
list.forEach { (info, artists) ->
info?.let {
galleryInfoList.add(it)
}
artists?.let { (gid, updateList) ->
EhDB.putDownloadArtist(gid, updateList)
val galleryInfoList = buildList {
list.forEach { (info, artists) ->
info?.let {
add(it)
}
artists?.let { (gid, updateList) ->
EhDB.putDownloadArtist(gid, updateList)
}
}
}
if (galleryInfoList.isNotEmpty()) EhDB.updateGalleryInfo(galleryInfoList)
@@ -598,9 +633,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
}
override fun onGet509(index: Int) {
override fun onFatal(error: FatalException) {
launch {
mDownloadListener?.onGet509()
mDownloadListener?.onFatal(error)
stopAllDownload()
}
}
@@ -647,7 +682,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
}
override fun onFinish(finished: Int, downloaded: Int, total: Int) {
override fun onFinish(finished: Int, downloaded: Int, total: Int, failed: Boolean) {
launch {
mSpeedReminder.onFinish()
mCurrentSpider?.let { spider ->
@@ -662,11 +697,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
info.downloaded = downloaded
info.total = total
info.legacy = total - finished
if (info.legacy == 0) {
info.state = DownloadInfo.STATE_FINISH
} else {
info.state = DownloadInfo.STATE_FAILED
}
info.state = if (failed) DownloadInfo.STATE_FAILED else DownloadInfo.STATE_FINISH
EhDB.putDownloadInfo(info)
mDownloadListener?.onFinish(info)
mutableNotifyFlow.emit(info)
@@ -677,9 +708,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
interface DownloadListener {
/**
* Get 509 error
* Fatal error
*/
fun onGet509()
fun onFatal(error: FatalException)
/**
* Start download
@@ -782,8 +813,6 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
private const val TAG = "DownloadManager"
private fun comparator() = SortMode.from(Settings.downloadSortMode.value).comparator()
}
var downloadLocation: Path
@@ -31,16 +31,20 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.savedstate.serialization.decodeFromSavedState
import androidx.savedstate.serialization.encodeToSavedState
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.unsafeLazy
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.client.exception.FatalException
import com.hippo.ehviewer.client.exception.InsufficientGpException
import com.hippo.ehviewer.client.exception.IpBannedException
import com.hippo.ehviewer.client.exception.QuotaExceededException
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.isAtLeastS
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -61,7 +65,7 @@ class DownloadService :
private val notifyManager by unsafeLazy { NotificationManagerCompat.from(this) }
private val downloadingNotification by unsafeLazy { initDownloadingNotification() }
private val downloadedNotification by lazy { initDownloadedNotification() }
private val error509Notification by lazy { init509Notification() }
private val fatalNotification by lazy { initFatalNotification() }
private val channelId by unsafeLazy { "$packageName.download" }
override fun onCreate() {
@@ -109,14 +113,12 @@ class DownloadService :
val label = intent.getStringExtra(KEY_LABEL)
deferredMgr.await().startDownload(decodeFromSavedState(gi), label)
}
ACTION_START_RANGE -> {
val gidList = intent.getLongArrayExtra(KEY_GID_LIST)
if (gidList != null) {
deferredMgr.await().startRangeDownload(gidList)
}
}
ACTION_START_ALL -> deferredMgr.await().startAllDownload()
ACTION_STOP_ALL -> deferredMgr.await().stopAllDownload()
ACTION_CLEAR -> clear()
@@ -172,24 +174,27 @@ class DownloadService :
.apply { launch { run() } }
}
private fun init509Notification(): NotificationHandler {
private fun initFatalNotification(): NotificationHandler {
val builder = NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(com.hippo.ehviewer.R.drawable.ic_baseline_warning_24)
.setContentTitle(getString(R.string.stat_509_alert_title))
.setContentText(getString(R.string.stat_509_alert_text))
.setStyle(
NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)),
)
.setAutoCancel(true)
.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR)
return NotificationHandler(this, notifyManager, builder, ID_509)
return NotificationHandler(this, notifyManager, builder, ID_FATAL)
.apply { launch { run() } }
}
override fun onGet509() {
error509Notification.run {
override fun onFatal(error: FatalException) {
val (title, text) = when (error) {
is IpBannedException -> R.string.error_ip_banned to error.message
is InsufficientGpException -> R.string.insufficient_funds to error.message
is QuotaExceededException -> R.string.stat_509_alert_title to getString(R.string.stat_509_alert_text)
}
fatalNotification.run {
builder.setWhen(System.currentTimeMillis())
.setContentTitle(getString(title))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
show()
}
}
@@ -381,7 +386,6 @@ class DownloadService :
notifyManager.notify(id, builder.build())
}
}
Ops.Cancel -> notifyManager.cancel(id)
Ops.StartForeground -> service.startForeground(id, builder.build())
}
@@ -412,7 +416,7 @@ class DownloadService :
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
private const val ID_DOWNLOADING = 1
private const val ID_DOWNLOADED = 2
private const val ID_509 = 3
private const val ID_FATAL = 3
private const val DELAY = 1000L // 1s
private val sItemStateArray = LongSparseArray<Boolean>()
private val sItemTitleArray = LongSparseArray<String>()
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.download
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.artists
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.database.model.artists
enum class DownloadsFilterMode(
val flag: Int,
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.download
import com.ehviewer.core.database.model.DownloadInfo
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.dao.DownloadInfo
data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel: Boolean = false) {
val flag
@@ -11,7 +11,6 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
GID(0),
TITLE(1),
TIME(2),
LABEL(3),
PAGES(4),
}
@@ -23,13 +22,16 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
fun comparator(): Comparator<DownloadInfo> {
var comparator = when (field) {
Field.GID -> compareBy { it.gid }
Field.TITLE, Field.LABEL -> compareBy { EhUtils.getSuitableTitle(it) }
Field.TITLE -> compareBy { EhUtils.getSuitableTitle(it) }
Field.TIME -> compareBy { it.time }
Field.PAGES -> compareBy<DownloadInfo> { it.pages }.thenBy { EhUtils.getSuitableTitle(it) }
Field.PAGES -> compareBy<DownloadInfo> { it.pages }
}
if (order == Order.DESC) {
comparator = comparator.reversed()
}
if (field == Field.PAGES) {
comparator = comparator.thenByDescending { it.time }
}
if (groupByDownloadLabel) {
comparator = compareBy<DownloadInfo> { it.label }.then(comparator)
}
@@ -38,15 +40,16 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
companion object {
fun from(flag: Int): SortMode {
val field = flag and 0xF shr 1
val order = flag and 0x1
val fieldValue = flag and 0xF shr 1
val orderValue = flag and 0x1
val groupByDownloadLabel = flag shr 4 == 1
require(field in Field.entries.indices && order in Order.entries.indices)
return SortMode(Field.entries[field], Order.entries[order], groupByDownloadLabel)
val field = Field.entries.find { it.value == fieldValue } ?: Default.field
val order = Order.entries.find { it.value == orderValue } ?: Default.order
return SortMode(field, order, groupByDownloadLabel)
}
val Default = SortMode(Field.TIME, Order.DESC)
val All = listOf(
SortMode(Field.TIME, Order.DESC),
Default,
SortMode(Field.TIME, Order.ASC),
SortMode(Field.GID, Order.DESC),
SortMode(Field.GID, Order.ASC),
@@ -16,7 +16,11 @@
package com.hippo.ehviewer.gallery
import arrow.autoCloseScope
import com.ehviewer.core.files.openFileDescriptor
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.Settings.archivePasswds
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.image.ImageSource
import com.hippo.ehviewer.image.byteBufferSource
import com.hippo.ehviewer.jni.closeArchive
@@ -29,8 +33,6 @@ import com.hippo.ehviewer.jni.providePassword
import com.hippo.ehviewer.jni.releaseByteBuffer
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.displayName
import com.hippo.files.openFileDescriptor
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.coroutineScope
import moe.tarsin.kt.install
import okio.Path
@@ -40,7 +42,7 @@ typealias PasswdProvider = suspend (PasswdInvalidator) -> String
suspend inline fun <T> useArchivePageLoader(
file: Path,
gid: Long = 0,
info: GalleryInfo? = null,
startPage: Int = 0,
hasAds: Boolean = false,
crossinline passwdProvider: PasswdProvider,
@@ -49,7 +51,7 @@ suspend inline fun <T> useArchivePageLoader(
coroutineScope {
val pfd = install(file.openFileDescriptor("r"))
val size = install(
{ openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name.endsWith(".zip")) },
{ openArchive(pfd.fd, pfd.statSize, info == null || file.name.endsWith(".zip")) },
{ _, _ -> closeArchive() },
)
check(size > 0) { "Archive have no content!" }
@@ -57,8 +59,14 @@ suspend inline fun <T> useArchivePageLoader(
archivePasswds += passwdProvider(::providePassword)
}
val loader = install(
object : PageLoader(this, gid, startPage, size, hasAds) {
override val title by lazy { FileUtils.getNameFromFilename(file.displayName)!! }
object : PageLoader(this, info, startPage, size, hasAds) {
override val title by lazy {
if (info != null) {
EhUtils.getSuitableTitle(info)
} else {
FileUtils.getNameFromFilename(file.displayName)!!
}
}
override fun getImageExtension(index: Int) = getExtension(index)
@@ -1,8 +1,8 @@
package com.hippo.ehviewer.gallery
import arrow.autoCloseScope
import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.spider.SpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
@@ -24,7 +24,7 @@ suspend inline fun <T> useEhPageLoader(
)
queen.awaitReady()
val loader = install(
object : PageLoader(this, info.gid, startPage, queen.size, info.hasAds) {
object : PageLoader(this, info, startPage, queen.size, info.hasAds) {
override val title by lazy { EhUtils.getSuitableTitle(info) }
override fun getImageExtension(index: Int) = queen.getExtension(index)
@@ -4,6 +4,8 @@ import androidx.collection.SieveCache
import androidx.collection.mutableIntObjectMapOf
import arrow.fx.coroutines.ExitCase
import arrow.fx.coroutines.bracketCase
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.isAtLeastO
import com.ehviewer.core.util.withNonCancellableContext
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
@@ -13,7 +15,6 @@ import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.OSUtils
import com.hippo.ehviewer.util.detectAds
import com.hippo.ehviewer.util.displayString
import com.hippo.ehviewer.util.isAtLeastO
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.read
@@ -35,7 +36,7 @@ private val progressScope = CoroutineScope(Dispatchers.IO)
private const val MAX_CACHE_SIZE = 512 * 1024 * 1024
private const val MIN_CACHE_SIZE = 256 * 1024 * 1024
abstract class PageLoader(val scope: CoroutineScope, val gid: Long, startPage: Int, val size: Int, val hasAds: Boolean = false) : AutoCloseable {
abstract class PageLoader(val scope: CoroutineScope, val info: GalleryInfo?, startPage: Int, val size: Int, val hasAds: Boolean = false) : AutoCloseable {
var startPage = startPage.coerceIn(0, size - 1)
private val jobs = mutableIntObjectMapOf<Job>()
@@ -111,14 +112,14 @@ abstract class PageLoader(val scope: CoroutineScope, val gid: Long, startPage: I
override fun close() {
lock.write { cache.evictAll() }
if (gid != 0L) {
info?.run {
progressScope.launch {
EhDB.putReadProgress(gid, startPage)
}
}
}
protected abstract val title: String
abstract val title: String
protected abstract fun getImageExtension(index: Int): String?
@@ -36,6 +36,10 @@ import coil3.size.Dimension
import coil3.size.Precision
import coil3.size.Size
import coil3.size.SizeResolver
import com.ehviewer.core.files.openFileDescriptor
import com.ehviewer.core.files.toUri
import com.ehviewer.core.util.isAtLeastP
import com.ehviewer.core.util.isAtLeastU
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.coil.AnimatedWebPDrawable
import com.hippo.ehviewer.coil.BitmapImageWithExtraInfo
@@ -48,10 +52,6 @@ import com.hippo.ehviewer.jni.munmap
import com.hippo.ehviewer.jni.rewriteGifSource
import com.hippo.ehviewer.ktbuilder.execute
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastU
import com.hippo.files.openFileDescriptor
import com.hippo.files.toUri
import java.nio.ByteBuffer
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.decrementAndFetch
@@ -130,7 +130,6 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
}
src.right().decodeCoil(checkExtraneousAds)
}
is ByteBufferSource -> {
if (isAtLeastP && !isAtLeastU) {
rewriteGifSource(src.source)
@@ -5,8 +5,8 @@ import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.imageLoader
import coil3.request.ImageRequest
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryPreview
import com.hippo.ehviewer.coil.ehPreview
import com.hippo.ehviewer.coil.ehUrl
import kotlinx.coroutines.CoroutineScope
@@ -1,8 +1,8 @@
package com.hippo.ehviewer.ktor
import com.hippo.ehviewer.BuildConfig
import androidx.webkit.WebViewCompat
import com.ehviewer.core.network.EhCookieStore
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.plugins.HttpTimeout
@@ -14,9 +14,12 @@ import io.ktor.client.request.header
import io.ktor.http.HttpHeaders
import io.ktor.http.userAgent
import io.ktor.util.appendIfNameAbsent
import splitties.init.appCtx
const val CHROME_MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Mobile Safari/537.36"
private const val CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Safari/537.36"
// It's safe to assume the WebView package will always be present as we require CookieManager anyway
private val WebViewVersion = WebViewCompat.getCurrentWebViewPackage(appCtx)!!.versionName!!.substringBefore('.')
val CHROME_MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$WebViewVersion.0.0.0 Mobile Safari/537.36"
private val CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$WebViewVersion.0.0.0 Safari/537.36"
private const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
private const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.9"
@@ -10,9 +10,6 @@ import android.net.http.UrlRequest
import android.net.http.UrlResponseInfo
import android.os.Build
import androidx.annotation.RequiresExtension
import com.ehviewer.core.preferences.edit
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.util.restartApplication
import io.ktor.client.engine.HttpClientEngineBase
import io.ktor.client.engine.callContext
import io.ktor.client.plugins.HttpTimeoutCapability
@@ -103,15 +100,6 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) {
// Cronet may crash on some devices, fuck xiaomi
// https://github.com/FooIbar/EhViewer/issues/1826
val message = error.message.orEmpty()
if ("ERR_FILE_NOT_FOUND" in message || "ERR_PROXY_CONNECTION_FAILED" in message) {
Settings.edit(true) {
it[enableCronet] = false
}
with(config.context) { restartApplication() }
}
if (continuation.isActive) {
continuation.resumeWithException(error)
} else {
@@ -1,12 +1,12 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.ktor
import android.net.http.HttpEngine
import android.os.Build
import androidx.annotation.RequiresExtension
import com.ehviewer.core.util.isAtLeastSExtension7
import java.io.File
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
fun CronetConfig.configureClient(enableQuic: Boolean) {
config = {
setEnableBrotli(true)
@@ -24,3 +24,14 @@ fun CronetConfig.configureClient(enableQuic: Boolean) {
}
}
}
val isCronetAvailable: Boolean
get() = isAtLeastSExtension7 && !isDeviceBlocked
// https://github.com/FooIbar/EhViewer/issues/1826
private val isDeviceBlocked = when (Build.VERSION.INCREMENTAL.substringBefore('.')) {
"V816", // HyperOS 1
"OS2", // HyperOS 2
-> true
else -> false
}
@@ -1,6 +1,6 @@
package com.hippo.ehviewer.ktor
import com.hippo.ehviewer.util.isAtLeastQ
import com.ehviewer.core.util.isAtLeastQ
import io.ktor.client.engine.okhttp.OkHttpConfig
import okhttp3.AsyncDns
import okhttp3.android.AndroidAsyncDns
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.legacy
import com.ehviewer.core.files.read
import com.hippo.ehviewer.spider.SpiderInfo
import com.hippo.files.read
import kotlinx.io.readLineStrict
import okio.Path
@@ -1,24 +1,24 @@
package com.hippo.ehviewer.spider
import com.ehviewer.core.database.util.SimpleTagsConverter
import com.ehviewer.core.files.read
import com.ehviewer.core.files.write
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryTag
import com.ehviewer.core.model.PowerStatus
import com.ehviewer.core.model.TagNamespace
import com.ehviewer.core.model.TagNamespace.Artist
import com.ehviewer.core.model.TagNamespace.Character
import com.ehviewer.core.model.TagNamespace.Cosplayer
import com.ehviewer.core.model.TagNamespace.Female
import com.ehviewer.core.model.TagNamespace.Group
import com.ehviewer.core.model.TagNamespace.Location
import com.ehviewer.core.model.TagNamespace.Male
import com.ehviewer.core.model.TagNamespace.Mixed
import com.ehviewer.core.model.TagNamespace.Other
import com.ehviewer.core.model.TagNamespace.Parody
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryTag
import com.hippo.ehviewer.client.data.PowerStatus
import com.hippo.ehviewer.client.data.SimpleTagsConverter
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.client.data.TagNamespace.Artist
import com.hippo.ehviewer.client.data.TagNamespace.Character
import com.hippo.ehviewer.client.data.TagNamespace.Cosplayer
import com.hippo.ehviewer.client.data.TagNamespace.Female
import com.hippo.ehviewer.client.data.TagNamespace.Group
import com.hippo.ehviewer.client.data.TagNamespace.Location
import com.hippo.ehviewer.client.data.TagNamespace.Male
import com.hippo.ehviewer.client.data.TagNamespace.Mixed
import com.hippo.ehviewer.client.data.TagNamespace.Other
import com.hippo.ehviewer.client.data.TagNamespace.Parody
import com.hippo.files.read
import com.hippo.files.write
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -68,7 +68,6 @@ fun GalleryInfo.getComicInfo(): ComicInfo {
else -> Unit
}
}
else -> simpleTags?.forEach { tagString ->
val (namespace, tag) = tagString.split(':', limit = 2)
.takeIf { it.size == 2 } ?: return@forEach // Ignore temp tags that don't have namespace
@@ -20,18 +20,28 @@ import arrow.fx.coroutines.autoCloseable
import arrow.fx.coroutines.closeable
import arrow.fx.coroutines.parMap
import arrow.fx.coroutines.resourceScope
import com.ehviewer.core.database.model.DownloadArtist
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.exists
import com.ehviewer.core.files.find
import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.list
import com.ehviewer.core.files.mkdirs
import com.ehviewer.core.files.moveTo
import com.ehviewer.core.files.openFileDescriptor
import com.ehviewer.core.files.sendTo
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
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.getImageKey
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.coil.suspendEdit
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.download.tempDownloadDir
@@ -39,24 +49,13 @@ import com.hippo.ehviewer.image.PathSource
import com.hippo.ehviewer.jni.archiveFdBatch
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.copyTo
import com.hippo.ehviewer.util.sendTo
import com.hippo.ehviewer.util.sha1
import com.hippo.files.delete
import com.hippo.files.exists
import com.hippo.files.find
import com.hippo.files.isDirectory
import com.hippo.files.list
import com.hippo.files.mkdirs
import com.hippo.files.moveTo
import com.hippo.files.openFileDescriptor
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsChannel
import io.ktor.client.statement.request
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
import kotlinx.coroutines.CancellationException
import okio.Path
class SpiderDen(val info: GalleryInfo) {
@@ -129,11 +128,9 @@ class SpiderDen(val info: GalleryInfo) {
SpiderQueen.MODE_READ -> {
containInCache(index) || containInDownloadDir(index)
}
SpiderQueen.MODE_DOWNLOAD -> {
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
}
else -> {
false
}
@@ -270,22 +267,18 @@ class SpiderDen(val info: GalleryInfo) {
}
}
suspend fun archive() {
if (saveAsCbz) {
downloadDir?.run {
resolve(archiveName).let { file ->
runCatching {
archiveTo(file)
}.onFailure {
file.delete()
if (it is CancellationException) throw it
logcat(it)
}
}
}
suspend fun archive() = saveAsCbz && downloadDir?.run {
resolve(archiveName).let { file ->
runCatching {
archiveTo(file)
}.onFailure {
file.delete()
logcat(it)
}.isFailure
}
}
} == true
// Postpone this to `SpiderQueen.stop` because files may still be in use by reader
suspend fun postArchive(): Boolean {
val dir = downloadDir
val archived = saveAsCbz && dir?.find(archiveName) != null
@@ -1,12 +1,12 @@
package com.hippo.ehviewer.spider
import com.ehviewer.core.files.read
import com.ehviewer.core.files.write
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.coil.edit
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.legacy.readLegacySpiderInfo
import com.hippo.files.read
import com.hippo.files.write
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.io.readByteArray
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor
@@ -20,23 +20,20 @@ import androidx.annotation.IntDef
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.partially1
import com.ehviewer.core.files.find
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryPreview
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
import com.hippo.ehviewer.client.EhUrl.referer
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.ehRequest
import com.hippo.ehviewer.client.exception.FatalException
import com.hippo.ehviewer.client.exception.QuotaExceededException
import com.hippo.ehviewer.client.fetchUsingAsText
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
import com.hippo.ehviewer.util.displayString
import com.hippo.files.find
import eu.kanade.tachiyomi.util.system.logcat
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.decrementAndFetch
import kotlin.concurrent.atomics.incrementAndFetch
@@ -91,9 +88,9 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
}
fun notifyGet509(index: Int) {
fun notifyFatal(error: FatalException) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach { it.onGet509(index) }
mSpiderListeners.forEach { it.onFatal(error) }
}
}
@@ -137,13 +134,14 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
}
private fun notifyAllPageDownloaded() {
private fun notifyAllPageDownloaded(failed: Boolean) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onFinish(
mFinishedPages.load(),
mDownloadedPages.load(),
pageStates.size,
failed,
)
}
}
@@ -216,7 +214,10 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private val prepareScope = CoroutineScope(coroutineContext + SupervisorJob())
private val prepareJob = prepareScope.async { doPrepare() }
private val archiveJob = launch(start = CoroutineStart.LAZY) { spiderDen.archive() }
private val archiveJob = launch(start = CoroutineStart.LAZY) {
val failed = spiderDen.archive()
notifyAllPageDownloaded(failed)
}
private suspend fun doPrepare() {
spiderDen.initDownloadDirIfExist()
@@ -331,7 +332,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
private suspend fun readSpiderInfoFromInternet() = EhEngine.getPreviewList(
getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, 0, false),
getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token),
).run {
val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token, total)
readPreviews(previews, 0, spiderInfo)
@@ -342,17 +343,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
suspend fun getPTokenFromMultiPageViewer(index: Int): String? {
if (!isMpvAvailable) return null
val url = getGalleryMultiPageViewerUrl(
galleryInfo.gid,
galleryInfo.token,
)
return runSuspendCatching {
ehRequest(url, referer).fetchUsingAsText {
GalleryMultiPageViewerPTokenParser.parse(this).forEachIndexed { index, s ->
spiderInfo.pTokenMap[index] = s
}
spiderInfo.pTokenMap[index]
EhEngine.getPTokenListFromMpv(galleryInfo.gid, galleryInfo.token).forEachIndexed { index, s ->
spiderInfo.pTokenMap[index] = s
}
spiderInfo.pTokenMap[index]
}.getOrElse {
logcat(it)
null
@@ -365,12 +360,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
} else {
0
}
val url = getGalleryDetailUrl(
galleryInfo.gid,
galleryInfo.token,
previewIndex,
false,
)
val url = getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, previewIndex)
return runSuspendCatching {
EhEngine.getPreviewList(url).run {
readPreviews(previews, previewIndex, spiderInfo)
@@ -414,8 +404,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
notifyPageSuccess(index)
}
if (mDownloadedPages.load() == size) {
if (mFinishedPages.load() == size) archiveJob.start()
notifyAllPageDownloaded()
if (mFinishedPages.load() == size) {
archiveJob.start()
} else {
notifyAllPageDownloaded(failed = true)
}
}
}
@@ -428,11 +421,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
annotation class State
interface OnSpiderListener {
fun onGetPages(pages: Int) {}
fun onGet509(index: Int) {}
fun onFatal(error: FatalException) {}
fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {}
fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}
fun onPageFailure(index: Int, error: String?, finished: Int, downloaded: Int, total: Int) {}
fun onFinish(finished: Int, downloaded: Int, total: Int) {}
fun onFinish(finished: Int, downloaded: Int, total: Int, failed: Boolean) {}
fun onPageReady(index: Int) {}
}
@@ -555,7 +548,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
if (!force && index in spiderDen) {
return updatePageState(index, STATE_FINISHED)
}
pToken = getPToken(index) ?: return updatePageState(index, STATE_FAILED, PTOKEN_FAILED_MESSAGE)
pToken = getPToken(index) ?: return updatePageState(index, STATE_FAILED, pTokenFailedMessage)
previousPToken = getPToken(index - 1)
// The lock for delay should be acquired before anything else to maintain FIFO order
@@ -650,10 +643,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
}
}.onFailure {
when (it) {
is QuotaExceededException -> notifyGet509(index)
// TODO: Check IP ban
}
if (it is FatalException) notifyFatal(it)
error = it.displayString()
if (error == "Invalid page.") {
pTokenLock.withLock {
@@ -664,13 +654,13 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
updatePageState(index, STATE_FAILED, error)
}
}
private val pTokenFailedMessage = appCtx.getString(R.string.error_get_ptoken_error)
}
private val PTOKEN_FAILED_MESSAGE = appCtx.getString(R.string.error_get_ptoken_error)
private val URL_509_PATTERN = Regex("\\.org/.+/509s?\\.gif")
private val Url509Regex = Regex("https://(?:ehgt\\.org/|exhentai\\.org/im)g/509s?\\.gif")
private const val FORCE_RETRY = "Force retry"
private const val WORKER_DEBUG_TAG = "SpiderQueenWorker"
private fun check509(url: String) {
if (URL_509_PATTERN in url) throw QuotaExceededException()
if (Url509Regex.matches(url)) throw QuotaExceededException()
}
@@ -35,19 +35,27 @@ import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.exists
import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.write
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryInfo.Companion.LOCAL_FAVORITED
import com.ehviewer.core.model.GalleryInfo.Companion.NOT_FAVORITED
import com.ehviewer.core.ui.component.LabeledCheckbox
import com.ehviewer.core.util.isAtLeastT
import com.ehviewer.core.util.mapToLongArray
import com.ehviewer.core.util.toEpochMillis
import com.ehviewer.core.util.toLocalDateTime
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.downloadDir
@@ -65,16 +73,8 @@ import com.hippo.ehviewer.ui.tools.awaitSelectItemWithIcon
import com.hippo.ehviewer.ui.tools.awaitSelectItemWithIconAndTextField
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.bgWork
import com.hippo.ehviewer.util.isAtLeastT
import com.hippo.ehviewer.util.mapToLongArray
import com.hippo.ehviewer.util.requestPermission
import com.hippo.ehviewer.util.restartApplication
import com.hippo.ehviewer.util.toEpochMillis
import com.hippo.ehviewer.util.toLocalDateTime
import com.hippo.files.delete
import com.hippo.files.exists
import com.hippo.files.isDirectory
import com.hippo.files.write
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlin.time.Clock
import kotlinx.coroutines.sync.Mutex
@@ -177,7 +177,7 @@ suspend fun startDownload(forceDefault: Boolean, vararg galleryInfos: BaseGaller
}
context(_: DialogState)
suspend fun modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
suspend fun modifyFavorites(galleryInfo: GalleryInfo): Boolean {
val localFavorited = EhDB.containLocalFavorites(galleryInfo.gid)
if (Settings.hasSignedIn.value) {
val isFavorited = galleryInfo.favoriteSlot != NOT_FAVORITED
@@ -215,7 +215,7 @@ suspend fun modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
}
private suspend fun doModifyFavorites(
galleryInfo: BaseGalleryInfo,
galleryInfo: GalleryInfo,
slot: Int = NOT_FAVORITED,
localFavorited: Boolean = true,
note: String = "",
@@ -233,7 +233,6 @@ private suspend fun doModifyFavorites(
}
false
}
LOCAL_FAVORITED -> {
if (localFavorited) {
EhDB.removeLocalFavorites(galleryInfo)
@@ -248,7 +247,6 @@ private suspend fun doModifyFavorites(
}
!localFavorited
}
in 0..9 -> {
EhEngine.modifyFavorites(gid, token, slot, note)
favoriteSlot = slot
@@ -256,14 +254,13 @@ private suspend fun doModifyFavorites(
favoriteNote = note
true
}
else -> throw EhException("Invalid favorite slot!")
}
FavouriteStatusRouter.notify(galleryInfo)
add
}
suspend fun removeFromFavorites(galleryInfo: BaseGalleryInfo) = doModifyFavorites(
suspend fun removeFromFavorites(galleryInfo: GalleryInfo) = doModifyFavorites(
galleryInfo = galleryInfo,
localFavorited = EhDB.containLocalFavorites(galleryInfo.gid),
)
@@ -305,13 +302,11 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
EhDB.putHistoryInfo(info)
navToReader(info)
}
1 -> if (downloaded) {
confirmRemoveDownload(info)
} else {
startDownload(false, info)
}
2 -> if (favorited) {
runSuspendCatching {
removeFromFavorites(info)
@@ -327,7 +322,6 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
tip(R.string.add_to_favorite_failure)
}
}
3 -> showMoveDownloadLabel(info)
}
}
@@ -419,7 +413,7 @@ suspend fun showMoveDownloadLabelList(list: Collection<DownloadInfo>): String? {
}
context(_: DialogState)
suspend fun awaitSelectDate(): String? {
suspend fun awaitSelectDate(): String {
val initial = LocalDate(2007, 3, 21)
val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY)
val initialMillis = initial.toEpochMillis()
@@ -432,8 +426,7 @@ suspend fun awaitSelectDate(): String? {
override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis in dateRange
},
)
val date = dateMillis?.run { toLocalDateTime().date.toString() }
return date
return dateMillis.toLocalDateTime().date.toString()
}
context(_: Context, _: DialogState)
@@ -20,8 +20,8 @@ import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.ActivityNavigator
import com.ehviewer.core.util.isAtLeastR
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.util.isAtLeastR
import eu.kanade.tachiyomi.util.view.setSecureScreen
abstract class EhActivity : AppCompatActivity() {
@@ -25,11 +25,11 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.thumbUrl
import com.hippo.ehviewer.ui.screen.navWithUrl
import com.hippo.ehviewer.util.addTextToClipboard
@@ -102,6 +102,8 @@ import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.navigation.compose.rememberNavController
import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.toOkioPath
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.component.LabeledCheckbox
import com.ehviewer.core.ui.component.LocalSideSheetState
@@ -110,7 +112,10 @@ import com.ehviewer.core.ui.icons.EhIcons
import com.ehviewer.core.ui.icons.filled.Subscriptions
import com.ehviewer.core.ui.util.LocalSnackBarFabPadding
import com.ehviewer.core.ui.util.LocalWindowSizeClass
import com.ehviewer.core.util.isAtLeastQ
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhApplication.Companion.initialized
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
@@ -143,11 +148,7 @@ import com.hippo.ehviewer.util.calculateFraction
import com.hippo.ehviewer.util.displayString
import com.hippo.ehviewer.util.getParcelableExtraCompat
import com.hippo.ehviewer.util.getUrlFromClipboard
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.isAtLeastS
import com.hippo.ehviewer.util.sha1
import com.hippo.files.isDirectory
import com.hippo.files.toOkioPath
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.spec.Direction
import com.ramcosta.composedestinations.utils.currentDestinationAsState
@@ -200,7 +201,7 @@ class MainActivity : EhActivity() {
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
installSplashScreen().setKeepOnScreenCondition { !initialized }
super.onCreate(savedInstanceState)
setMD3Content {
val configuration = LocalConfiguration.current
@@ -14,10 +14,10 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavBackStackEntry
import com.ehviewer.core.ui.util.ProvideVectorPainterCache
import com.hippo.ehviewer.ui.theme.EhTheme
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LocalGlobalDialogState
import com.hippo.ehviewer.ui.tools.ProvideVectorPainterCache
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import me.zhanghai.compose.preference.ProvidePreferenceTheme
import me.zhanghai.compose.preference.preferenceTheme
@@ -21,7 +21,7 @@ import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.client.data.GalleryDetail
import com.ehviewer.core.model.GalleryDetail
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -7,8 +7,8 @@ import coil3.decode.DecodeUtils
import coil3.imageLoader
import coil3.request.crossfade
import coil3.size.Scale
import com.ehviewer.core.util.toIntOrDefault
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.util.toIntOrDefault
import kotlin.math.roundToInt
import splitties.init.appCtx
@@ -41,6 +41,7 @@ class LinkifyTextView(
val x = event.x + scrollX - totalPaddingLeft
val y = event.y + scrollY - totalPaddingTop
val line = layout.getLineForVertical(y.toInt())
if (x > layout.getLineMax(line)) return@let
val off = layout.getOffsetForHorizontal(line, x)
val spans = text.getSpans<URLSpan>(off, off)
if (spans.isNotEmpty()) {
@@ -1,11 +1,11 @@
package com.hippo.ehviewer.ui.login
import arrow.core.Either.Companion.catch
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -60,6 +60,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.util.LocalWindowSizeClass
import com.ehviewer.core.ui.util.ifTrueThen
import com.ehviewer.core.ui.util.isExpanded
import com.ehviewer.core.ui.util.thenIf
import com.ehviewer.core.util.launchIO
@@ -73,7 +74,6 @@ import com.hippo.ehviewer.ui.destinations.WebViewSignInScreenDestination
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.util.displayString
import com.jamal.composeprefs3.ui.ifTrueThen
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import com.ehviewer.core.network.EhCookieStore
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.Screen
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DownloadDone
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Card
@@ -26,6 +27,7 @@ import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableIntStateOf
@@ -35,19 +37,18 @@ import androidx.compose.runtime.setValue
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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.component.CrystalCard
import com.ehviewer.core.ui.component.GalleryListCardRating
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
import com.ehviewer.core.ui.util.listThumbGenerator
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ui.tools.GalleryListCardRating
import com.hippo.ehviewer.util.FileUtils
@Composable
@@ -60,6 +61,7 @@ fun DownloadCard(
onStop: () -> Unit,
info: DownloadInfo,
selectMode: Boolean,
showProgress: Boolean,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) = CrystalCard(modifier = modifier, onClick = onClick, onLongClick = onLongClick, interactionSource = interactionSource) {
@@ -99,7 +101,14 @@ fun DownloadCard(
DownloadInfo.STATE_NONE -> stringResource(R.string.download_state_none)
DownloadInfo.STATE_WAIT -> stringResource(R.string.download_state_wait)
DownloadInfo.STATE_FAILED -> if (info.legacy <= 0) stateFailed else stateFailed2
DownloadInfo.STATE_FINISH -> stringResource(R.string.download_state_finish)
DownloadInfo.STATE_FINISH -> {
if (showProgress) {
val readProgress by remember { EhDB.getReadProgressFlow(info.gid) }.collectAsState(0)
if (readProgress > 0) "${readProgress + 1}/${info.pages}P" else "${info.pages}P"
} else {
"${info.pages}P"
}
}
else -> null // The item has been removed and this will be disposed soon
}
ProvideTextStyle(MaterialTheme.typography.labelLarge) {
@@ -149,24 +158,24 @@ fun DownloadCard(
Text(
text = categoryText,
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
color = if (Settings.harmonizeCategoryColor.value) Color.Unspecified else EhUtils.categoryTextColor,
color = EhUtils.getCategoryTextColor(categoryColor),
style = MaterialTheme.typography.labelLarge,
)
}
Spacer(modifier = Modifier.weight(1f))
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)
}
val icon = when (downloadState) {
DownloadInfo.STATE_WAIT, DownloadInfo.STATE_DOWNLOAD -> Icons.Default.Pause
DownloadInfo.STATE_FINISH -> Icons.Default.DownloadDone
else -> Icons.Default.PlayArrow
}
if (selectMode) {
Box(modifier = Modifier.offset(4.dp).minimumInteractiveComponentSize()) {
icon(running)
Icon(imageVector = icon, contentDescription = null)
}
} else {
IconButton(onClick = if (running) onStop else onStart, shapes = IconButtonDefaults.shapes(), modifier = Modifier.offset(4.dp)) {
icon(running)
Icon(imageVector = icon, contentDescription = null)
}
}
}
@@ -18,11 +18,11 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil3.compose.AsyncImagePainter
import coil3.compose.rememberAsyncImagePainter
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.ui.util.SETNodeGenerator
import com.ehviewer.core.ui.util.SharedElementBox
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
import com.ehviewer.core.ui.util.thenIf
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.tools.shouldCrop
@@ -24,7 +24,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.parseAsHtml
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.client.data.GalleryComment
import com.ehviewer.core.model.GalleryComment
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
import com.hippo.ehviewer.ui.legacy.LinkifyTextView
import com.hippo.ehviewer.util.ReadableTime
@@ -33,13 +33,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.ui.icons.EhIcons
import com.ehviewer.core.ui.icons.big.SadAndroid
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
import com.ehviewer.core.ui.util.detailThumbGenerator
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
@Composable
fun GalleryDetailHeaderInfoCard(
@@ -23,8 +23,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
@@ -33,22 +33,21 @@ 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.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryInfo.Companion.NOT_FAVORITED
import com.ehviewer.core.ui.component.CrystalCard
import com.ehviewer.core.ui.component.ElevatedCard
import com.ehviewer.core.ui.component.GalleryListCardRating
import com.ehviewer.core.ui.util.SharedElementBox
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
import com.ehviewer.core.ui.util.listThumbGenerator
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ui.tools.GalleryListCardRating
import com.hippo.ehviewer.util.FavouriteStatusRouter
@Composable
@@ -58,6 +57,7 @@ fun GalleryInfoListItem(
onLongClick: () -> Unit,
info: GalleryInfo,
showPages: Boolean,
showProgress: Boolean,
modifier: Modifier = Modifier,
isInFavScene: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@@ -126,7 +126,12 @@ fun GalleryInfoListItem(
Text(text = it)
}
if (info.pages != 0 && showPages) {
Text(text = "${info.pages}P")
val readProgress = if (showProgress) {
remember { EhDB.getReadProgressFlow(info.gid) }.collectAsState(0).value
} else {
0
}
Text(text = if (readProgress > 0) "${readProgress + 1}/${info.pages}P" else "${info.pages}P")
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -135,7 +140,7 @@ fun GalleryInfoListItem(
Text(
text = categoryText,
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
color = if (Settings.harmonizeCategoryColor.value) Color.Unspecified else EhUtils.categoryTextColor,
color = EhUtils.getCategoryTextColor(categoryColor),
)
Spacer(modifier = Modifier.weight(1f))
Text(text = info.posted.orEmpty())
@@ -154,6 +159,7 @@ fun GalleryInfoGridItem(
modifier: Modifier = Modifier,
showLanguage: Boolean = true,
showPages: Boolean = true,
showProgress: Boolean = true,
showFavoriteStatus: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) = ElevatedCard(
@@ -187,11 +193,16 @@ fun GalleryInfoGridItem(
Badge(
modifier = Modifier.align(Alignment.TopEnd).widthIn(min = 32.dp).height(24.dp),
containerColor = categoryColor,
contentColor = if (Settings.harmonizeCategoryColor.value) contentColorFor(categoryColor) else EhUtils.categoryTextColor,
contentColor = EhUtils.getCategoryTextColor(categoryColor),
) {
val shouldShowLanguage = showLanguage && info.simpleLanguage != null
if (showPages && info.pages > 0) {
Text(text = "${info.pages}")
val readProgress = if (showProgress) {
remember { EhDB.getReadProgressFlow(info.gid) }.collectAsState(0).value
} else {
0
}
Text(text = if (readProgress > 0) "${readProgress + 1}/${info.pages}" else "${info.pages}")
if (shouldShowLanguage) {
Spacer(modifier = Modifier.width(4.dp))
}
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.grid.GridCells
@@ -32,7 +33,6 @@ import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -45,20 +45,19 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.ui.component.FastScrollLazyVerticalGrid
import com.ehviewer.core.ui.component.FastScrollLazyVerticalStaggeredGrid
import com.ehviewer.core.ui.icons.EhIcons
import com.ehviewer.core.ui.icons.big.SadAndroid
import com.ehviewer.core.util.launch
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.exception.NoHitsFoundException
import com.hippo.ehviewer.coil.PrefetchAround
import com.hippo.ehviewer.collectAsState
@@ -69,15 +68,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
@Stable
operator fun PaddingValues.plus(r: PaddingValues) = object : PaddingValues {
val l = this@plus
override fun calculateBottomPadding() = l.calculateBottomPadding() + r.calculateBottomPadding()
override fun calculateLeftPadding(layoutDirection: LayoutDirection) = l.calculateLeftPadding(layoutDirection) + r.calculateLeftPadding(layoutDirection)
override fun calculateRightPadding(layoutDirection: LayoutDirection) = l.calculateRightPadding(layoutDirection) + r.calculateRightPadding(layoutDirection)
override fun calculateTopPadding() = l.calculateTopPadding() + r.calculateTopPadding()
}
@Composable
context(_: CoroutineScope, _: Context)
fun GalleryList(
@@ -207,7 +197,6 @@ fun GalleryList(
}
}
}
is LoadState.Error -> {
Surface {
ErrorTip(
@@ -216,7 +205,6 @@ fun GalleryList(
)
}
}
is LoadState.NotLoading -> if (data.itemCount == 0) {
// Only for local favorites as empty gallery lists from network are treated as error
ErrorTip(modifier = Modifier.widthIn(max = 228.dp), text = stringResource(id = R.string.gallery_list_empty_hit))
@@ -23,9 +23,9 @@ import androidx.compose.ui.unit.IntSize
import coil3.BitmapImage
import coil3.compose.AsyncImagePainter.State
import coil3.compose.rememberAsyncImagePainter
import com.ehviewer.core.model.GalleryPreview
import com.ehviewer.core.model.V2GalleryPreview
import com.ehviewer.core.ui.component.CrystalCard
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.V2GalleryPreview
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.tools.shouldCrop
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -19,14 +20,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.ehviewer.core.model.GalleryTagGroup
import com.ehviewer.core.model.PowerStatus
import com.ehviewer.core.model.TagNamespace
import com.ehviewer.core.model.VoteStatus
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.hippo.ehviewer.client.data.PowerStatus
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.client.data.VoteStatus
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.tools.includeFontPadding
@@ -59,6 +61,7 @@ fun GalleryTags(
BaseRoundText(
text = translation,
weak = power == PowerStatus.Weak,
solid = power == PowerStatus.Solid && showVote,
modifier = Modifier.combinedClickable(
onClick = { onTagClick(tag) },
onLongClick = {
@@ -88,6 +91,7 @@ private fun BaseRoundText(
text: String,
modifier: Modifier = Modifier,
weak: Boolean = false,
solid: Boolean = false,
isGroup: Boolean = false,
) {
val bgColor = if (isGroup) {
@@ -103,8 +107,9 @@ private fun BaseRoundText(
Text(
text = text,
modifier = modifier.padding(horizontal = 12.dp, vertical = 4.dp).width(IntrinsicSize.Max),
color = MaterialTheme.colorScheme.onSurface.let { if (weak) it.copy(0.5F) else it },
color = LocalContentColor.current.let { if (weak) it.copy(0.5F) else it },
style = MaterialTheme.typography.labelLarge.includeFontPadding,
textDecoration = if (solid) TextDecoration.Underline else null,
)
}
}
@@ -22,7 +22,6 @@ fun LoadStateIndicator(
is LoadState.Loading -> {
LinearWavyProgressIndicator(modifier = modifier)
}
is LoadState.Error -> {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = state.error.displayString())
@@ -31,6 +30,5 @@ fun LoadStateIndicator(
}
}
}
is LoadState.NotLoading -> Unit
}
@@ -34,17 +34,17 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import arrow.core.raise.ensure
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryInfo.Companion.S_LANG_TAGS
import com.ehviewer.core.ui.component.DropdownFilterChip
import com.ehviewer.core.ui.util.thenIf
import com.ehviewer.core.util.launch
import com.ehviewer.core.util.toIntOrDefault
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANG_TAGS
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitResult
import com.hippo.ehviewer.util.toIntOrDefault
import kotlinx.coroutines.CoroutineScope
private val categoryTable = arrayOf(
@@ -88,7 +88,7 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
SpinnerChoice(
title = stringResource(id = R.string.pref_color_filter_mode),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.color_filter_modes),
values = arrayOf("0", "1", "2", "3", "4", "5"),
values = listOf(0, 1, 2, 3, 4, 5),
field = Settings.colorFilterMode.asMutableState(),
)
SwitchChoice(
@@ -8,15 +8,17 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
fun Modifier.keyEventHandler(
enabled: () -> Boolean,
reverse: () -> Boolean,
volumeKeysEnabled: () -> Boolean,
volumeKeysInverted: () -> Boolean,
movePrevious: () -> Unit,
moveNext: () -> Unit,
) = onPreviewKeyEvent {
if (enabled() && it.type == KeyEventType.KeyDown) {
if (it.type == KeyEventType.KeyDown) {
when (it.key) {
Key.DirectionUp, Key.PageUp, Key.VolumeUp -> if (reverse()) moveNext() else movePrevious()
Key.DirectionDown, Key.PageDown, Key.VolumeDown -> if (reverse()) movePrevious() else moveNext()
Key.DirectionUp, Key.PageUp -> movePrevious()
Key.DirectionDown, Key.PageDown -> moveNext()
Key.VolumeUp if volumeKeysEnabled() -> if (volumeKeysInverted()) moveNext() else movePrevious()
Key.VolumeDown if volumeKeysEnabled() -> if (volumeKeysInverted()) movePrevious() else moveNext()
else -> return@onPreviewKeyEvent false
}
true
@@ -13,21 +13,21 @@ import android.webkit.MimeTypeMap
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.SnackbarHostState
import androidx.core.content.FileProvider
import com.ehviewer.core.files.toOkioPath
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.isAtLeastQ
import com.ehviewer.core.util.isAtLeastT
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.BuildConfig.APPLICATION_ID
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.gallery.Page
import com.hippo.ehviewer.gallery.PageLoader
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.awaitActivityResult
import com.hippo.ehviewer.util.displayPath
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.isAtLeastT
import com.hippo.ehviewer.util.requestPermission
import com.hippo.files.toOkioPath
import eu.kanade.tachiyomi.util.system.logcat
import java.io.File
import kotlin.time.Clock
import moe.tarsin.coroutines.runSuspendCatching
@@ -39,6 +39,7 @@ import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.util.thenIf
import com.ehviewer.core.util.unreachable
import com.google.accompanist.drawablepainter.DrawablePainter
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.collectAsState
@@ -52,7 +53,6 @@ import com.hippo.ehviewer.util.AdsPlaceholderFile
import eu.kanade.tachiyomi.ui.reader.viewer.CombinedCircularProgressIndicator
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.drop
import moe.tarsin.kt.unreachable
@Composable
fun PagerItem(
@@ -109,11 +109,11 @@ fun PagerItem(
// DrawablePainter <: RememberObserver
painter = remember(painter) { painter },
contentDescription = null,
modifier = contentModifier.fillMaxSize().thenIf(drawable is Animatable) {
modifier = Modifier.thenIf(drawable is Animatable) {
onVisibilityChanged(minDurationMs = 33, minFractionVisible = 0.5f) {
drawable!!.setVisible(it, false)
}
},
}.then(contentModifier).fillMaxSize(),
contentScale = contentScale,
colorFilter = when {
grayScale && invert -> grayScaleAndInvertFilter
@@ -42,10 +42,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import me.saket.telephoto.zoomable.OverzoomEffect
import me.saket.telephoto.zoomable.Viewport
import me.saket.telephoto.zoomable.ZoomLimit
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.ZoomableContentLocation
import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.rememberZoomableState
import me.saket.telephoto.zoomable.spatial.CoordinateSpace
import me.saket.telephoto.zoomable.zoomable
@Composable
@@ -171,7 +175,9 @@ private fun PageContainer(
val zoomFraction = snapshotFlow { zoomableState.zoomFraction }.first { it != null }
if (zoomFraction == 0f) {
delay(500)
val contentSize = zoomableState.transformedContentBounds.size
val contentSize = with(zoomableState.coordinateSystem) {
unscaledContentBounds(false).sizeIn(CoordinateSpace.Viewport)
}
val scale = ContentScale.FillHeight.computeScaleFactor(contentSize, layoutSize)
val targetScale = scale.scaleX.coerceAtMost(zoomableState.zoomSpec.maximum.factor)
val offset = alignment.align(0, layoutSize.width.toInt(), LayoutDirection.Ltr)
@@ -198,20 +204,17 @@ private fun PageContainer(
moveToNext()
}
}
NavigationRegion.PREV -> {
val canPan = if (isRtl) panLeft(distance, bounds) else panRight(distance, bounds)
if (!canPan) {
moveToPrevious()
}
}
NavigationRegion.RIGHT -> {
if (!panLeft(distance, bounds)) {
if (isRtl) moveToPrevious() else moveToNext()
}
}
NavigationRegion.LEFT -> {
if (!panRight(distance, bounds)) {
if (isRtl) moveToNext() else moveToPrevious()
@@ -258,7 +261,11 @@ private inline fun ZoomableState?.canPan(getRemaining: (Rect) -> Float): Boolean
contract {
returns(true) implies (this@canPan != null)
}
return this != null && Settings.navigateToPan.value && getRemaining(transformedContentBounds) > 1f
return this != null && Settings.navigateToPan.value &&
getRemaining(with(coordinateSystem) { contentBounds(false).rectIn(CoordinateSpace.Viewport) }) > 1f
}
private val PagerZoomSpec = ZoomSpec(maxZoomFactor = 5f)
private val PagerZoomSpec = ZoomSpec(
maximum = ZoomLimit(factor = 5f),
minimum = ZoomLimit(factor = 1f, overzoomEffect = OverzoomEffect.Disabled),
)
@@ -9,19 +9,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.integerArrayResource
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import com.ehviewer.core.i18n.R
import com.ehviewer.core.util.isAtLeastP
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.util.isAtLeastP
@Composable
fun ReaderGeneralSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
SpinnerChoice(
title = stringResource(id = R.string.pref_reader_theme),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.reader_themes),
values = stringArrayResource(id = com.hippo.ehviewer.R.array.reader_themes_values),
values = integerArrayResource(id = com.hippo.ehviewer.R.array.reader_themes_values).toList(),
field = Settings.readerTheme.asMutableState(),
)
SwitchChoice(
@@ -63,8 +64,15 @@ fun ReaderGeneralSetting() = Column(modifier = Modifier.verticalScroll(rememberS
title = stringResource(id = R.string.pref_page_transitions),
field = Settings.pageTransitions.asMutableState(),
)
val volume = Settings.readWithVolumeKeys.asMutableState()
SwitchChoice(
title = stringResource(id = R.string.settings_read_reverse_controls),
field = Settings.readerReverseControls.asMutableState(),
title = stringResource(id = R.string.settings_read_volume_page),
field = volume,
)
AnimatedVisibility(visible = volume.value) {
SwitchChoice(
title = stringResource(id = R.string.settings_read_reverse_volume),
field = Settings.readWithVolumeKeysInverted.asMutableState(),
)
}
}
@@ -12,40 +12,37 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.integerArrayResource
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.collectAsState
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@Composable
fun ReaderModeSetting() = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
fun ReaderModeSetting(isWebtoon: Boolean) = Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) {
SpinnerChoice(
title = stringResource(id = R.string.pref_category_reading_mode),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.viewers_selector),
values = arrayOf("0", "1", "2", "3", "4", "5"),
values = ReadingModeType.entries.map { it.prefValue },
field = Settings.readingMode.asMutableState(),
)
SpinnerChoice(
title = stringResource(id = R.string.rotation_type),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.rotation_type),
values = arrayOf("0", "8", "16", "24", "32", "40", "48"),
values = OrientationType.entries.map { it.prefValue },
field = Settings.orientationMode.asMutableState(),
)
Spacer(modifier = Modifier.size(16.dp))
val isPager by Settings.readingMode.collectAsState { value ->
ReadingModeType.isPagerType(value)
}
Crossfade(targetState = isPager, label = "Pager") { pager ->
if (pager) {
PagerSetting()
} else {
Crossfade(targetState = isWebtoon, label = "Setting") { webtoon ->
if (webtoon) {
WebtoonSetting()
} else {
PagerSetting()
}
}
}
@@ -61,7 +58,7 @@ private fun PagerSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_viewer_nav),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.pager_nav),
values = arrayOf("0", "1", "2", "3", "4", "5"),
values = listOf(0, 1, 2, 3, 4, 5),
field = pagerNav,
)
AnimatedVisibility(visible = pagerNav.value != 5) {
@@ -69,7 +66,7 @@ private fun PagerSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
values = arrayOf("0", "1", "2", "3"),
values = listOf(0, 1, 2, 3),
field = Settings.readerPagerNavInverted.asMutableState(),
)
SwitchChoice(
@@ -82,7 +79,7 @@ private fun PagerSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_image_scale_type),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.image_scale_type),
values = arrayOf("1", "2", "3", "4", "5", "6"),
values = listOf(1, 2, 3, 4, 5, 6),
field = scaleType,
)
AnimatedVisibility(visible = scaleType.value == 1) {
@@ -94,7 +91,7 @@ private fun PagerSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_zoom_start),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.zoom_start),
values = arrayOf("1", "2", "3", "4"),
values = listOf(1, 2, 3, 4),
field = Settings.zoomStart.asMutableState(),
)
SwitchChoice(
@@ -114,21 +111,21 @@ private fun WebtoonSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_viewer_nav),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.webtoon_nav),
values = arrayOf("0", "1", "2", "3", "4", "5"),
values = listOf(0, 1, 2, 3, 4, 5),
field = webtoonNav,
)
AnimatedVisibility(visible = webtoonNav.value != 5) {
SpinnerChoice(
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
values = arrayOf("0", "1", "2", "3"),
values = listOf(0, 1, 2, 3),
field = Settings.readerWebtoonNavInverted.asMutableState(),
)
}
SpinnerChoice(
title = stringResource(id = R.string.pref_webtoon_side_padding),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.webtoon_side_padding),
values = stringArrayResource(id = com.hippo.ehviewer.R.array.webtoon_side_padding_values),
values = integerArrayResource(id = com.hippo.ehviewer.R.array.webtoon_side_padding_values).toList(),
field = Settings.webtoonSidePadding.asMutableState(),
)
SwitchChoice(
@@ -13,14 +13,11 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.rememberPagerState
@@ -53,22 +50,21 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.keepScreenOn
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.zIndex
import androidx.core.view.WindowInsetsControllerCompat
import arrow.core.Either
import arrow.core.Either.Companion.catch
import arrow.core.raise.ensure
import arrow.core.right
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.ui.util.Await
import com.ehviewer.core.ui.util.asyncInVM
import com.ehviewer.core.ui.util.rememberSystemUiController
import com.ehviewer.core.ui.util.thenIf
import com.ehviewer.core.util.launch
import com.ehviewer.core.util.launchIO
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.ehviewer.core.util.unreachable
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.archiveFile
@@ -82,7 +78,6 @@ import com.hippo.ehviewer.gallery.useEhPageLoader
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.theme.EhTheme
import com.hippo.ehviewer.ui.theme.scrim
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitInputText
import com.hippo.ehviewer.ui.tools.dialog
@@ -104,7 +99,6 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.Serializable
import moe.tarsin.kt.unreachable
import moe.tarsin.string
import okio.Path.Companion.toPath
@@ -132,26 +126,24 @@ fun AnimatedVisibilityScope.ReaderScreen(args: ReaderScreenArgs, navigator: Dest
val uiController = rememberSystemUiController()
DisposableEffect(uiController) {
val lightStatusBar = uiController.statusBarDarkContentEnabled
uiController.statusBarDarkContentEnabled = bgColor == Color.White
onDispose {
uiController.statusBarDarkContentEnabled = lightStatusBar
}
}
LaunchedEffect(uiController) {
snapshotFlow { bgColor }.collect {
uiController.statusBarDarkContentEnabled = it == Color.White
}
}
Await(
block = asyncInVM(args) { alive ->
suspendCancellableCoroutine { cont ->
alive.launchIO {
catch {
usePageLoader(args) { loader ->
cont.resume(loader.right())
awaitCancellation()
}
}.let { left -> cont.resume(left) }
with(alive) {
launchIO {
catch {
usePageLoader(args) { loader ->
cont.resume(loader.right())
awaitCancellation()
}
}.let { left -> cont.resume(left) }
}
}
}
}.value.run {
@@ -183,7 +175,7 @@ fun AnimatedVisibilityScope.ReaderScreen(args: ReaderScreenArgs, navigator: Dest
}
@Composable
context(activity: MainActivity, _: SnackbarHostState, _: DialogState, _: CoroutineScope)
context(activity: MainActivity, _: SnackbarHostState, _: DialogState, _: CoroutineScope, _: DestinationsNavigator)
fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
LaunchedEffect(Unit) {
val orientation = activity.requestedOrientation
@@ -198,18 +190,28 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
}
}
}
val webtoon = remember(info) {
// Tags in database may or may not have the prefix "other:"
info?.simpleTags?.any { it.endsWith("webtoon") } == true
}
val showSeekbar by Settings.showReaderSeekbar.collectAsState()
val readingMode by Settings.readingMode.collectAsState { ReadingModeType.fromPreference(it) }
val reverseControls by Settings.readerReverseControls.collectAsState()
val readingMode by Settings.readingMode.collectAsState {
when (val mode = ReadingModeType.fromPreference(it)) {
ReadingModeType.DEFAULT -> if (webtoon) ReadingModeType.WEBTOON else ReadingModeType.RIGHT_TO_LEFT
else -> mode
}
}
val volumeKeysEnabled by Settings.readWithVolumeKeys.collectAsState()
val volumeKeysInverted by Settings.readWithVolumeKeysInverted.collectAsState()
val fullscreen by Settings.fullscreen.collectAsState()
val cutoutShort by Settings.cutoutShort.collectAsState()
val keepScreenOn by Settings.keepScreenOn.collectAsState()
val uiController = rememberSystemUiController()
DisposableEffect(uiController) {
uiController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
uiController.showTransientSystemBarsBySwipe = true
onDispose {
uiController.isSystemBarsVisible = true
uiController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
uiController.showTransientSystemBarsBySwipe = false
}
}
val lazyListState = rememberLazyListState(LazyLayoutCacheWindow(SCROLL_FRACTION, SCROLL_FRACTION), pageLoader.startPage)
@@ -220,8 +222,8 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
val focusRequester = remember { FocusRequester() }
Box(
Modifier.keyEventHandler(
enabled = { !appbarVisible },
reverse = { reverseControls },
volumeKeysEnabled = { volumeKeysEnabled && !appbarVisible },
volumeKeysInverted = { volumeKeysInverted },
movePrevious = { launch { if (isWebtoon) lazyListState.scrollUp() else pagerState.moveToPrevious() } },
moveNext = { launch { if (isWebtoon) lazyListState.scrollDown() else pagerState.moveToNext() } },
).focusRequester(focusRequester).focusable().thenIf(keepScreenOn) { keepScreenOn() },
@@ -229,18 +231,14 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
val bgColor by collectBackgroundColorAsState()
syncState.Sync(isWebtoon) { appbarVisible = false }
if (fullscreen) {
LaunchedEffect(Unit) {
snapshotFlow { appbarVisible }.collect {
uiController.isSystemBarsVisible = it
}
val bgColor by collectBackgroundColorAsState()
val isDarkTheme = isSystemInDarkTheme()
LaunchedEffect(isDarkTheme) {
snapshotFlow { appbarVisible }.collect {
uiController.isSystemBarsVisible = it || !fullscreen
uiController.statusBarDarkContentEnabled = if (it) !isDarkTheme else bgColor == Color.White
}
Box(
Modifier.windowInsetsTopHeight(WindowInsets.statusBars).fillMaxWidth()
.align(Alignment.TopCenter).background(bgColor.scrim()).zIndex(1f),
)
}
var showNavigationOverlay by remember {
val showOnStart = Settings.showNavigationOverlayNewUser.value || Settings.showNavigationOverlayOnStart.value
@@ -342,6 +340,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
}
ReaderAppBars(
visible = appbarVisible,
title = pageLoader.title,
isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT,
showSeekBar = showSeekbar,
currentPage = syncState.sliderValue,
@@ -364,7 +363,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
dragHandle = null,
contentWindowInsets = { WindowInsets() },
) {
SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
SettingsPager(isWebtoon = isWebtoon, modifier = Modifier.fillMaxSize()) { page ->
isColorFilter = page == 2
appbarVisible = !isColorFilter
}
@@ -372,7 +371,6 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
}
}
},
modifier = Modifier.align(Alignment.BottomCenter),
)
}
}
@@ -384,7 +382,7 @@ suspend inline fun <T> usePageLoader(args: ReaderScreenArgs, crossinline block:
val page = args.page.takeUnless { it == -1 } ?: EhDB.getReadProgress(info.gid)
val archive = DownloadManager.getDownloadInfo(info.gid)?.archiveFile
if (archive != null) {
useArchivePageLoader(archive, info.gid, page, info.hasAds, { error("Managed Archive have password???") }, block)
useArchivePageLoader(archive, info, page, info.hasAds, { error("Managed Archive have password???") }, block)
} else {
useEhPageLoader(info, page, block)
}
@@ -25,7 +25,7 @@ private val tabs = intArrayOf(
)
@Composable
fun SettingsPager(modifier: Modifier = Modifier, onPageSelected: (Int) -> Unit) {
fun SettingsPager(isWebtoon: Boolean, modifier: Modifier = Modifier, onPageSelected: (Int) -> Unit) {
val pagerState = rememberPagerState { tabs.size }
LaunchedEffect(onPageSelected) {
snapshotFlow { pagerState.currentPage }.collect { page ->
@@ -53,7 +53,7 @@ fun SettingsPager(modifier: Modifier = Modifier, onPageSelected: (Int) -> Unit)
) { page ->
ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
when (page) {
0 -> ReaderModeSetting()
0 -> ReaderModeSetting(isWebtoon)
1 -> ReaderGeneralSetting()
2 -> ColorFilterSetting()
}
@@ -24,6 +24,8 @@ import eu.kanade.tachiyomi.ui.reader.viewer.NavigationRegions
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import eu.kanade.tachiyomi.ui.reader.viewer.getAction
import kotlinx.coroutines.launch
import me.saket.telephoto.zoomable.OverzoomEffect
import me.saket.telephoto.zoomable.ZoomLimit
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.rememberZoomableState
import me.saket.telephoto.zoomable.zoomable
@@ -90,4 +92,7 @@ fun WebtoonViewer(
}
}
private val WebtoonZoomSpec = ZoomSpec(maxZoomFactor = 3f)
private val WebtoonZoomSpec = ZoomSpec(
maximum = ZoomLimit(factor = 3f),
minimum = ZoomLimit(factor = 1f, overzoomEffect = OverzoomEffect.Disabled),
)
@@ -28,10 +28,10 @@ import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.Slider
@Composable
fun SpinnerChoice(title: String, entries: Array<String>, values: Array<String>, field: MutableState<Int>) {
fun SpinnerChoice(title: String, entries: Array<String>, values: List<Int>, field: MutableState<Int>) {
var value by field
var dropdown by remember { mutableStateOf(false) }
val data = remember { entries zip values.map { v -> v.toInt() } }
val data = remember { entries zip values }
Row(
modifier = Modifier.fillMaxWidth().height(48.dp).clickable { dropdown = true }.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -1,8 +1,8 @@
package com.hippo.ehviewer.ui.screen
import com.hippo.ehviewer.dao.DownloadInfo
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.util.containsIgnoreCase
import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.util.containsIgnoreCase
import kotlinx.serialization.Serializable
@Serializable
@@ -5,8 +5,10 @@ import android.view.ViewConfiguration
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -17,6 +19,7 @@ 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.plus
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
@@ -43,6 +46,7 @@ import androidx.compose.material.icons.filled.Reorder
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
@@ -82,7 +86,9 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import arrow.core.partially1
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.TagNamespace
import com.ehviewer.core.ui.component.FAB_ANIMATE_TIME
import com.ehviewer.core.ui.component.FabLayout
import com.ehviewer.core.ui.component.FastScrollLazyColumn
@@ -91,12 +97,16 @@ import com.ehviewer.core.ui.component.LocalSideSheetState
import com.ehviewer.core.ui.component.ProvideSideSheetContent
import com.ehviewer.core.ui.icons.EhIcons
import com.ehviewer.core.ui.icons.big.Download
import com.ehviewer.core.ui.util.Await
import com.ehviewer.core.ui.util.HapticFeedbackType
import com.ehviewer.core.ui.util.asyncState
import com.ehviewer.core.ui.util.ifTrueThen
import com.ehviewer.core.ui.util.rememberHapticFeedback
import com.ehviewer.core.ui.util.rememberInVM
import com.ehviewer.core.ui.util.takeAndClear
import com.ehviewer.core.ui.util.thenIf
import com.ehviewer.core.util.launch
import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.mapToLongArray
import com.ehviewer.core.util.onEachLatest
import com.ehviewer.core.util.withNonCancellableContext
import com.ehviewer.core.util.withUIContext
@@ -104,11 +114,8 @@ import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadManager.downloadInfoList
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.download.SortMode
@@ -117,20 +124,14 @@ import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.confirmRemoveDownloadRange
import com.hippo.ehviewer.ui.main.DownloadCard
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
import com.hippo.ehviewer.ui.main.plus
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.showMoveDownloadLabelList
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.HapticFeedbackType
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitInputText
import com.hippo.ehviewer.ui.tools.awaitSelectAction
import com.hippo.ehviewer.ui.tools.awaitSelectItemWithCheckBox
import com.hippo.ehviewer.ui.tools.awaitSingleChoice
import com.hippo.ehviewer.ui.tools.rememberHapticFeedback
import com.hippo.ehviewer.util.mapToLongArray
import com.hippo.ehviewer.util.takeAndClear
import com.jamal.composeprefs3.ui.ifTrueThen
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -146,8 +147,10 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
var gridView by Settings.gridView.asMutableState()
var sortMode by Settings.downloadSortMode.asMutableState()
val filterMode by Settings.downloadFilterMode.collectAsState { DownloadsFilterMode.from(it) }
val showProgress by Settings.showReadingProgress.collectAsState()
var filterState by rememberSerializable { mutableStateOf(DownloadsFilterState(filterMode, Settings.recentDownloadLabel.value)) }
var invalidateKey by rememberSaveable { mutableStateOf(false) }
var isLoading by rememberSaveable { mutableStateOf(true) }
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
var searchBarOffsetY by remember { mutableIntStateOf(0) }
val animateItems by Settings.animateItems.collectAsState()
@@ -183,10 +186,19 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
},
)
val hint = stringResource(R.string.search_bar_hint, title)
val list = remember(filterState, invalidateKey) {
downloadInfoList.filterTo(mutableStateListOf()) { info ->
filterState.take(info)
val list = if (DownloadManager.isInitialized) {
remember(filterState, invalidateKey) {
DownloadManager.downloadInfoList.filterTo(mutableStateListOf()) { info ->
filterState.take(info)
}.also {
launch {
delay(200)
isLoading = false
}
}
}
} else {
remember { mutableStateListOf() }
}
val newLabel = stringResource(R.string.new_label_title)
@@ -218,6 +230,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
fun switchLabel(label: String?) {
Settings.recentDownloadLabel.value = label
filterState = filterState.copy(label = label)
fabHidden = false
}
LaunchedEffect(filterState) {
@@ -562,7 +575,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
}
fun onItemClick(info: DownloadInfo) {
launchIO { EhDB.putHistoryInfo(info.galleryInfo) }
launchIO { EhDB.putHistoryInfo(info) }
navToReader(info.galleryInfo)
}
@@ -584,6 +597,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
info = info,
modifier = Modifier.thenIf(animateItems) { animateItem() },
showLanguage = false,
showProgress = showProgress,
)
}
}
@@ -623,6 +637,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
onStop = { launchIO { DownloadManager.stopDownload(info.gid) } },
info = info,
selectMode = selectMode,
showProgress = showProgress,
modifier = Modifier.height(height),
interactionSource = interactionSource,
)
@@ -632,24 +647,26 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
}
}
Await({ delay(200) }) {
if (list.isEmpty()) {
Column(
modifier = Modifier.padding(realPadding).fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = EhIcons.Big.Default.Download,
contentDescription = null,
modifier = Modifier.padding(16.dp),
tint = MaterialTheme.colorScheme.primary,
)
Text(
text = stringResource(id = R.string.no_download_info),
style = MaterialTheme.typography.headlineMedium,
)
}
if (isLoading) {
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface), contentAlignment = Alignment.Center) {
CircularWavyProgressIndicator()
}
} else if (list.isEmpty()) {
Column(
modifier = Modifier.padding(realPadding).fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = EhIcons.Big.Default.Download,
contentDescription = null,
modifier = Modifier.padding(16.dp),
tint = MaterialTheme.colorScheme.primary,
)
Text(
text = stringResource(id = R.string.no_download_info),
style = MaterialTheme.typography.headlineMedium,
)
}
}
}
@@ -689,9 +706,12 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
oldMode.groupByDownloadLabel,
)
val mode = SortMode.All[selected].copy(groupByDownloadLabel = checked)
DownloadManager.sortDownloads(mode)
sortMode = mode.flag
invalidateKey = !invalidateKey
if (mode != oldMode) {
sortMode = mode.flag
isLoading = true
DownloadManager.sortDownloads(mode)
invalidateKey = !invalidateKey
}
}
onClick(Icons.Default.FilterList) {
val downloadStates = contextOf<Context>().resources.getStringArray(com.hippo.ehviewer.R.array.download_state).toList()
@@ -54,6 +54,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.compose.collectAsLazyPagingItems
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.ui.component.FAB_ANIMATE_TIME
import com.ehviewer.core.ui.component.FabLayout
import com.ehviewer.core.ui.component.LocalSideSheetState
@@ -61,14 +62,15 @@ import com.ehviewer.core.ui.component.ProvideSideSheetContent
import com.ehviewer.core.ui.icons.EhIcons
import com.ehviewer.core.ui.icons.filled.GoTo
import com.ehviewer.core.ui.util.asyncState
import com.ehviewer.core.ui.util.takeAndClear
import com.ehviewer.core.ui.util.thenIf
import com.ehviewer.core.util.launch
import com.ehviewer.core.util.mapToLongArray
import com.ehviewer.core.util.onEachLatest
import com.ehviewer.core.util.withUIContext
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.DrawerHandle
@@ -81,8 +83,6 @@ import com.hippo.ehviewer.ui.main.GalleryList
import com.hippo.ehviewer.ui.startDownload
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitSelectItem
import com.hippo.ehviewer.util.mapToLongArray
import com.hippo.ehviewer.util.takeAndClear
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -104,6 +104,8 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
var urlBuilder by viewModel.urlBuilder
var searchBarExpanded by rememberSaveable { mutableStateOf(false) }
var searchBarOffsetY by remember { mutableIntStateOf(0) }
var fabExpanded by remember { mutableStateOf(false) }
var fabHidden by remember { mutableStateOf(false) }
// Derived State
val keyword = urlBuilder.keyword
@@ -163,6 +165,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
val newCat = index - 2
refresh(FavListUrlBuilder(newCat))
Settings.recentFavCat = newCat
fabHidden = false
launch { sheetState.close() }
},
colors = listItemOnDrawerColor(urlBuilder.favCat == index - 2),
@@ -171,8 +174,6 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
}
}
var fabExpanded by remember { mutableStateOf(false) }
var fabHidden by remember { mutableStateOf(false) }
val checkedInfoMap = remember { mutableStateMapOf<Long, BaseGalleryInfo>() }
val selectMode = checkedInfoMap.isNotEmpty()
DrawerHandle(!selectMode && !searchBarExpanded)
@@ -200,6 +201,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
val listMode by Settings.listMode.collectAsState()
val height by collectListThumbSizeAsState()
val showPages by Settings.showGalleryPages.collectAsState()
val showProgress by Settings.showReadingProgress.collectAsState()
val searchBarConnection = remember {
val slop = ViewConfiguration.get(contextOf<Context>()).scaledTouchSlop
val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() }
@@ -244,6 +246,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
},
info = info,
showPages = showPages,
showProgress = showProgress,
modifier = Modifier.height(height),
isInFavScene = true,
interactionSource = interactionSource,
@@ -273,6 +276,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
},
info = info,
showPages = showPages,
showProgress = showProgress,
showFavoriteStatus = false,
interactionSource = interactionSource,
)
@@ -313,7 +317,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
}
onClick(EhIcons.Default.GoTo) {
val date = awaitSelectDate()
refresh(urlBuilder.copy(jumpTo = date))
refresh(urlBuilder.copy(jumpTo = date, prev = null, next = "2"))
}
onClick(Icons.Default.Refresh) {
refresh()
@@ -13,19 +13,24 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.paging.filter
import androidx.paging.map
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.ui.tools.foldToLoadResult
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.tarsin.coroutines.runSuspendCatching
class FavoritesViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
private val mutex = Mutex()
val urlBuilder by savedStateHandle.saved(MutableStateSerializer()) {
mutableStateOf(FavListUrlBuilder(favCat = Settings.recentFavCat))
}
@@ -41,44 +46,40 @@ class FavoritesViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
} else {
EhDB.searchLocalFav(keywordNow)
}
}
}.flow.map { data -> data.map<_, BaseGalleryInfo> { it } }
} else {
Pager(PagingConfig(25)) {
Pager(PagingConfig(DEFAULT_PAGE_SIZE, prefetchDistance = 20)) {
object : PagingSource<String, BaseGalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
override suspend fun load(params: LoadParams<String>) = withIOContext {
val urlBuilder = urlBuilder.value
when (params) {
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
is LoadParams.Append -> urlBuilder.setIndex(params.key, isNext = true)
is LoadParams.Refresh -> {
val key = params.key
if (key.isNullOrBlank()) {
if (urlBuilder.jumpTo != null) {
urlBuilder.next ?: urlBuilder.setIndex("2", true)
}
} else {
urlBuilder.setIndex(key, false)
val url = mutex.withLock {
with(urlBuilder.value) {
when (params) {
is LoadParams.Prepend -> setIndex(params.key, isNext = false)
is LoadParams.Append -> setIndex(params.key, isNext = true)
is LoadParams.Refresh -> params.key?.let { setIndex(it, false) }
}
build()
}
}
runSuspendCatching {
EhEngine.getFavorites(urlBuilder.build())
EhEngine.getFavorites(url)
}.foldToLoadResult { result ->
Settings.favCat = result.catArray.toTypedArray()
Settings.favCount = result.countArray.toIntArray()
Settings.favCloudCount = result.countArray.sum()
urlBuilder.jumpTo = null
LoadResult.Page(result.galleryInfoList, result.prev, result.next)
}
}
}
}.flow.map { data ->
// https://github.com/FooIbar/EhViewer/issues/1190
// Workaround for duplicate items when sorting by favorited time
val gidSet = MutableLongSet(DEFAULT_PAGE_SIZE)
data.filter { gidSet.add(it.gid) }
}
}.flow.map { pagingData ->
// https://github.com/FooIbar/EhViewer/issues/1190
// Workaround for duplicate items when sorting by favorited time
val gidSet = MutableLongSet(50)
pagingData.filter { gidSet.add(it.gid) }
}
}.cachedIn(viewModelScope)
}
private const val DEFAULT_PAGE_SIZE = 50
@@ -11,6 +11,7 @@ import android.text.style.URLSpan
import androidx.annotation.ColorInt
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -25,6 +26,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -82,22 +84,24 @@ import androidx.core.text.buildSpannedString
import androidx.core.text.getSpans
import androidx.core.text.inSpans
import androidx.core.text.parseAsHtml
import com.ehviewer.core.database.model.Filter
import com.ehviewer.core.database.model.FilterMode
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryComment
import com.ehviewer.core.ui.util.animateFloatMergePredictiveBackAsState
import com.ehviewer.core.ui.util.snackBarPadding
import com.ehviewer.core.ui.util.thenIf
import com.ehviewer.core.util.isAtLeastP
import com.ehviewer.core.util.launch
import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withUIContext
import com.hippo.ehviewer.Settings
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.Screen
import com.hippo.ehviewer.ui.jumpToReaderByPage
import com.hippo.ehviewer.ui.main.GalleryCommentCard
@@ -116,7 +120,6 @@ 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.system.logcat
import kotlin.math.roundToInt
import moe.tarsin.coroutines.runSuspendCatching
import moe.tarsin.navigate
@@ -193,7 +196,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
val density = LocalDensity.current
suspend fun refreshComment(showAll: Boolean) {
val url = EhUrl.getGalleryDetailUrl(galleryDetail.gid, galleryDetail.token, 0, showAll)
val url = EhUrl.getGalleryDetailUrl(galleryDetail.gid, galleryDetail.token, allComment = showAll)
val detail = EhEngine.getGalleryDetail(url)
comments = detail.comments
}
@@ -216,7 +219,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
suspend fun sendComment() {
commenting = false
withUIContext { focusManager.clearFocus() }
val url = EhUrl.getGalleryDetailUrl(galleryDetail.gid, galleryDetail.token, 0, false)
val url = EhUrl.getGalleryDetailUrl(galleryDetail.gid, galleryDetail.token)
userComment.runSuspendCatching {
val bbcode = annotatedString.normalizeSpan().toBBCode()
logcat("sendComment") { bbcode }
@@ -394,7 +397,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
}
if (!comment.voteState.isNullOrEmpty()) {
onSelect(checkVoteStatus) {
showCommentVoteStatus(comment.voteState)
showCommentVoteStatus(comment.voteState!!)
}
}
}()
@@ -455,6 +458,11 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
}
}
}
if (!isAtLeastP) {
// Workaround for crash when 0-sized TextField is focused
// https://issuetracker.google.com/440964236
Box(Modifier.size(1.dp).focusable())
}
Surface(
modifier = Modifier.align(Alignment.BottomCenter).layout { measurable, constraints ->
val origin = measurable.measure(constraints)
Loaded 100 of 265 files, more files were not shown because too many files have changed in this diff. Show more