Compare commits

...
289 Commits
Author SHA1 Message Date
yenru0 f66a463393 Release 1.15.0 2026-10-08 17:48:29 +09:00
yenru0 33fd89041c Merge remote-tracking branch 'origin/main' 2026-10-08 17:45:37 +09:00
Dude so hot 40271d744b Fixup deser type 2026-10-08 16:36:38 +08:00
yenru0 e6eba042b9 Rewrite README for standalone fork 2026-10-08 17:29:09 +09:00
yenru0 aeb9b47228 Raise default API request timeout to 30s 2026-10-08 17:28:42 +09:00
yenru0 8b2ea9a418 Disable update check by default
The updater compares against upstream CI builds, which is meaningless
for this fork. Manual check still works.
2026-10-08 17:28:42 +09:00
yenru0 97bca3eecc Adopt standalone applicationId yenru0.yrkaierx.ehviewer 2026-10-08 17:28:42 +09:00
yenru0 d7c64f540b Gate Cronet engine behind enable_quic (default off)
Cronet bypasses the OkHttp DNS/SSL stack and would nullify the
anti-DPI hooks. enable_quic is the effective engine switch.
2026-10-08 17:28:23 +09:00
yenru0 793ba815f7 Restore built-in hosts, domain fronting and anti-DPI SNI evasion
- built-in hosts for exhentai/github (EhHostsConfig.kt)
- Cloudflare CDN suffix fallback for blocked domains (EhDns.kt)
- SNI spoofing to front host for domain-fronted TLS (EhSSLSocketFactory.kt)
2026-10-08 17:28:23 +09:00
Dude so hot d901326640 Support automatically download ci 2026-10-08 16:19:03 +08:00
Dude so hot de2321ce8f Fix repo url
Will add ci download later
2026-10-08 15:19:27 +08:00
Dude so hot 39398365e5 ChapterNavigator use RollingNumber, no dynamic size 2026-10-08 03:57:28 +08:00
Dude so hot baf8333b70 Refactor for upstream Slider api changes 2026-10-08 03:44:12 +08:00
Dude so hot 5c406ac8b0 Fix Slider api migration 2026-10-08 01:51:03 +08:00
Dude so hot f07ff61069 Move to collection literals 2026-10-08 01:05:02 +08:00
Dude so hot 2163238f48 Use aboutlibraries default style
It's refined enough.
2026-10-08 00:42:05 +08:00
Dude so hot 1f4b93bddd Upgrade rust toolchain 2026-10-08 00:03:58 +08:00
Dude so hot ab2c60824a Upgrade Kotlin deps 2026-10-07 23:49:40 +08:00
tarsinandDude so hot dbe455350d Forgejo CI (#1)
Co-authored-by: Dude so hot <djohn@fbi.gov>
Reviewed-on: https://codeberg.org/EhViewer/EhViewer/pulls/1
2026-10-07 16:48:55 +02:00
Dude so hot 78cb123af1 Enable LinkBuffer Composer 2026-08-22 19:25:58 +08:00
Dude so hot 5cd2496f7f Update rust toolchain 2026-08-22 19:21:05 +08:00
Dude so hot 06d4abd916 Notify Marshmallow deprecation in readme
For material3-ripple update
2026-08-22 18:27:39 +08:00
renovate[bot]andDude so hot 881ad21ea2 Update deps & Fix some deprecations (#3004)
* Update deps & Fix some deprecations

* fixup!

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
2026-08-22 18:07:56 +08:00
FooIbar 257bfe843e Add missing nbsp and times entities 2026-07-27 11:27:21 +08:00
FooIbar 49eb3bc234 Separate thumbnail and gallery image caches
Prevent thumbnail cache from being evicted by gallery cache.
2026-06-08 17:04:49 +08:00
FooIbar 3006e25f3d Disable -Werror until Kotlin 2.4 support in IDE 2026-06-07 23:37:00 +08:00
FooIbar 79fbe9cbd0 Remove obsolete workaround 2026-06-07 22:42:29 +08:00
FooIbar 36b0eeef5a Kotlin 2.4 2026-06-07 22:31:24 +08:00
FooIbar eeeeb3ca0d Java 25 2026-06-07 20:25:22 +08:00
FooIbar 345dbc2b86 Target SDK 37 2026-06-07 00:09:06 +08:00
renovate[bot] cca4adc6a3 Lock file maintenance (#2982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-16 06:53:58 +00:00
renovate[bot]andFooIbar 8decbed291 Update dependency rust-nightly to v2026-05-15 (#2977)
* Update dependency rust-nightly to v2026-05-15

* Fix clippy

* Fix fmt

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-05-16 06:39:56 +00:00
renovate[bot] 960ac5d2fe Update spotless to v8.5.1 (#2981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-16 01:00:15 +00:00
renovate[bot] 20aef7f77c Update Rust crate quick-xml to 0.40 (#2980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-16 00:59:31 +00:00
renovate[bot] 96d1ba780b Update kotlinx-coroutines monorepo to v1.11.0 (#2978)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 20:42:33 +00:00
renovate[bot] 8383821324 Update ktor monorepo to v3.5.0 (#2979)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 20:41:31 +00:00
renovate[bot] f46a5e4652 Update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.8.0 (#2976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 16:54:20 +00:00
renovate[bot] d8d2e8299d Update compose.multiplatform to v1.11.0 (#2975)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 08:49:58 +00:00
renovate[bot] 284af1e6fb Update aboutlibraries to v14.2.0 (#2974)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 08:49:39 +00:00
renovate[bot] c7576a3941 Update Gradle to v9.5.1 (#2973)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 07:09:45 +00:00
renovate[bot] c216bc3b82 Update dependency com.google.devtools.ksp to v2.3.8 (#2972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 07:08:22 +00:00
renovate[bot] 2ac3b179c0 Update dependency androidx.paging:paging-compose to v3.5.0 (#2970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 01:09:37 +00:00
renovate[bot] aa2269fd9f Update moko.resources to v0.26.4 (#2968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 21:18:12 +00:00
renovate[bot] 793a17afea Update dependency androidx.compose:compose-bom-alpha to v2026.05.00 (#2969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 21:17:06 +00:00
renovate[bot] dab6dcc04d Update AGP to v9.2.1 (#2966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 20:21:07 +00:00
renovate[bot] be398a6ab6 Update benchmark to v1.5.0-alpha06 (#2967)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 20:19:12 +00:00
renovate[bot] 80cffe479f Update aboutlibraries to v14.1.0 (#2963)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-01 18:20:30 +00:00
FooIbar 30c5fe38c4 Update Rust crate rxing to 0.9 2026-05-01 16:54:45 +08:00
renovate[bot] 87b538d703 Lock file maintenance (#2961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-01 03:18:31 +00:00
renovate[bot] affe4ffbf1 Update Gradle to v9.5.0 (#2960)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 14:19:42 +00:00
renovate[bot] 3d74869187 Update kotlin monorepo to v2.3.21 (#2958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 09:45:59 +00:00
renovate[bot] 58d90ff49c Update dependency com.google.devtools.ksp to v2.3.7 (#2957)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 02:49:28 +00:00
renovate[bot] 07217f1481 Update dependency androidx.biometric:biometric-compose to v1.4.0-alpha07 (#2955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-22 20:47:42 +00:00
renovate[bot] cf87df35a6 Update dependency androidx.compose:compose-bom-alpha to v2026.04.01 (#2956)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-22 20:47:41 +00:00
renovate[bot] c69cb76ecd Update ktor monorepo to v3.4.3 (#2954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-22 16:06:29 +00:00
renovate[bot] 0ad0cc771f Update AGP to v9.2.0 (#2953)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 16:11:48 +00:00
renovate[bot] ca8c6883f9 Update dependency sh.calvin.reorderable:reorderable to v3.1.0 (#2952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 23:21:16 +00:00
renovate[bot] 812d9d9860 Lock file maintenance (#2950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 01:42:22 +00:00
renovate[bot] f2673ddd9a Update AGP to v9.1.1 (#2949)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 17:52:22 +00:00
renovate[bot] a6f789f446 Update dependency libarchive to v3.8.7 (#2948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 13:41:23 +00:00
renovate[bot] bba6ea4b3c Update softprops/action-gh-release action to v3 (#2947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-12 05:52:14 +00:00
renovate[bot] ae6c664f03 Update aboutlibraries to v14.0.1 (#2946)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 21:37:31 +00:00
renovate[bot]andrenovate[bot] 2b3755a09c Update dependency androidx.compose:compose-bom-alpha to v2026.04.00 (#2943)
* Update dependency androidx.compose:compose-bom-alpha to v2026.04.00

* Fix deprecation

* Update compileSdk version to 37

* Suppress deprecation warning for DnsResolver usage

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <99822064+revonateB0T@users.noreply.github.com>
2026-04-09 19:25:23 +00:00
renovate[bot] efde6e9f3d Update kotlinx.serialization to v1.11.0 (#2945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 19:02:40 +00:00
renovate[bot] cff64839bb Update moko.resources to v0.26.3 (#2944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 09:32:12 +00:00
renovate[bot] d199b58f89 Update moko.resources to v0.26.2 (#2942)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 10:36:44 +00:00
renovate[bot] 8005f9be56 Update aboutlibraries to v14.0.0 (#2941)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-06 17:31:35 +00:00
renovate[bot] c13186da78 Update dependency me.saket.telephoto:zoomable to v0.19.0 (#2940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-02 09:10:33 +00:00
renovate[bot] f407d38421 Update dependency rust-nightly to v2026-04-01 (#2939)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-01 07:57:32 +00:00
renovate[bot] 19af89d58a Lock file maintenance (#2937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-01 00:57:04 +00:00
renovate[bot] b7b2d07d60 Update dependency liblzma to v5.8.3 (#2936)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 21:40:30 +00:00
renovate[bot] e8bba2a2e5 Update ktor monorepo to v3.4.2 (#2933)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 15:40:15 +00:00
renovate[bot]andFooIbar f8e01265d4 Update dependency androidx.biometric:biometric-compose to v1.4.0-alpha06 (#2929)
* Update dependency androidx.biometric:biometric-compose to v1.4.0-alpha06

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-03-27 10:45:47 +00:00
renovate[bot] e0c5763a47 Update dependency androidx.compose:compose-bom-alpha to v2026.03.01 (#2932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 18:58:29 +00:00
renovate[bot] fe3fd59083 Update dependency androidx.browser:browser to v1.10.0 (#2931)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 21:29:06 +00:00
renovate[bot] 09693f91c7 Update dependency androidx.work:work-runtime to v2.11.2 (#2930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 21:28:34 +00:00
renovate[bot] b69500a441 Update benchmark to v1.5.0-alpha05 (#2928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 18:16:34 +00:00
renovate[bot] f344a9a0a5 Update gradle/actions action to v6 (#2926)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 21:23:20 +00:00
renovate[bot] ef62454c00 Update compose.multiplatform to v1.10.3 (#2924)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 21:00:47 +00:00
renovate[bot] fe2cd1b4f4 Update Gradle to v9.4.1 (#2923)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 13:16:10 +00:00
renovate[bot] 19b9fa3581 Update dependency com.diffplug.spotless to v8.4.0 (#2922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 18:17:05 +00:00
renovate[bot] 431a8565ee Update dependency io.arrow-kt:arrow-stack to v2.2.2.1 (#2921)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 13:05:56 +00:00
renovate[bot] 3880f757a5 Update aboutlibraries to v14.0.0-b03 (#2920)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 17:20:15 +00:00
FooIbar acfc81056b Fix error handling when getting original image URL 2026-03-18 01:10:45 +08:00
FooIbar 5f1200a9b3 Silence a warning 2026-03-17 21:24:21 +08:00
renovate[bot] f370ae64ad Update kotlin monorepo to v2.3.20 (#2917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 09:59:23 +00:00
renovate[bot] 509c818ed0 Update moko.resources to v0.26.1 (#2915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-15 08:49:11 +00:00
renovate[bot] 124d1ff638 Lock file maintenance (#2914)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-15 01:12:57 +00:00
renovate[bot] 0348eba0fd Update dependency androidx.activity:activity-compose to v1.13.0 (#2908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 19:08:40 +00:00
renovate[bot] 1d27292bf1 Update dependency androidx.datastore:datastore-preferences to v1.2.1 (#2912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 19:08:02 +00:00
renovate[bot]andFooIbar 1619f6cce6 Update dependency androidx.core:core-ktx to v1.18.0 (#2910)
* Update dependency androidx.core:core-ktx to v1.18.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>
2026-03-12 15:22:59 +00:00
renovate[bot] 489538baaa Update dependency androidx.compose:compose-bom-alpha to v2026.03.00 (#2909)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 14:02:22 +00:00
renovate[bot] 8794b7cc4d Update dependency com.squareup.okio:okio to v3.17.0 (#2911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 09:49:08 +00:00
renovate[bot] ebd114d10e Update dependency androidx.test.uiautomator:uiautomator to v2.4.0-beta02 (#2907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 00:54:01 +00:00
renovate[bot] f17dd93f27 Update benchmark to v1.5.0-alpha04 (#2905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-11 20:47:42 +00:00
renovate[bot] 899c2e14c8 Update dependency androidx.paging:paging-compose to v3.4.2 (#2906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-11 20:47:17 +00:00
renovate[bot] ae50280fb1 Update dependency me.zhanghai.compose.preference:preference to v2.2.0 (#2904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-11 09:17:34 +00:00
FooIbar a2bd809fbc Remove upstreamed patch 2026-03-11 11:02:58 +08:00
renovate[bot] 963f129bcf Update dependency libarchive to v3.8.6 (#2902)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 12:11:48 +00:00
renovate[bot] 37c0148d0b Update compose.multiplatform to v1.10.2 (#2900)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 09:55:19 +00:00
renovate[bot]andFooIbar 1fa485e31f Lock file maintenance (#2892)
* Lock file maintenance

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-03-10 09:39:45 +00:00
renovate[bot] dceea3b2ff Update Gradle to v9.4.0 (#2898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 14:18:17 +00:00
renovate[bot] bab7770857 Update dependency io.arrow-kt:arrow-stack to v2.2.2 (#2897)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 02:45:16 +00:00
renovate[bot] af9251db0c Update AGP to v9.1.0 (#2896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 16:53:31 +00:00
renovate[bot] 010b990f8d Update ktor monorepo to v3.4.1 (#2895)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:07:23 +00:00
renovate[bot] 0e61e4b00a Update spotless to v8.3.0 (#2894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 00:54:00 +00:00
renovate[bot] 66e43924bd Update dependency androidx.compose:compose-bom-alpha to v2026.02.01 (#2891)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 01:59:59 +00:00
renovate[bot] 3437bb88a2 Update actions/upload-artifact action to v7 (#2890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 18:59:16 +00:00
renovate[bot]andFooIbar f6e0733080 Update dependency io.coil-kt.coil3:coil-bom to v3.4.0 (#2888)
* Update dependency io.coil-kt.coil3:coil-bom to v3.4.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>
2026-02-25 06:55:27 +00:00
renovate[bot] add2676292 Update dependency org.jetbrains.kotlinx:kotlinx-io-core to v0.9.0 (#2886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 21:48:42 +00:00
renovate[bot] 95839dbb60 Update dependency com.google.devtools.ksp to v2.3.6 (#2885)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 22:42:45 +00:00
renovate[bot]andFooIbar 6dbc2b2e67 Update dependency rust-nightly to v2026-02-15 (#2883)
* Update dependency rust-nightly to v2026-02-15

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-02-15 08:10:35 +00:00
renovate[bot] fc89ca007e Lock file maintenance (#2882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-15 00:42:58 +00:00
renovate[bot] a75fba27cd Update AGP to v9.0.1 (#2881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 07:44:21 +00:00
renovate[bot] c2f1233360 Update dependency androidx.compose:compose-bom-alpha to v2026.02.00 (#2879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 01:49:48 +00:00
renovate[bot] 4e3cadc778 Update compose.multiplatform to v1.10.1 (#2874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 21:36:21 +00:00
renovate[bot] 73f544c14e Update dependency androidx.paging:paging-compose to v3.4.1 (#2877)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 21:35:38 +00:00
renovate[bot] 025a947bb0 Update dependency androidx.test.uiautomator:uiautomator to v2.4.0-beta01 (#2878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 21:35:18 +00:00
renovate[bot] 8927074cf1 Update benchmark to v1.5.0-alpha03 (#2875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 18:55:27 +00:00
renovate[bot] da6ffe7304 Update dependency androidx.activity:activity-compose to v1.12.4 (#2876)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-11 18:53:26 +00:00
renovate[bot]andFooIbar 956a3ac1fd Update dependency nettle to v4 (#2872)
* Update dependency nettle to v4

* Fix incompatibility with Nettle 4.x

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-02-06 15:04:04 +00:00
FooIbar f6b43cb7e3 Revert "Update download URL for Nettle"
This reverts commit dd526d33

Reason for revert: ftpmirror.gnu.org has poor availability.
2026-02-06 17:26:01 +08:00
renovate[bot] 47f3406271 Update kotlin monorepo to v2.3.10 (#2871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-05 09:03:38 +00:00
FooIbar 4ac4d3da01 Migrate to AGP KMP 2026-02-04 20:35:04 +08:00
renovate[bot] ea9ed2dc67 Update dependency com.materialkolor:material-kolor to v4.1.1 (#2869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 01:39:53 +00:00
renovate[bot] 10ad0b6449 Update moko.resources to v0.26.0 (#2868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 13:58:06 +00:00
renovate[bot] 8b02116dda Update kotlin monorepo to v2.3.10-RC2 (#2867)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 12:53:00 +00:00
FooIbar 1578da3b02 Enable Neon in Rust for Android ARMv7 targets 2026-02-01 11:04:18 +08:00
renovate[bot] c77451cae9 Lock file maintenance (#2865)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 02:29:17 +00:00
renovate[bot] 41b503918b Update dependency androidx.activity:activity-compose to v1.12.3 (#2864)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 02:03:51 +00:00
renovate[bot] 5f40d8d474 Update Gradle to v9.3.1 (#2863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 18:14:44 +00:00
FooIbar 3109a6258b Remove snapshot workaround for Compose 2026-01-29 12:55:42 +08:00
renovate[bot] cacf578615 Update dependency androidx.work:work-runtime to v2.11.1 (#2860)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 04:39:09 +00:00
renovate[bot] bb42a84b44 Update dependency androidx.paging:paging-compose to v3.4.0 (#2861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 04:38:37 +00:00
FooIbar d6519d4819 Fix hang when downloading updates from CI channel 2026-01-29 12:29:01 +08:00
renovate[bot] df5db83337 Update dependency androidx.compose:compose-bom-alpha to v2026.01.01 (#2859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 02:15:06 +00:00
renovate[bot] af4b751425 Update benchmark to v1.5.0-alpha02 (#2858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 02:14:31 +00:00
renovate[bot] 58209b0420 Update dependency com.google.devtools.ksp to v2.3.5 (#2857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 19:03:50 +00:00
renovate[bot] d4c39fd72f Update spotless to v8.2.1 (#2855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 02:23:04 +00:00
FooIbar 24505cb77c Fix missing quotes in uploader name 2026-01-27 21:23:04 +08:00
FooIbar 80225bfe6c Do not rotate determinate progress indicator (#2850)
* Do not rotate determinate progress indicator

It's hard to determine the progress with rotating wavy indicator.

* Inline progress indicator to reduce recomposition

* Fix transition between progress indicator

* Cleanup
2026-01-23 13:41:09 +08:00
renovate[bot] 14b24d6c58 Update spotless to v8.2.0 (#2851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 21:44:13 +00:00
renovate[bot] 3c10d3fe00 Update ktor monorepo to v3.4.0 (#2849)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 13:45:22 +00:00
renovate[bot] fd6a1fd1ec Update kotlinx.serialization to v1.10.0 (#2848)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 17:31:48 +00:00
FooIbar 325578b989 Update kotlin to 2.3.10-RC for compose mapping fix 2026-01-21 22:14:30 +08:00
FooIbar 705e39a50d Update compileSdk to 36.1 2026-01-21 22:08:31 +08:00
FooIbar 5ac3f4cd44 Update benchmark to v1.5.0-alpha01 2026-01-21 21:31:27 +08:00
renovate[bot]andFooIbar 80c3c697d4 Update dependency androidx.compose:compose-bom-alpha to v2026 (#2838)
* Update dependency androidx.compose:compose-bom-alpha to v2026

* Update to snapshot to fix haptic feedback

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-01-21 12:12:34 +00:00
renovate[bot] 8cc597836e Update dependency Corrosion to v0.6.1 (#2842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 21:07:53 +00:00
renovate[bot] 9a214c939b Update Gradle to v9.3.0 (#2841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 14:03:10 +00:00
FooIbar 11a3201e54 Don't upload partitioned mapping (mapping.prt)
It's for Android Studio logcat retrace.
2026-01-16 12:33:21 +08:00
FooIbar 5156bd7747 AGP 9 2026-01-16 12:33:21 +08:00
FooIbar e62738519c Revert "Update dependency androidx.compose:compose-bom-alpha to v2026 (#2835)"
This reverts commit d87de0d3e2.
2026-01-16 00:56:19 +08:00
renovate[bot]andFooIbar d87de0d3e2 Update dependency androidx.compose:compose-bom-alpha to v2026 (#2835)
* Update dependency androidx.compose:compose-bom-alpha to v2026

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-01-15 13:42:42 +00:00
renovate[bot]andFooIbar 2aa2c25cdd Update compose.multiplatform to v1.10.0 (#2833)
* Update compose.multiplatform to v1.10.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>
2026-01-15 13:23:59 +00:00
renovate[bot] 123a7cd8e3 Lock file maintenance (#2836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 02:43:33 +00:00
renovate[bot] 72a46acb46 Update dependency com.materialkolor:material-kolor to v4.1.0 (#2834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 18:43:12 +00:00
renovate[bot] 50815c6e91 Update Rust crate quick-xml to 0.39 (#2831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 21:44:14 +00:00
renovate[bot] 2c6f98b582 Update dependency me.zhanghai.compose.preference:preference to v2.1.1 (#2829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 05:17:02 +00:00
renovate[bot] 240bc5d0ae Update dependency libarchive to v3.8.5 (#2826)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 16:14:53 +00:00
FooIbar e4306a7303 Fix delay on the first notification 2026-01-02 22:54:48 +08:00
FooIbar 15179a82e3 Fix sad panda check 2026-01-01 18:33:51 +08:00
renovate[bot] 1e3b2bebb3 Update dependency rust-nightly to v2026 (#2821)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-01 09:33:48 +00:00
renovate[bot] 2c32cafd38 Update Rust crate reqwest to 0.13 (#2819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 18:10:05 +00:00
FooIbar ddd11477f7 Lower image target size to reduce moiré patterns
This partially reverts commit 63164dc70a.
2025-12-27 10:38:04 +08:00
renovate[bot] f3b2d8495a Update dependency io.github.petertrr:kotlin-multiplatform-diff to v1.3.0 (#2816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-26 21:33:10 +00:00
renovate[bot] a56dd32425 Update plugin composeCompilerReportGenerator to v1.5.0 (#2813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 08:42:56 +00:00
FooIbar 873b5e8540 Migrate to biometric-compose 2025-12-20 13:32:06 +08:00
renovate[bot] 34719b599e Update aboutlibraries to v13.2.1 (#2810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 15:40:22 +00:00
FooIbar a3a3a7b46d Simplify release process 2025-12-18 20:10:02 +08:00
renovate[bot] 1a46f75693 Update dependency androidx.webkit:webkit to v1.15.0 (#2805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 03:19:28 +00:00
renovate[bot] 90b4d7777e Update dependency androidx.compose:compose-bom-alpha to v2025.12.01 (#2803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 23:48:45 +00:00
renovate[bot] c363f4df51 Update dependency liblzma to v5.8.2 (#2804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 23:48:26 +00:00
renovate[bot] 4dc0981b34 Update dependency androidx.biometric:biometric to v1.4.0-alpha05 (#2802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 18:35:14 +00:00
renovate[bot] e2772bde04 Update dependency androidx.activity:activity-compose to v1.12.2 (#2801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 18:35:03 +00:00
FooIbar f1666d7a64 Prepare for next iteration 2025-12-17 22:12:34 +08:00
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
164 changed files with 2330 additions and 2317 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
File renamed without changes.
File renamed without changes.
@@ -13,42 +13,39 @@ on:
jobs:
baseline-profile:
runs-on: ubuntu-latest
runs-on: nekomoee
steps:
- name: Free disk space
run: rm -rf $AGENT_TOOLSDIRECTORY
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
java-version-file: .java-version
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
uses: https://github.com/Swatinem/rust-cache@v2
with:
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@v5
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
@@ -59,7 +56,7 @@ jobs:
- name: Upload reports
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: reports
path: benchmark/build/reports
+128
View File
@@ -0,0 +1,128 @@
name: CI
on:
push:
branches:
- '*'
pull_request:
workflow_dispatch:
jobs:
check:
runs-on: nekomoee
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
- name: Clippy Check
working-directory: ./app/src/main/rust
run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: true
- name: Check build-logic
run: ./gradlew :build-logic:convention:check
- name: Spotless Check
run: ./gradlew spotlessCheck
default:
runs-on: nekomoee
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
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') }}
restore-keys: cmake-default-
- name: Build
run: ./gradlew assembleDefaultRelease
- name: Upload ARM64
uses: actions/upload-artifact@v3
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@v3
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@v3
with:
name: default-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
- name: Upload mapping
uses: actions/upload-artifact@v3
with:
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease/*.txt
- name: Upload native debug symbols
uses: actions/upload-artifact@v3
with:
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
+79
View File
@@ -0,0 +1,79 @@
name: Releases
on:
push:
tags:
- "*"
jobs:
build:
runs-on: nekomoee
steps:
- name: Free disk space
run: rm -rf $AGENT_TOOLSDIRECTORY
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache/restore@v5
with:
path: app/.cxx
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
- name: Generate Baseline Profile
run: ./gradlew generateBaselineProfile
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
-Prelease
- name: Build
run: ./gradlew assembleRelease
-Prelease
- name: Rename Apks
run: |
mkdir release
mv app/build/outputs/apk/default/release/app-default-universal-release.apk release/EhViewer-${{ github.ref_name }}-default-universal.apk
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk release/EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk release/EhViewer-${{ github.ref_name }}-default-x86_64.apk
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk release/EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultRelease/mapping.txt release/EhViewer-${{ github.ref_name }}-default-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip release/EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
- name: Releases
uses: actions/forgejo-release@v2
with:
direction: upload
url: https://codeberg.org
repo: EhViewer/EhViewer
release-dir: release
body: Bump Version
-1
View File
@@ -1 +0,0 @@
*.sh eol=lf
-196
View File
@@ -1,196 +0,0 @@
name: CI
on:
push:
branches:
- '*'
pull_request:
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
- name: Clippy Check
working-directory: ./app/src/main/rust
run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-read-only: true
- name: Check build-logic
run: ./gradlew :build-logic:convention:check
- name: Spotless Check
run: ./gradlew spotlessCheck
- name: Lint Check
run: ./gradlew lintMarshmallowRelease
default:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v4
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') }}
restore-keys: cmake-default-
# https://github.com/mikepenz/AboutLibraries/issues/1225
- name: Generate aboutlibraries.json
run: ./gradlew :app:prepareLibraryDefinitionsDefaultRelease --rerun
- name: Build
run: ./gradlew assembleDefaultRelease
- name: Upload ARM64
uses: actions/upload-artifact@v4
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
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
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
with:
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
with:
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
marshmallow:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v4
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') }}
restore-keys: cmake-marshmallow-
# https://github.com/mikepenz/AboutLibraries/issues/1225
- name: Generate aboutlibraries.json
run: ./gradlew :app:prepareLibraryDefinitionsMarshmallowRelease --rerun
- name: Build
run: ./gradlew assembleMarshmallowRelease
- name: Upload ARM64
uses: actions/upload-artifact@v4
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
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
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
with:
name: marshmallow-mapping-${{ github.sha }}
path: app/build/outputs/mapping/marshmallowRelease
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
with:
name: marshmallow-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
-21
View File
@@ -1,21 +0,0 @@
name: 'Lock Threads'
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
permissions:
issues: write
concurrency:
group: lock-threads
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: '1'
process-only: 'issues'
-96
View File
@@ -1,96 +0,0 @@
name: Releases
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Free disk space
run: rm -rf $AGENT_TOOLSDIRECTORY
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache/restore@v4
with:
path: app/.cxx
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
- name: Generate Baseline Profile
run: ./gradlew generateBaselineProfile
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
# https://github.com/mikepenz/AboutLibraries/issues/1225
- name: Generate aboutlibraries.json
run: ./gradlew
:app:prepareLibraryDefinitionsDefaultRelease --rerun
:app:prepareLibraryDefinitionsMarshmallowRelease --rerun
- name: Build
run: ./gradlew assembleRelease
- name: Rename Apks
run: |
mv app/build/outputs/apk/default/release/app-default-universal-release.apk EhViewer-${{ github.ref_name }}-default-universal.apk
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-x86_64.apk
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
mv app/build/outputs/mapping/marshmallowRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
mv app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
- name: Releases
uses: softprops/action-gh-release@v2
with:
body: Bump Version
files: |
EhViewer-${{ github.ref_name }}-default-universal.apk
EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-default-x86_64.apk
EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-default-mapping.txt
EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
+1
View File
@@ -0,0 +1 @@
25
+34
View File
@@ -1,5 +1,39 @@
# 变更日志
## [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
### 改进
+12 -90
View File
@@ -1,103 +1,25 @@
<p align="right">
<strong>English</strong>
<span> | </span>
<a href="/docs/README/zh-cn.md">
简体中文
</a>
<span> | </span>
<a href="/docs/README/zh-tw.md">
正體中文
</a>
<span> | </span>
<a href="/docs/README/ja.md">
日本語
</a>
</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>
<p align="center">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="/LICENSE">
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/FooIbar/EhViewer/releases">
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
</a>
<a href="https://github.com/FooIbar/EhViewer/issues">
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
</a>
Standalone fork of
<a href="https://codeberg.org/EhViewer/EhViewer">EhViewer (codeberg)</a>
maintained at <a href="https://gitea.yrkaierx.com/yenru0/EhViewer">gitea.yrkaierx.com/yenru0/EhViewer</a>
</p>
<div align="center">
<h3>
<a href="#description">
Description
</a>
<span> | </span>
<a href="#download">
Download
</a>
<span> | </span>
<a href="#screenshot">
Screenshot
</a>
<span> | </span>
<a href="#thanks">
Thanks
</a>
<span> | </span>
<a href="#license">
License
</a>
</h3>
</div>
# Changes over upstream
# Description
- Built-in hosts, Cloudflare CDN fallback and domain fronting with SNI spoofing for censored networks (`EhHostsConfig.kt`, `EhDns.kt`, `EhSSLSocketFactory.kt`)
- Cronet engine disabled by default (bypasses the anti-DPI OkHttp stack; `enable_quic` re-enables it)
- Update check disabled by default (manual check still available)
- API request timeout raised to 30 s
- Own application id (`yenru0.yrkaierx.ehviewer`), installs side by side with upstream builds
A modern EhViewer fork dedicated to high-performance
# Upstream
with [Material Design 3](https://m3.material.io/)
and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview) Support
# Download
| Flavor | Minimum Android Version | Notes |
|-------------|-------------------------|--------------------------------|
| Default | 8.0 | Full support |
| Marshmallow | 6.0* | Limited support, no guarantees |
*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"/>
</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)
# Thanks
Here is the libraries
- [Arrow](https://arrow-kt.io/)
- [AOSP & AndroidX](https://source.android.com/)
- [Kotlin & KotlinX](https://kotlinlang.org/)
- [Material Icons](https://github.com/google/material-design-icons)
- [Ktor](https://ktor.io/)
- [Coil](https://coil-kt.github.io/coil/)
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
- [libarchive](https://www.libarchive.org/)
Synced from [EhViewer/EhViewer on codeberg](https://codeberg.org/EhViewer/EhViewer), the continuation of FooIbar's EhViewer. Feature work happens there; this fork tracks it and carries the network/resilience patches above.
# License
+12 -13
View File
@@ -49,21 +49,23 @@ android {
val repoName = providers.exec {
commandLine = "git remote get-url origin".split(' ')
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
.removeSuffix(".git")
}.standardOutput.asText.get().trim().removeSuffix(".git").removePrefix("https://codeberg.org/")
val chromeVersion = rootProject.layout.projectDirectory.file("chrome.version").asFile.readText().trim()
val snapshot = !hasProperty("release")
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
versionCode = 180063
versionName = "1.14.4"
// versionNameSuffix = "-SNAPSHOT"
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
applicationId = "yenru0.yrkaierx.ehviewer"
versionCode = 180064
versionName = if (snapshot) {
"1.15.0-SNAPSHOT"
} else {
"1.15.0"
}
buildConfigField("boolean", "SNAPSHOT", "$snapshot")
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName\"")
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)
@@ -179,10 +181,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)
+4
View File
@@ -15,6 +15,10 @@
java.nio.ByteBuffer source;
}
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
static boolean isLinkBufferComposerEnabled() return true;
}
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
+1 -5
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"
@@ -118,10 +118,6 @@
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name=".ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings" />
<activity
android:name=".shortcuts.ShortcutsActivity"
android:excludeFromRecents="true"
+33 -26
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)
@@ -29,7 +32,7 @@ option(XZ_TOOL_XZDEC OFF)
FetchContent_Declare(
liblzma
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
GIT_TAG v5.8.1
GIT_TAG v5.8.3
GIT_SHALLOW 1
)
@@ -38,23 +41,27 @@ 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 ${WSL} ./.bootstrap && ${WSL} ./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_TAG nettle_4.0_release_20260205
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'"
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
BUILD_COMMAND ${WSL} make -j4 libnettle.a
INSTALL_COMMAND ""
BUILD_IN_SOURCE 1
BUILD_BYPRODUCTS <SOURCE_DIR>/libnettle.a
@@ -96,7 +103,7 @@ set(LIBARCHIVE_PATCH
FetchContent_Declare(
libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
GIT_TAG v3.8.1
GIT_TAG v3.8.7
GIT_SHALLOW 1
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
)
@@ -133,8 +140,8 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ../../../../rust-
unset(Rust_TOOLCHAIN CACHE)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.5.2
GIT_REPOSITORY https://github.com/FooIbar/corrosion.git
GIT_TAG v0.6.1
GIT_SHALLOW 1
)
@@ -145,7 +152,7 @@ corrosion_import_crate(
FEATURES $<IF:$<VERSION_GREATER_EQUAL:${ANDROID_NATIVE_API_LEVEL},26>,android-26,android>
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>)
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 -1
View File
@@ -24,7 +24,7 @@ Java_com_hippo_ehviewer_jni_HashKt_sha1(JNIEnv *env, jclass clazz, jint fd) {
}
byte digest[SHA1_DIGEST_SIZE];
sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest);
sha1_digest(&ctx, digest);
byte byte;
char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
@@ -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)
}
@@ -39,12 +39,12 @@ 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.isAtLeastSExtension7
import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withUIContext
@@ -65,8 +65,8 @@ 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
import com.hippo.ehviewer.ui.tools.dataStateFlow
import com.hippo.ehviewer.util.AppConfig
@@ -83,7 +83,6 @@ 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
@@ -109,7 +108,6 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
}
}
}
lifecycle.addObserver(lockObserver)
CrashHandler.install()
super.onCreate()
System.loadLibrary("ehviewer")
@@ -124,6 +122,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
}
launchIO {
EhTagDatabase.launchUpdate()
@Suppress("UNUSED_EXPRESSION")
launch { EhDB }
launch { dataStateFlow.value }
launch { OSUtils.totalMemory }
@@ -146,6 +145,8 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
if (BuildConfig.DEBUG) {
StrictMode.enableDefaults()
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.SourceInformation)
} else {
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.Auto)
}
}
@@ -195,7 +196,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
add(GifDecoder.Factory())
}
}
diskCache { imageCache }
diskCache { thumbCache }
crossfade(300)
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
if (drawable != null) error(drawable.asImage(true))
@@ -218,7 +219,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
private set
val ktorClient by lazy {
if (isAtLeastSExtension7 && Settings.enableCronet.value) {
if (isCronetAvailable && Settings.enableQuic.value) {
HttpClient(Cronet) {
engine { configureClient(Settings.enableQuic.value) }
configureCommon()
@@ -237,10 +238,10 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
}
}
val imageCache by lazy {
val thumbCache by lazy {
diskCache {
directory(appCtx.cacheDir.toOkioPath() / "image_cache")
maxSizeBytes(Settings.readCacheSize.value.coerceIn(320, 5120).toLong() * 1024 * 1024)
directory(appCtx.cacheDir.toOkioPath() / "thumb")
maxSizeBytes(80L * 1024 * 1024)
}
}
+16 -12
View File
@@ -15,13 +15,14 @@
*/
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.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
@@ -32,13 +33,13 @@ 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 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"
@@ -277,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())
@@ -0,0 +1,56 @@
package com.hippo.ehviewer
import com.hippo.ehviewer.client.hosts
import com.hippo.ehviewer.client.hostsDsl
val builtInHosts = hostsDsl {
hosts("exhentai.org") {
"178.175.128.251" blockedInCN false
"178.175.128.252" blockedInCN false
"178.175.128.253" blockedInCN false
"178.175.128.254" blockedInCN false
"178.175.129.251" blockedInCN false
"178.175.129.252" blockedInCN false
"178.175.129.253" blockedInCN false
"178.175.129.254" blockedInCN false
"178.175.132.19" blockedInCN false
"178.175.132.20" blockedInCN false
"178.175.132.21" blockedInCN false
"178.175.132.22" blockedInCN false
}
hosts("api.github.com") {
"140.82.114.5" blockedInCN false
"140.82.114.6" blockedInCN false
"140.82.116.5" blockedInCN false
"140.82.116.6" blockedInCN false
"140.82.121.5" blockedInCN true
"140.82.121.6" blockedInCN true
"20.200.245.245" blockedInCN false
"20.201.28.148" blockedInCN false
"20.205.243.168" blockedInCN false
"20.26.156.210" blockedInCN false
"20.27.177.116" blockedInCN false
"20.87.245.6" blockedInCN false
"4.237.22.34" blockedInCN false
}
hosts("github.com") {
"140.82.113.4" blockedInCN false
"140.82.114.3" blockedInCN false
"140.82.116.4" blockedInCN false
"140.82.121.3" blockedInCN true
"140.82.121.4" blockedInCN true
"20.200.245.247" blockedInCN false
"20.201.28.151" blockedInCN false
"20.205.243.166" blockedInCN false
"20.26.156.215" blockedInCN false
"20.27.177.113" blockedInCN false
"20.87.245.0" blockedInCN false
"4.237.22.38" blockedInCN false
}
hosts("raw.githubusercontent.com") {
"185.199.108.133" blockedInCN false
"185.199.109.133" blockedInCN false
"185.199.110.133" blockedInCN false
"185.199.111.133" blockedInCN false
}
}
@@ -9,14 +9,13 @@ 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
import com.hippo.ehviewer.util.AppConfig
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import java.util.Locale
@@ -82,6 +81,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)
@@ -111,7 +111,7 @@ object Settings : DataStorePreferences(null) {
val saveCrashLog = boolPref("save_crash_log", true)
val readCacheSize = intPref("read_cache_size_2", 640)
val enableCronet = boolPref("enable_cronet", true)
val enableQuic = boolPref("enable_quic", true)
val enableQuic = boolPref("enable_quic", false)
val hardwareBitmapThreshold = intPref("hardware_bitmap_threshold", 16384)
val preloadThumbAggressively = boolPref("preload_thumb_aggressively", false)
val animateItems = boolPref("animate_items", true)
@@ -119,8 +119,8 @@ object Settings : DataStorePreferences(null) {
// About
val backupBeforeUpdate = boolPref("backup_before_update", false)
val useCIUpdateChannel = boolPref("ci_update_channel", AppConfig.isSnapshot)
val updateIntervalDays = intPref("update_interval_days", 7)
val useCIUpdateChannel = boolPref("ci_update_channel", BuildConfig.SNAPSHOT)
val updateIntervalDays = intPref("update_interval_days", 0)
// Misc
val languageFilter = intPref("language_filter", -1)
@@ -194,6 +194,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)
}
}
}
@@ -0,0 +1,67 @@
package com.hippo.ehviewer.client
import com.ehviewer.core.util.isAtLeastQ
import com.hippo.ehviewer.builtInHosts
import java.net.Inet4Address
import java.net.InetAddress
import okhttp3.AsyncDns
import okhttp3.Dns
import okhttp3.android.AndroidAsyncDns
private typealias HostsMap = MutableMap<String, List<InetAddress>>
const val CFSUFFIX = ".cdn.cloudflare.net"
val cloudflaredDomains = listOf(
"e-hentai.org",
"api.e-hentai.org",
"upload.e-hentai.org",
"forums.e-hentai.org",
"exhentai.org",
"s.exhentai.org",
"testingcf.jsdelivr.net",
)
val dFEnabledDomains = listOf(
"github.com", "api.github.com",
"ehgt.org", "gt0.ehgt.org", "gt1.ehgt.org", "gt2.ehgt.org", "gt3.ehgt.org", "ul.ehgt.org",
"e-hentai.org", "api.e-hentai.org", "forums.e-hentai.org", "repo.e-hentai.org", "upload.e-hentai.org",
"exhentai.org", "s.exhentai.org",
)
val dohSkipDomains = listOf(
"www.recaptcha.net",
"www.gstatic.cn",
"www.gstatic.com",
)
fun hostsDsl(builder: HostsMap.() -> Unit): HostsMap = mutableMapOf<String, List<InetAddress>>().apply(builder)
fun interface HostMapBuilder {
infix fun String.blockedInCN(boolean: Boolean)
}
fun HostsMap.hosts(vararg hosts: String, builder: HostMapBuilder.() -> Unit) = apply {
hosts.forEach { host ->
fun String.toInetAddress() = InetAddress.getByName(this).let { InetAddress.getByAddress(host, it.address) }
mutableListOf<InetAddress>().apply {
HostMapBuilder { if (!it) add(toInetAddress()) }.apply(builder)
put(host, this)
}
}
}
val systemDns = if (isAtLeastQ) AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6) else Dns.SYSTEM
object EhDns : Dns {
override fun lookup(hostname: String): List<InetAddress> = when {
(hostname in "exhentai.org") ->
builtInHosts[hostname] ?: systemDns.lookup("s.exhentai.org$CFSUFFIX")
(hostname in cloudflaredDomains) ->
builtInHosts[hostname] ?: systemDns.lookup("$hostname$CFSUFFIX")
(hostname in dohSkipDomains) ->
systemDns.lookup(hostname)
else ->
builtInHosts[hostname] ?: systemDns.lookup(hostname)
}.shuffled()
}
@@ -24,6 +24,7 @@ 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
@@ -31,6 +32,7 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder
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
@@ -69,6 +71,7 @@ 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
@@ -79,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
@@ -119,7 +124,7 @@ private fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuff
{ !it.hasRemaining() },
)
if (empty) {
val message = if (EhUtils.isExHentai) {
val message = if (response.request.url.host == EhUrl.DOMAIN_EX) {
"Sad Panda\n(without panda)"
} else {
appCtx.getString(R.string.error_ip_banned)
@@ -127,27 +132,30 @@ private fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuff
throw IpBannedException(message)
}
// 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.isNotEmpty() -> {
when (message[0]) {
'5' -> throw IpBannedException(message.substring(1))
'6' -> throw EhException(message.substring(1))
}
}
}
}
// Parse error from rust
val error = body.getOrNull()?.runCatching {
val array = moveToByteArray()
rewind()
Cbor.decodeFromByteArray<EhError>(array)
}?.getOrNull()
// 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 IpBannedException(it) }
if (Settings.saveParseErrorBody.value) body.saveParseError(e)
@@ -186,7 +194,12 @@ private suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline
object EhEngine {
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
response.headers["Location"]?.takeUnless { Url(it).isLogin } ?: throw InsufficientGpException()
val location = response.headers["Location"] ?: run {
val body = response.bodyAsUtf8Text()
rethrowExactly(response, body.left(), InsufficientGpException())
}
if (Url(location).isLogin) throw InsufficientGpException()
location
}
suspend fun getPTokenListFromMpv(gid: Long, token: String) = ehRequest(EhUrl.getGalleryMultiPageViewerUrl(gid, token), EhUrl.referer)
@@ -0,0 +1,41 @@
package com.hippo.ehviewer.client
import android.util.Log
import com.hippo.ehviewer.Settings
import java.net.InetAddress
import java.net.Socket
import java.security.KeyStore
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import okhttp3.OkHttpClient
const val EXCEPTIONAL_DOMAIN = "hath.network"
private val sslSocketFactory: SSLSocketFactory = SSLContext.getInstance("TLS").apply {
init(null, null, null)
}.socketFactory
object EhSSLSocketFactory : SSLSocketFactory() {
override fun getDefaultCipherSuites(): Array<String> = sslSocketFactory.defaultCipherSuites
override fun getSupportedCipherSuites(): Array<String> = sslSocketFactory.supportedCipherSuites
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket = sslSocketFactory.createSocket(s, resolveHost(s, host), port, autoClose) as SSLSocket
override fun createSocket(host: String, port: Int): Socket = sslSocketFactory.createSocket(host, port)
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket = sslSocketFactory.createSocket(host, port, localHost, localPort)
override fun createSocket(host: InetAddress, port: Int): Socket = sslSocketFactory.createSocket(host, port)
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket = sslSocketFactory.createSocket(address, port, localAddress, localPort)
private fun resolveHost(socket: Socket, host: String): String = socket.inetAddress.hostAddress.takeIf {
host in dFEnabledDomains || EXCEPTIONAL_DOMAIN in host
} ?: host
}
fun OkHttpClient.Builder.install(sslSocketFactory: SSLSocketFactory) = apply {
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())!!
factory.init(null as KeyStore?)
val manager = factory.trustManagers!!
val trustManager = manager.filterIsInstance<X509TrustManager>().first()
sslSocketFactory(sslSocketFactory, trustManager)
}
@@ -53,12 +53,7 @@ import splitties.init.appCtx
object EhTagDatabase : CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
private const val NAMESPACE_PREFIX = "n"
private val PREFIXES = arrayOf(
"-",
"~",
"tag:",
"weak:",
)
private val PREFIXES = ["-", "~", "tag:", "weak:"]
private lateinit var tagGroups: Map<String, Map<String, String>>
private val updateLock = Mutex()
var initialized by mutableStateOf(false)
@@ -107,14 +107,14 @@ object EhUrl {
else -> URL_IMAGE_SEARCH_E
}
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) = ehUrl(listOf("g", "$gid", token)) {
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) = ehUrl(["g", "$gid", token]) {
if (index != 0) addQueryParameter("p", "$index")
if (allComment) addQueryParameter("hc", "1")
}.buildString()
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", "$gid", token)).buildString()
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(["mpv", "$gid", token]).buildString()
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(listOf("s", pToken, "$gid-${index + 1}")) {
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(["s", pToken, "$gid-${index + 1}"]) {
addQueryParameterIfNotBlank("nl", nl)
}.buildString()
@@ -143,7 +143,7 @@ object EhUrl {
}
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
path = if (path.isNullOrBlank()) emptyList() else listOf(path),
path = if (path.isNullOrBlank()) [] else [path],
host = host,
parameters = Parameters.build(builder),
)
@@ -20,15 +20,19 @@ 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.parser.Archive
@@ -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)
@@ -35,7 +35,7 @@ data class ListUrlBuilder(
private var jumpTo: String? = null,
private var range: Int = 0,
var category: Int = EhUtils.NONE,
private var mKeyword: String? = null,
var keyword: String? = null,
var hash: String? = null,
var language: Int = -1,
var advanceSearch: Int = -1,
@@ -69,16 +69,10 @@ data class ListUrlBuilder(
jumpTo = date
}
var keyword: String?
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
set(keyword) {
mKeyword = keyword
}
constructor(q: QuickSearch) : this(
mode = q.mode,
category = q.category,
mKeyword = q.keyword,
keyword = q.keyword,
advanceSearch = q.advanceSearch,
minRating = q.minRating,
pageFrom = q.pageFrom,
@@ -90,7 +84,7 @@ data class ListUrlBuilder(
name = name,
mode = mode,
category = category,
keyword = mKeyword,
keyword = keyword,
advanceSearch = advanceSearch,
minRating = minRating,
pageFrom = pageFrom,
@@ -107,7 +101,7 @@ data class ListUrlBuilder(
if (q.category != this.category) {
return false
}
if (q.keyword != mKeyword) {
if (q.keyword != keyword) {
return false
}
if (q.advanceSearch != advanceSearch) {
@@ -129,7 +123,7 @@ data class ListUrlBuilder(
jumpTo = params["seek"],
range = params["range"]?.toIntOrNull() ?: 0,
category = params["f_cats"]?.toIntOrNull()?.let(EhUtils::invCategory) ?: EhUtils.NONE,
mKeyword = params["f_search"],
keyword = params["f_search"],
) {
if (params["advsearch"] == "1") {
advanceSearch = 0
@@ -160,7 +154,7 @@ data class ListUrlBuilder(
if (category > 0) {
addQueryParameter("f_cats", "${EhUtils.invCategory(category)}")
}
val query = mKeyword?.let { keyword ->
val query = keyword?.let { keyword ->
if (language == -1 || "gid:" in keyword || "l:" in keyword || "language:" in keyword) {
keyword
} else {
@@ -212,26 +206,23 @@ data class ListUrlBuilder(
}
}
}.buildString()
MODE_UPLOADER, MODE_TAG -> {
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
ehUrl(listOf(path, requireNotNull(mKeyword))) {
ehUrl([path, requireNotNull(keyword)]) {
addQueryParameterIfNotBlank("prev", prev)
addQueryParameterIfNotBlank("next", next)
addQueryParameterIfNotBlank("seek", jumpTo)
addQueryParameterIfNotBlank("range", range.takeIf { it > 0 }?.toString())
}.buildString()
}
MODE_WHATS_HOT -> EhUrl.popularUrl
MODE_IMAGE_SEARCH -> ehUrl {
addQueryParameter("f_shash", requireNotNull(hash))
}.buildString()
MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) {
addQueryParameter("tl", requireNotNull(mKeyword))
addQueryParameter("tl", requireNotNull(keyword))
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
}
@@ -13,14 +13,14 @@ object GalleryListParser {
throw ParseException("Can't parse gallery list", it)
}
val emptyResult = GalleryListResult(null, null, arrayListOf())
val emptyResult = GalleryListResult(null, null, [])
}
@Serializable
data class GalleryListResult(
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
val galleryInfoList: MutableList<BaseGalleryInfo>,
)
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
@@ -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,20 +42,18 @@ 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")) {
ListUrlBuilder(
mode = ListUrlBuilder.MODE_TOPLIST,
jumpTo = url.parameters["p"],
mKeyword = tl,
keyword = tl,
)
} else {
null
}
}
else -> {
val category = EhUtils.getCategory(head)
if (category != EhUtils.UNKNOWN) {
@@ -12,7 +12,7 @@ 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.EhApplication.Companion.thumbCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.download.downloadLocation
@@ -53,7 +53,7 @@ object DownloadThumbInterceptor : Interceptor {
// Accessing the recreated file immediately after deleting it throws
// FileNotFoundException, so we just overwrite the existing file.
val key = requireNotNull(chain.request.memoryCacheKey)
imageCache.read(key) {
thumbCache.read(key) {
data sendTo thumb
}
if (thumb != v1Thumb) v1Thumb.delete()
@@ -148,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
@@ -682,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 ->
@@ -697,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)
@@ -52,8 +52,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class DownloadService :
@@ -113,14 +111,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()
@@ -377,8 +373,8 @@ class DownloadService :
}
suspend fun run() {
channel.receiveAsFlow().sample(DELAY).collect {
when (it) {
while (true) {
when (channel.receive()) {
Ops.Notify -> {
if (ActivityCompat.checkSelfPermission(
service,
@@ -388,10 +384,10 @@ class DownloadService :
notifyManager.notify(id, builder.build())
}
}
Ops.Cancel -> notifyManager.cancel(id)
Ops.StartForeground -> service.startForeground(id, builder.build())
}
delay(DELAY)
}
}
@@ -48,7 +48,7 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
return SortMode(field, order, groupByDownloadLabel)
}
val Default = SortMode(Field.TIME, Order.DESC)
val All = listOf(
val All = [
Default,
SortMode(Field.TIME, Order.ASC),
SortMode(Field.GID, Order.DESC),
@@ -57,6 +57,6 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
SortMode(Field.TITLE, Order.DESC),
SortMode(Field.PAGES, Order.ASC),
SortMode(Field.PAGES, Order.DESC),
)
]
}
}
@@ -17,8 +17,10 @@ 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
@@ -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)
@@ -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,7 @@ 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
@@ -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?
@@ -32,8 +32,9 @@ import coil3.request.CachePolicy
import coil3.request.ErrorResult
import coil3.request.SuccessResult
import coil3.request.allowHardware
import coil3.size.Dimension
import coil3.request.maxBitmapSize
import coil3.size.Precision
import coil3.size.Scale
import coil3.size.Size
import coil3.size.SizeResolver
import com.ehviewer.core.files.openFileDescriptor
@@ -90,8 +91,10 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
}
companion object {
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
private val sizeResolver = SizeResolver(Size(targetWidth, Dimension.Undefined))
private val sizeResolver = with(appCtx.resources.displayMetrics) {
val targetSize = minOf(widthPixels, heightPixels) * 4 / 3
SizeResolver(Size(targetSize, targetSize))
}
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
val request = with(appCtx) {
@@ -99,7 +102,9 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
onLeft { data(it.source) }
onRight { data(it.source.toUri()) }
size(sizeResolver)
scale(Scale.FILL)
precision(Precision.INEXACT)
maxBitmapSize(Size.ORIGINAL)
allowHardware(false)
hardwareThreshold(Settings.hardwareBitmapThreshold.value)
maybeCropBorder(Settings.cropBorder.value)
@@ -130,7 +135,6 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
}
src.right().decodeCoil(checkExtraneousAds)
}
is ByteBufferSource -> {
if (isAtLeastP && !isAtLeastU) {
rewriteGifSource(src.source)
@@ -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"
@@ -26,7 +29,7 @@ fun <T : HttpClientEngineConfig> HttpClientConfig<T>.configureCommon(redirect: B
}
install(HttpTimeout) {
reset()
requestTimeoutMillis = 10_000
requestTimeoutMillis = 30_000
}
install(UserAgent)
defaultRequest {
@@ -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
@@ -35,20 +32,21 @@ import java.nio.ByteBuffer
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.job
import kotlinx.coroutines.suspendCancellableCoroutine
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
// Limit thread to 1 since we are async & non-blocking
override val dispatcher = Dispatchers.Default.limitedParallelism(1)
// TODO: Investigate https://github.com/FooIbar/EhViewer/issues/2854
// override val dispatcher = Dispatchers.Default.limitedParallelism(1)
override val supportedCapabilities = setOf(HttpTimeoutCapability)
private val executor = dispatcher.asExecutor()
private val executor = dispatcher.limitedParallelism(1).asExecutor()
private val pool = DirectByteBufferPool(32)
private val client by lazy {
with(config) { HttpEngine.Builder(context).apply(config).build() }
@@ -95,7 +93,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
}
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
chunkChan.trySend(byteBuffer).getOrThrow()
chunkChan.trySendBlocking(byteBuffer).getOrThrow()
}
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
@@ -103,15 +101,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,14 +1,18 @@
package com.hippo.ehviewer.ktor
import com.ehviewer.core.util.isAtLeastQ
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.install
import io.ktor.client.engine.okhttp.OkHttpConfig
import okhttp3.AsyncDns
import okhttp3.android.AndroidAsyncDns
import java.net.Proxy
fun OkHttpConfig.configureClient() {
config {
if (isAtLeastQ) {
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
dns(EhDns)
proxy(Proxy.NO_PROXY)
if (!Settings.enableQuic.value) {
install(EhSSLSocketFactory)
}
}
}
@@ -43,7 +43,7 @@ private val xml = XML {
}
xmlDeclMode = XmlDeclMode.Charset
xmlVersion = XmlVersion.XML10
indent = 2
setIndent(2)
}
fun GalleryInfo.getComicInfo(): ComicInfo {
@@ -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
@@ -33,7 +33,6 @@ 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
@@ -47,6 +46,7 @@ import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.download.tempDownloadDir
import com.hippo.ehviewer.image.PathSource
import com.hippo.ehviewer.jni.archiveFdBatch
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.copyTo
import com.hippo.ehviewer.util.sha1
@@ -56,8 +56,9 @@ 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
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
class SpiderDen(val info: GalleryInfo) {
private val gid = info.gid
@@ -129,11 +130,9 @@ class SpiderDen(val info: GalleryInfo) {
SpiderQueen.MODE_READ -> {
containInCache(index) || containInDownloadDir(index)
}
SpiderQueen.MODE_DOWNLOAD -> {
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
}
else -> {
false
}
@@ -154,6 +153,10 @@ class SpiderDen(val info: GalleryInfo) {
removeTempFile(index)
}
fun clearCache() {
repeat(info.pages, ::removeFromCache)
}
private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) {
val name = perFilename(index, extension)
read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } }
@@ -270,22 +273,18 @@ class SpiderDen(val info: GalleryInfo) {
}
}
suspend fun archive() {
if (saveAsCbz) {
downloadDir?.run {
suspend fun archive() = saveAsCbz && downloadDir?.run {
resolve(archiveName).let { file ->
runCatching {
archiveTo(file)
}.onFailure {
file.delete()
if (it is CancellationException) throw it
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
@@ -335,7 +334,7 @@ class SpiderDen(val info: GalleryInfo) {
downloadDir?.run {
resolve(COMIC_INFO_FILE).also {
if (info !is GalleryDetail && fetchMetadata) {
EhEngine.fillGalleryListByApi(listOf(info))
EhEngine.fillGalleryListByApi([info])
}
info.getComicInfo().apply {
writeComicInfo(this, it)
@@ -349,6 +348,12 @@ class SpiderDen(val info: GalleryInfo) {
}
}
private val sCache by lazy {
diskCache {
directory(appCtx.cacheDir.toOkioPath() / "image_cache")
maxSizeBytes(Settings.readCacheSize.value.coerceIn(320, 5120).toLong() * 1024 * 1024)
}
}
private const val TEMP_SUFFIX = ".tmp"
private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
private val FileHashRegex = Regex("/h/([0-9a-f]{40})")
@@ -134,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,
)
}
}
@@ -213,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()
@@ -400,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)
}
}
}
@@ -418,7 +425,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
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) {}
}
@@ -587,6 +594,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
previousPToken,
)
}.getOrElse {
if (it is FatalException) throw it
forceHtml = true
return@repeat
}.let {
@@ -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,7 +254,6 @@ private suspend fun doModifyFavorites(
favoriteNote = note
true
}
else -> throw EhException("Invalid favorite slot!")
}
FavouriteStatusRouter.notify(galleryInfo)
@@ -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)
}
}
@@ -400,7 +394,7 @@ suspend fun showMoveDownloadLabel(info: GalleryInfo) {
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
val label = if (selected == 0) null else labels[selected]
DownloadManager.changeLabel(listOf(downloadInfo), label)
DownloadManager.changeLabel([downloadInfo], label)
}
context(_: DialogState)
@@ -1,51 +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.ui
import android.os.Bundle
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 eu.kanade.tachiyomi.util.view.setSecureScreen
abstract class EhActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// https://issuetracker.google.com/204791558
// Fix system bars insets still exist in fullscreen mode on API < 30
@Suppress("DEPRECATION")
if (!isAtLeastR) {
with(window.decorView) {
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
}
}
}
override fun onResume() {
interceptSecurityOrReturn()
super.onResume()
window.setSecureScreen(Settings.enabledSecurity.value)
}
override fun finish() {
super.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}
}
@@ -24,7 +24,10 @@ import android.content.pm.verify.domain.DomainVerificationManager
import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.animateDpAsState
@@ -63,7 +66,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberDrawerState2
import androidx.compose.runtime.Composable
@@ -98,6 +101,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import androidx.compose.ui.zIndex
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.LifecycleResumeEffect
@@ -113,6 +117,7 @@ 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.isAtLeastR
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhApplication.Companion.initialized
@@ -153,6 +158,7 @@ import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.spec.Direction
import com.ramcosta.composedestinations.utils.currentDestinationAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import eu.kanade.tachiyomi.util.view.setSecureScreen
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
@@ -175,7 +181,7 @@ private val navItems = arrayOf<Triple<Direction, Int, ImageVector>>(
Triple(SettingsScreenDestination, R.string.settings, Icons.Default.Settings),
)
class MainActivity : EhActivity() {
class MainActivity : AppCompatActivity() {
private var shareUrl: String? = null
@@ -203,6 +209,15 @@ class MainActivity : EhActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen().setKeepOnScreenCondition { !initialized }
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// https://issuetracker.google.com/204791558
// Fix system bars insets still exist in fullscreen mode on API < 30
@Suppress("DEPRECATION")
if (!isAtLeastR) {
with(window.decorView) {
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
}
}
setMD3Content {
val configuration = LocalConfiguration.current
val navDrawerState = rememberDrawerState(DrawerValue.Closed)
@@ -253,6 +268,15 @@ class MainActivity : EhActivity() {
}
}
LaunchedEffect(Unit) {
Settings.enabledSecurity.valueFlow().collect {
window.setSecureScreen(it)
}
}
if (isAuthenticationSupported()) {
SecurityScreen(onError = { moveTaskToBack(true) }, modifier = Modifier.zIndex(1f))
}
val cannotParse = stringResource(R.string.error_cannot_parse_the_url)
LaunchedEffect(Unit) {
intentFlow.collect { intent ->
@@ -276,7 +300,7 @@ class MainActivity : EhActivity() {
if ("text/plain" == type) {
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
if (keyword != null && !navWithUrl(keyword)) {
navigate(ListUrlBuilder(mKeyword = keyword).asDst())
navigate(ListUrlBuilder(keyword = keyword).asDst())
}
} else if (type != null && type.startsWith("image/")) {
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
@@ -356,7 +380,7 @@ class MainActivity : EhActivity() {
var snackbarFabPadding by remember { mutableStateOf(0.dp) }
val drawerEnabled = drawerHandle.isNotEmpty()
val density = LocalDensity.current
val adaptiveInfo = currentWindowAdaptiveInfo()
val adaptiveInfo = currentWindowAdaptiveInfoV2()
val needSignIn by Settings.needSignIn.collectAsState()
val launchPage by Settings.launchPage.collectAsState()
CompositionLocalProvider(
@@ -1,102 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.AuthenticationRequest.Biometric
import androidx.biometric.AuthenticationRequest.Companion.biometricRequest
import androidx.biometric.AuthenticationResult
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.registerForAuthenticationResult
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.Settings
context(ctx: Context)
fun isAuthenticationSupported(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
return BiometricManager.from(ctx).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
class SecurityActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
if (isAuthenticationSupported()) {
startAuthentication()
} else {
onSuccess()
}
}
private val authLauncher = registerForAuthenticationResult { result ->
when (result) {
is AuthenticationResult.Success -> onSuccess()
is AuthenticationResult.Error -> onError()
}
}
@Synchronized
private fun startAuthentication() {
if (isAuthenticating) return
isAuthenticating = true
val request = biometricRequest(
title = getString(R.string.settings_privacy_require_unlock),
authFallback = Biometric.Fallback.DeviceCredential,
) {}
authLauncher.launch(request)
}
private fun onSuccess() {
isAuthenticating = false
locked = false
finish()
}
private fun onError() {
moveTaskToBack(true)
isAuthenticating = false
}
}
private var isAuthenticating = false
var locked = true
var lockedLastLeaveTime: Long = 0
val lockObserver = object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
if (!locked) {
lockedLastLeaveTime = System.currentTimeMillis() / 1000
}
locked = true
}
}
fun Context.interceptSecurityOrReturn() {
val lockedResumeTime = System.currentTimeMillis() / 1000
val lockedDelayTime = lockedResumeTime - lockedLastLeaveTime
if (lockedDelayTime < Settings.securityDelay.value * 60) {
locked = false
} else if (Settings.security.value && isAuthenticationSupported() && locked) {
startActivity(Intent(this, SecurityActivity::class.java))
}
}
@@ -0,0 +1,97 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui
import android.content.Context
import androidx.biometric.AuthenticationRequest.Biometric
import androidx.biometric.AuthenticationRequest.Companion.biometricRequest
import androidx.biometric.AuthenticationResult
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.compose.rememberAuthenticationLauncher
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.LifecycleResumeEffect
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.collectAsState
import kotlin.time.Duration.Companion.minutes
import kotlin.time.TimeSource
context(ctx: Context)
fun isAuthenticationSupported(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
return BiometricManager.from(ctx).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
@Composable
fun SecurityScreen(onError: () -> Unit, modifier: Modifier) {
val security by Settings.security.collectAsState()
var locked by remember { mutableStateOf(security) }
var isAuthenticating by remember { mutableStateOf(false) }
val title = stringResource(R.string.settings_privacy_require_unlock)
val launcher = rememberAuthenticationLauncher { result ->
isAuthenticating = false
when (result) {
is AuthenticationResult.Success -> locked = false
else -> onError()
}
}
@Synchronized
fun startAuthentication() {
if (isAuthenticating) return
isAuthenticating = true
val request = biometricRequest(title, Biometric.Fallback.DeviceCredential) {}
launcher.launch(request)
}
val securityDelay by Settings.securityDelay.collectAsState { it.minutes }
var leaveTime by remember { mutableStateOf(TimeSource.Monotonic.markNow()) }
LifecycleResumeEffect(Unit) {
if (security && leaveTime.elapsedNow() > securityDelay) {
locked = true
}
onPauseOrDispose {
if (!locked) {
leaveTime = TimeSource.Monotonic.markNow()
}
}
}
if (locked) {
val windowInfo = LocalWindowInfo.current
LaunchedEffect(windowInfo) {
snapshotFlow { windowInfo.isWindowFocused }.collect {
if (it) startAuthentication()
}
}
Surface(modifier = modifier.fillMaxSize()) {}
}
}
@@ -7,6 +7,7 @@ import coil3.decode.DecodeUtils
import coil3.imageLoader
import coil3.request.crossfade
import coil3.size.Scale
import coil3.size.Size
import com.ehviewer.core.util.toIntOrDefault
import com.hippo.ehviewer.ktbuilder.imageRequest
import kotlin.math.roundToInt
@@ -21,7 +22,7 @@ class CoilImageGetter(private val onSuccess: () -> Unit) : Html.ImageGetter {
val srcHeight = groupValues[2].toInt()
val dstWidth = groupValues[3].toIntOrDefault(200)
val dstHeight = dstWidth / 2 * 3
val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT)
val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT, Size.ORIGINAL)
setBounds(0, 0, (srcWidth * multiplier).roundToInt(), (srcHeight * multiplier).roundToInt())
} ?: setBounds(0, 0, 200, 300)
with(appCtx) {
@@ -1,9 +1,9 @@
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 kotlinx.coroutines.CoroutineScope
@@ -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
@@ -31,6 +31,7 @@ fun AnimatedVisibilityScope.WebViewSignInScreen(navigator: DestinationsNavigator
Settings.desktopSite.value = false
}
if (EhCookieStore.hasSignedIn()) {
EhCookieStore.flush()
postLogin()
state.webView?.destroy()
bgWork { awaitCancellation() }
@@ -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,7 +37,6 @@ 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
@@ -45,7 +46,7 @@ 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.download.DownloadManager
import com.hippo.ehviewer.util.FileUtils
@@ -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)
}
}
}
@@ -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,7 +33,6 @@ 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
@@ -46,7 +45,7 @@ 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.download.DownloadManager
import com.hippo.ehviewer.util.FavouriteStatusRouter
@@ -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
@@ -196,7 +197,6 @@ fun GalleryList(
}
}
}
is LoadState.Error -> {
Surface {
ErrorTip(
@@ -205,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))
@@ -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
}
@@ -47,7 +47,7 @@ import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitResult
import kotlinx.coroutines.CoroutineScope
private val categoryTable = arrayOf(
private val categoryTable = [
EhUtils.DOUJINSHI to R.string.doujinshi,
EhUtils.MANGA to R.string.manga,
EhUtils.ARTIST_CG to R.string.artist_cg,
@@ -58,7 +58,7 @@ private val categoryTable = arrayOf(
EhUtils.COSPLAY to R.string.cosplay,
EhUtils.ASIAN_PORN to R.string.asian_porn,
EhUtils.MISC to R.string.misc,
)
]
@Composable
context(_: DialogState, _: CoroutineScope, _: Context)
@@ -193,9 +193,6 @@ fun SearchFilter(
}
if (error != null) {
ListItem(
headlineContent = {
Text(text = error, style = MaterialTheme.typography.bodySmall)
},
leadingContent = {
Icon(imageVector = Icons.Default.Info, contentDescription = null)
},
@@ -204,6 +201,9 @@ fun SearchFilter(
headlineColor = MaterialTheme.colorScheme.error,
leadingIconColor = MaterialTheme.colorScheme.error,
),
content = {
Text(text = error, style = MaterialTheme.typography.bodySmall)
},
)
}
}
@@ -10,10 +10,6 @@ import androidx.compose.material.icons.filled.Brightness5
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
@@ -23,6 +19,8 @@ import androidx.core.graphics.green
import androidx.core.graphics.red
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.component.RollingNumber
import com.ehviewer.core.ui.component.rememberIntPrefSliderState
import com.ehviewer.core.ui.component.rememberIntSliderState
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
@@ -34,12 +32,11 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
field = customBrightness,
)
AnimatedVisibility(visible = customBrightness.value) {
val brightness = Settings.customBrightnessValue.asMutableState()
val brightness = rememberIntPrefSliderState(Settings.customBrightnessValue, -75..100)
SliderChoice(
startSlot = { Icon(imageVector = Icons.Default.Brightness5, contentDescription = null) },
endSlot = { RollingNumber(number = brightness.value, length = 3) },
range = -75..100,
field = brightness,
state = brightness,
)
}
val colorFilter = Settings.colorFilter.asMutableState()
@@ -48,47 +45,39 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
field = colorFilter,
)
AnimatedVisibility(visible = colorFilter.value) {
var color by Settings.colorFilterValue.asMutableState()
val rf = remember { mutableIntStateOf(color.red) }
val r by rf
val gf = remember { mutableIntStateOf(color.green) }
val g by gf
val bf = remember { mutableIntStateOf(color.blue) }
val b by bf
val af = remember { mutableIntStateOf(color.alpha) }
val a by af
color = (a shl 24) or (r shl 16) or (g shl 8) or (b)
var color by Settings.colorFilterValue
val r = rememberIntSliderState(color.red, 0..255)
val g = rememberIntSliderState(color.green, 0..255)
val b = rememberIntSliderState(color.blue, 0..255)
val a = rememberIntSliderState(color.alpha, 0..255)
color = (a.value shl 24) or (r.value shl 16) or (g.value shl 8) or (b.value)
Column {
SliderChoice(
startSlot = { Text(text = "R") },
endSlot = { RollingNumber(number = r, length = 3) },
range = 0..255,
field = rf,
endSlot = { RollingNumber(number = r.value, length = 3) },
state = r,
)
SliderChoice(
startSlot = { Text(text = "G") },
endSlot = { RollingNumber(number = g, length = 3) },
range = 0..255,
field = gf,
endSlot = { RollingNumber(number = g.value, length = 3) },
state = g,
)
SliderChoice(
startSlot = { Text(text = "B") },
endSlot = { RollingNumber(number = b, length = 3) },
range = 0..255,
field = bf,
endSlot = { RollingNumber(number = b.value, length = 3) },
state = b,
)
SliderChoice(
startSlot = { Text(text = "A") },
endSlot = { RollingNumber(number = a, length = 3) },
range = 0..255,
field = af,
endSlot = { RollingNumber(number = a.value, length = 3) },
state = a,
)
}
}
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 = [0, 1, 2, 3, 4, 5],
field = Settings.colorFilterMode.asMutableState(),
)
SwitchChoice(
@@ -1,6 +1,11 @@
package com.hippo.ehviewer.ui.reader
import android.graphics.drawable.Animatable
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -11,8 +16,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.WavyProgressIndicatorDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -50,7 +57,6 @@ import com.hippo.ehviewer.gallery.progressObserved
import com.hippo.ehviewer.gallery.statusObserved
import com.hippo.ehviewer.image.Image
import com.hippo.ehviewer.util.AdsPlaceholderFile
import eu.kanade.tachiyomi.ui.reader.viewer.CombinedCircularProgressIndicator
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.drop
@@ -83,7 +89,22 @@ fun PagerItem(
modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT),
contentAlignment = Alignment.Center,
) {
CombinedCircularProgressIndicator(progress = state.progressObserved)
AnimatedContent(
targetState = state is PageStatus.Loading,
transitionSpec = { fadeIn() togetherWith fadeOut() },
label = "progressState",
) { determinate ->
val animatedProgress by animateFloatAsState(
targetValue = state.progressObserved,
animationSpec = WavyProgressIndicatorDefaults.ProgressAnimationSpec,
label = "progress",
)
if (determinate) {
CircularWavyProgressIndicator(progress = { animatedProgress })
} else {
CircularWavyProgressIndicator()
}
}
}
}
is PageStatus.Ready -> {
@@ -204,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()
@@ -9,6 +9,7 @@ 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
@@ -21,7 +22,7 @@ fun ReaderGeneralSetting() = Column(modifier = Modifier.verticalScroll(rememberS
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(
@@ -13,25 +13,28 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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 eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@Composable
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))
@@ -55,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 = [0, 1, 2, 3, 4, 5],
field = pagerNav,
)
AnimatedVisibility(visible = pagerNav.value != 5) {
@@ -63,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 = [0, 1, 2, 3],
field = Settings.readerPagerNavInverted.asMutableState(),
)
SwitchChoice(
@@ -76,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 = [1, 2, 3, 4, 5, 6],
field = scaleType,
)
AnimatedVisibility(visible = scaleType.value == 1) {
@@ -88,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 = [1, 2, 3, 4],
field = Settings.zoomStart.asMutableState(),
)
SwitchChoice(
@@ -108,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 = [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 = [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,15 +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
import androidx.compose.material3.BottomSheetDefaults
@@ -29,9 +25,10 @@ import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetValue
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -53,7 +50,6 @@ 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 arrow.core.Either
import arrow.core.Either.Companion.catch
import arrow.core.raise.ensure
@@ -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
@@ -97,6 +92,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderContentOverlay
import eu.kanade.tachiyomi.ui.reader.ReaderPageSheetMeta
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.merge
@@ -131,15 +127,11 @@ 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 ->
@@ -184,7 +176,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
@@ -223,9 +215,9 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
uiController.showTransientSystemBarsBySwipe = false
}
}
val lazyListState = rememberLazyListState(LazyLayoutCacheWindow(SCROLL_FRACTION, SCROLL_FRACTION), pageLoader.startPage)
val lazyListState = rememberLazyListState(pageLoader.startPage)
val pagerState = rememberPagerState(pageLoader.startPage) { pageLoader.size }
val syncState = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
val sync = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
var appbarVisible by remember { mutableStateOf(false) }
val isWebtoon by rememberUpdatedState(ReadingModeType.isWebtoon(readingMode))
val focusRequester = remember { FocusRequester() }
@@ -240,19 +232,15 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
sync.Sync(isWebtoon) { appbarVisible = false }
val bgColor by collectBackgroundColorAsState()
syncState.Sync(isWebtoon) { appbarVisible = false }
if (fullscreen) {
LaunchedEffect(Unit) {
val isDarkTheme = isSystemInDarkTheme()
LaunchedEffect(isDarkTheme) {
snapshotFlow { appbarVisible }.collect {
uiController.isSystemBarsVisible = it
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
Settings.showNavigationOverlayNewUser.value = false
@@ -264,7 +252,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
val blocked = page.status is PageStatus.Blocked
dialog { cont ->
fun dispose() = cont.resume(Unit)
val state = rememberModalBottomSheetState()
val state = rememberBottomSheetState(SheetValue.Hidden)
ModalBottomSheet(
onDismissRequest = { dispose() },
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
@@ -305,10 +293,10 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
onNavigationModeChange = { showNavigationOverlay = true },
onSelectPage = onSelectPage,
onMenuRegionClick = { appbarVisible = !appbarVisible },
modifier = Modifier.background(bgColor).pointerInput(syncState) {
modifier = Modifier.background(bgColor).pointerInput(sync) {
awaitEachGesture {
waitForUpOrCancellation()
syncState.reset()
sync.reset()
showNavigationOverlay = false
}
}.fillMaxSize().windowInsetsPadding(insets),
@@ -336,7 +324,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
)
if (brightness) {
LaunchedEffect(Unit) {
Settings.customBrightnessValue.valueFlow().sample(100)
Settings.customBrightnessValue.valueFlow().sample(100.milliseconds)
.onCompletion { activity.setCustomBrightnessValue(0) }
.collect { activity.setCustomBrightnessValue(it) }
}
@@ -345,7 +333,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
if (showPageNumber && !appbarVisible) {
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
PageIndicatorText(
currentPage = syncState.sliderValue,
currentPage = sync.slider.value,
totalPages = pageLoader.size,
modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
)
@@ -353,11 +341,12 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
}
ReaderAppBars(
visible = appbarVisible,
title = pageLoader.title,
isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT,
showSeekBar = showSeekbar,
currentPage = syncState.sliderValue,
slider = sync.slider,
totalPages = pageLoader.size,
onSliderValueChange = syncState::sliderScrollTo,
onSliderValueChange = sync::sliderScrollTo,
onClickSettings = {
launch {
dialog { cont ->
@@ -383,7 +372,6 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
}
}
},
modifier = Modifier.align(Alignment.BottomCenter),
)
}
}
@@ -395,7 +383,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)
}
@@ -6,11 +6,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import com.ehviewer.core.ui.component.IntSliderState
import com.hippo.ehviewer.gallery.PageLoader
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
@@ -23,12 +23,11 @@ class SliderPagerDoubleSync(
private val pageLoader: PageLoader,
) {
private var sliderFollowPager by mutableStateOf(true)
var sliderValue by mutableIntStateOf(pageLoader.startPage + 1)
private set
val slider = IntSliderState(pageLoader.startPage + 1, 1..pageLoader.size)
fun sliderScrollTo(index: Int) {
sliderFollowPager = false
sliderValue = index.coerceIn(1, pageLoader.size)
slider.value = index
}
fun reset() {
@@ -50,7 +49,7 @@ class SliderPagerDoubleSync(
@Composable
fun Sync(webtoon: Boolean, onPageSelected: () -> Unit) {
val currentIndexFlow = remember(webtoon) {
val initialIndex = sliderValue - 1
val initialIndex = slider.value - 1
sliderFollowPager = if (webtoon) {
lazyListState.firstVisibleItemIndex == initialIndex
} else {
@@ -61,14 +60,14 @@ class SliderPagerDoubleSync(
if (sliderFollowPager) {
LaunchedEffect(currentIndexFlow) {
currentIndexFlow.drop(1).collect { index ->
sliderValue = index + 1
slider.value = index + 1
pageLoader.startPage = index
onPageSelected()
}
}
} else {
LaunchedEffect(webtoon) {
snapshotFlow { sliderValue - 1 }.collectLatest { index ->
snapshotFlow { slider.value - 1 }.collectLatest { index ->
if (webtoon) {
lazyListState.scrollToItem(index)
} else {
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -81,6 +82,7 @@ fun WebtoonViewer(
state = lazyListState,
contentPadding = PaddingValues(horizontal = sidePadding),
verticalArrangement = Arrangement.spacedBy(if (withGaps) 15.dp else 0.dp),
cacheWindow = LazyLayoutCacheWindow(SCROLL_FRACTION, SCROLL_FRACTION),
) {
items(items, key = { it.index }) { page ->
PagerItem(
@@ -25,13 +25,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.IntSliderState
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,
@@ -84,20 +85,17 @@ fun SwitchChoice(title: String, field: MutableState<Boolean>) {
fun SliderChoice(
startSlot: @Composable () -> Unit,
endSlot: @Composable () -> Unit,
range: IntRange,
field: MutableState<Int>,
state: IntSliderState,
) {
var value by field
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
startSlot()
Slider(
value = value,
onValueChange = { value = it },
state = state,
onValueChange = { state.value = it },
modifier = Modifier.weight(1f).padding(8.dp),
valueRange = range,
)
endSlot()
}
@@ -19,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
@@ -146,6 +147,7 @@ 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) }
@@ -228,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) {
@@ -594,6 +597,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) =
info = info,
modifier = Modifier.thenIf(animateItems) { animateItem() },
showLanguage = false,
showProgress = showProgress,
)
}
}
@@ -633,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,
)
@@ -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
@@ -157,22 +159,21 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
) {
faves.forEachIndexed { index, (name, count) ->
ListItem(
headlineContent = { Text(text = name) },
trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) },
modifier = Modifier.clip(CardDefaults.shape).clickable {
val newCat = index - 2
refresh(FavListUrlBuilder(newCat))
Settings.recentFavCat = newCat
fabHidden = false
launch { sheetState.close() }
},
colors = listItemOnDrawerColor(urlBuilder.favCat == index - 2),
content = { Text(text = name) },
)
}
}
}
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,
)
@@ -260,12 +260,8 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
Column {
data.forEach { (name, vote) ->
ListItem(
headlineContent = {
Text(text = name)
},
trailingContent = {
Text(text = vote)
},
trailingContent = { Text(text = vote) },
content = { Text(text = name) },
)
}
}
@@ -409,7 +405,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_UPLOADER,
mKeyword = item.user,
keyword = item.user,
).asDst(),
)
},
@@ -215,7 +215,7 @@ fun GalleryDetailContent(
if (uploader.isNullOrEmpty() || disowned) {
return
}
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, mKeyword = uploader).asDst())
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, keyword = uploader).asDst())
}
fun onGalleryInfoCardClick() {
@@ -320,7 +320,6 @@ fun GalleryDetailContent(
galleryPreview(galleryDetail, previews) { navToReader(galleryDetail.galleryInfo, it) }
}
}
else -> FastScrollLazyVerticalGrid(
columns = GridCells.Fixed(thumbColumns),
contentPadding = contentPadding,
@@ -530,21 +529,21 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_NORMAL,
mKeyword = "\"" + keyword + "\"",
keyword = "\"" + keyword + "\"",
).asDst(),
)
} else if (artistTag != null) {
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_TAG,
mKeyword = artistTag,
keyword = artistTag,
).asDst(),
)
} else if (null != galleryDetail.uploader) {
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_UPLOADER,
mKeyword = galleryDetail.uploader,
keyword = galleryDetail.uploader,
).asDst(),
)
}
@@ -722,15 +721,17 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
val upTag = stringResource(R.string.tag_vote_up)
val downTag = stringResource(R.string.tag_vote_down)
val withDraw = stringResource(R.string.tag_vote_withdraw)
fun search(tag: String) = navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, keyword = tag).asDst())
GalleryTags(
tagGroups = tags,
onTagClick = {
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, mKeyword = it).asDst())
},
onTagClick = ::search,
onTagLongClick = { tag, translation, vote ->
val rawValue = tag.substringAfter(':')
launchIO {
awaitSelectAction {
onSelect(ctx.getString(R.string.search_bar_hint, tag)) {
withUIContext { search(tag) }
}
onSelect(copy) {
addTextToClipboard(tag)
}
@@ -49,14 +49,12 @@ import com.ehviewer.core.ui.util.rememberInVM
import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.fillInfo
import com.hippo.ehviewer.client.getImageKey
import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ktbuilder.executeIn
@@ -250,10 +248,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
) {
Text(text = stringResource(id = R.string.clear_image_cache_confirm))
}
repeat(gd.pages) {
val key = getImageKey(gd.gid, it)
imageCache.remove(key)
}
SpiderDen(gd).clearCache()
snackbar(cacheCleared)
}
},
@@ -147,7 +147,7 @@ fun AnimatedVisibilityScope.WhatshotScreen(navigator: DestinationsNavigator) = G
@Destination<RootGraph>
@Composable
fun AnimatedVisibilityScope.ToplistScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_TOPLIST, mKeyword = Settings.recentToplist), navigator)
fun AnimatedVisibilityScope.ToplistScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_TOPLIST, keyword = Settings.recentToplist), navigator)
@Destination<RootGraph>
@Composable
@@ -160,6 +160,9 @@ fun AnimatedVisibilityScope.GalleryListScreen(
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) }
val animateItems by Settings.animateItems.collectAsState()
var category by rememberMutableStateInDataStore("SearchCategory") { EhUtils.ALL_CATEGORY }
@@ -169,16 +172,16 @@ fun AnimatedVisibilityScope.GalleryListScreen(
LaunchedEffect(urlBuilder) {
if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category
if (urlBuilder.mode != MODE_TOPLIST) {
var keyword = urlBuilder.keyword.orEmpty()
if (urlBuilder.mode == MODE_TAG) {
keyword = wrapTagKeyword(keyword)
val keyword = urlBuilder.keyword?.let { keyword ->
when (urlBuilder.mode) {
MODE_UPLOADER -> "uploader:\"$keyword\""
MODE_TAG -> wrapTagKeyword(keyword)
MODE_TOPLIST -> ""
else -> keyword
}
if (keyword.isNotBlank()) {
}.orEmpty()
searchFieldState.setTextAndPlaceCursorAtEnd(keyword)
}
}
}
val density = LocalDensity.current
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
@@ -218,14 +221,13 @@ fun AnimatedVisibilityScope.GalleryListScreen(
ListItem(
modifier = Modifier.padding(horizontal = 4.dp).clip(CardDefaults.shape).clickable {
Settings.recentToplist = keyword
urlBuilder = ListUrlBuilder(MODE_TOPLIST, mKeyword = keyword)
urlBuilder = ListUrlBuilder(MODE_TOPLIST, keyword = keyword)
data.refresh()
fabHidden = false
launch { sheetState.close() }
},
headlineContent = {
Text(text = name)
},
colors = listItemOnDrawerColor(urlBuilder.keyword == keyword),
content = { Text(text = name) },
)
}
}
@@ -374,6 +376,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
language = languageFilter
}
data.refresh()
fabHidden = false
}
launch { sheetState.close() }
},
@@ -425,9 +428,6 @@ fun AnimatedVisibilityScope.GalleryListScreen(
}
}
var fabExpanded by remember { mutableStateOf(false) }
var fabHidden by remember { mutableStateOf(false) }
val openGalleryKeyword = stringResource(R.string.gallery_list_search_bar_open_gallery)
abstract class UrlSuggestion : Suggestion() {
override val keyword = openGalleryKeyword
@@ -482,9 +482,9 @@ fun AnimatedVisibilityScope.GalleryListScreen(
searchFieldState = searchFieldState,
suggestionProvider = {
GalleryDetailUrlParser.parse(it, false)?.run {
listOf(GalleryDetailUrlSuggestion(gid, token))
[GalleryDetailUrlSuggestion(gid, token)]
} ?: GalleryPageUrlParser.parse(it, false)?.run {
listOf(GalleryPageUrlSuggestion(gid, pToken, page))
[GalleryPageUrlSuggestion(gid, pToken, page)]
}.orEmpty()
},
localSearch = false,
@@ -509,6 +509,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
) { contentPadding ->
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() }
@@ -537,6 +538,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
onLongClick = { launch { doGalleryInfoAction(info) } },
info = info,
showPages = showPages,
showProgress = showProgress,
modifier = Modifier.height(height),
)
},
@@ -547,6 +549,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
onLongClick = { launch { doGalleryInfoAction(info) } },
info = info,
showPages = showPages,
showProgress = showProgress,
)
},
searchBarOffsetY = { searchBarOffsetY },
@@ -577,7 +580,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
onExpandChanged = { fabExpanded = it },
autoCancel = true,
) {
if (urlBuilder.mode in arrayOf(MODE_NORMAL, MODE_UPLOADER, MODE_TAG)) {
if (urlBuilder.mode in [MODE_NORMAL, MODE_UPLOADER, MODE_TAG]) {
onClick(Icons.Default.Shuffle) {
urlBuilder.setRange(Random.nextInt(100))
data.refresh()
@@ -640,6 +643,7 @@ private fun getSuitableTitleForUrlBuilder(urlBuilder: ListUrlBuilder, appName: B
val canTranslate = Settings.showTagTranslations.value && EhTagDatabase.translatable && EhTagDatabase.initialized
wrapTagKeyword(keyword, canTranslate)
}
MODE_UPLOADER -> "uploader:\"$keyword\""
else -> keyword
}
} else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) {
@@ -136,6 +136,7 @@ fun AnimatedVisibilityScope.HistoryScreen(navigator: DestinationsNavigator) = Sc
val marginH = dimensionResource(id = com.hippo.ehviewer.R.dimen.gallery_list_margin_h)
val cardHeight by collectListThumbSizeAsState()
val showPages by Settings.showGalleryPages.collectAsState()
val showProgress by Settings.showReadingProgress.collectAsState()
FastScrollLazyColumn(
modifier = Modifier.nestedScroll(searchBarConnection).fillMaxSize(),
contentPadding = paddingValues,
@@ -161,6 +162,7 @@ fun AnimatedVisibilityScope.HistoryScreen(navigator: DestinationsNavigator) = Sc
onLongClick = { launch { doGalleryInfoAction(info) } },
info = info,
showPages = showPages,
showProgress = showProgress,
modifier = Modifier.height(cardHeight).padding(horizontal = marginH),
)
}
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.plus
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ElevatedCard
@@ -294,7 +294,6 @@ fun SearchBarScreen(
item {}
items(mSuggestionList, key = { it.keyword.hashCode() * 31 + it.canDelete.hashCode() }) {
ListItem(
headlineContent = { Text(text = it.keyword) },
supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) },
leadingContent = it.canOpenDirectly.ifTrueThen {
Icon(
@@ -312,6 +311,7 @@ fun SearchBarScreen(
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
modifier = Modifier.clickable { it.onClick() }.thenIf(animateItems) { animateItem() },
content = { Text(text = it.keyword) },
)
}
}
@@ -36,7 +36,7 @@ import com.hippo.ehviewer.ui.main.NavigationIcon
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.updater.AppUpdater
import com.hippo.ehviewer.updater.Release
import com.hippo.ehviewer.updater.EhRelease
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.displayString
@@ -49,8 +49,8 @@ import moe.tarsin.navigate
import moe.tarsin.snackbar
import moe.tarsin.string
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}"
private const val RELEASE_URL = "$REPO_URL/releases"
const val REPO_URL = "https://codeberg.org/${BuildConfig.REPO_NAME}"
const val RELEASE_URL = "$REPO_URL/releases"
@Composable
@Stable
@@ -123,8 +123,8 @@ fun AnimatedVisibilityScope.AboutScreen(navigator: DestinationsNavigator) = Scre
}
}
context(ctx: Context, _: DialogState)
suspend fun showNewVersion(release: Release) {
context(_: Context, _: DialogState)
suspend fun showNewVersion(release: EhRelease) {
awaitConfirmationOrCancel(
confirmText = R.string.download,
title = R.string.new_version_available,
@@ -140,7 +140,7 @@ suspend fun showNewVersion(release: Release) {
}
if (Settings.backupBeforeUpdate.value) {
val time = ReadableTime.getFilenamableTime()
EhDB.exportDB(ctx, (downloadLocation / "$time.db"))
EhDB.exportDB(downloadLocation / "$time.db")
}
// TODO: Download in the background and show progress in notification
val path = AppConfig.tempDir / "update.apk"
@@ -35,7 +35,6 @@ import com.ehviewer.core.files.sendTo
import com.ehviewer.core.files.toOkioPath
import com.ehviewer.core.i18n.R
import com.ehviewer.core.util.isAtLeastO
import com.ehviewer.core.util.isAtLeastSExtension7
import com.ehviewer.core.util.launch
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withIOContext
@@ -46,6 +45,7 @@ import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ktor.isCronetAvailable
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.main.NavigationIcon
import com.hippo.ehviewer.ui.showRestartDialog
@@ -63,6 +63,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.merge
@@ -94,15 +95,15 @@ private fun dumplog(uri: Uri): Unit = with(ctx) {
}
context(ctx: Context)
private fun exportDatabase(uri: Uri) {
private suspend fun exportDatabase(uri: Uri) {
ctx.grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
EhDB.exportDB(ctx, uri.toOkioPath())
EhDB.exportDB(uri.toOkioPath())
}
context(ctx: Context)
private suspend fun importDatabase(uri: Uri) {
ctx.grantUriPermission(BuildConfig.APPLICATION_ID, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
EhDB.importDB(ctx, uri)
EhDB.importDB(uri.toOkioPath())
}
@Destination<RootGraph>
@@ -185,7 +186,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
title = { Text(stringResource(id = R.string.settings_advanced_app_language_title)) },
summary = { Text(languages[currentLanguage].orEmpty()) },
)
if (isAtLeastSExtension7) {
if (isCronetAvailable) {
val enableCronet = Settings.enableCronet.asMutableState()
if (BuildConfig.DEBUG || !enableCronet.value) {
SwitchPreference(
@@ -214,7 +215,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
step = 3,
title = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold),
summary = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold_summary),
state = Settings.hardwareBitmapThreshold.asMutableState(),
pref = Settings.hardwareBitmapThreshold,
)
}
SwitchPreference(
@@ -289,7 +290,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
EhDB.putLocalFavorites(result.galleryInfoList)
launchSnackbar(string(R.string.settings_advanced_backup_favorite_start, status))
if (result.next != null) {
delay(Settings.downloadDelay.value.toLong())
delay(Settings.downloadDelay.value.milliseconds)
favListUrlBuilder.setIndex(result.next, true)
doBackup()
}
@@ -195,7 +195,7 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
maxValue = 10,
minValue = MIN_SPEED_LEVEL,
title = stringResource(id = R.string.settings_download_timeout_speed),
state = Settings.timeoutSpeed.asMutableState(),
pref = Settings.timeoutSpeed,
display = ::speedLevelToSpeed,
)
val preloadImage = Settings.preloadImage.asMutableState()
@@ -29,11 +29,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.fromHtml
import com.ehviewer.core.i18n.R
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.util.isAtLeastT
import com.ehviewer.core.util.launch
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.collectAsState
@@ -193,7 +193,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
minValue = 20,
step = 7,
title = stringResource(id = R.string.list_tile_thumb_size),
state = Settings.listThumbSize.asMutableState(),
pref = Settings.listThumbSize,
)
SimpleMenuPreferenceInt(
title = stringResource(id = R.string.settings_eh_detail_size),
@@ -207,13 +207,18 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
maxValue = 10,
minValue = 1,
title = stringResource(id = R.string.settings_eh_thumb_columns),
state = Settings.thumbColumns.asMutableState(),
pref = Settings.thumbColumns,
)
SwitchPreference(
title = stringResource(id = R.string.settings_eh_show_gallery_pages),
summary = stringResource(id = R.string.settings_eh_show_gallery_pages_summary),
state = Settings.showGalleryPages.asMutableState(),
)
SwitchPreference(
title = stringResource(id = R.string.settings_eh_show_reading_progress),
summary = stringResource(id = R.string.settings_eh_show_reading_progress_summary),
state = Settings.showReadingProgress.asMutableState(),
)
SwitchPreference(
title = stringResource(id = R.string.settings_eh_show_vote_status),
state = Settings.showVoteStatus.asMutableState(),
@@ -230,7 +235,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
minValue = -101,
title = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold),
summary = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold_summary),
state = Settings.commentThreshold.asMutableState(),
pref = Settings.commentThreshold,
)
}
if (EhTagDatabase.translatable) {
@@ -26,6 +26,7 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenu
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
@@ -1,8 +1,6 @@
package com.hippo.ehviewer.ui.settings
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@@ -12,14 +10,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.main.NavigationIcon
import com.hippo.ehviewer.ui.openBrowser
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.variant.LibrariesVariant
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryActionKind
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryBadges
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -42,10 +41,19 @@ fun AnimatedVisibilityScope.LicenseScreen(navigator: DestinationsNavigator) = Sc
libraries = libraries,
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
contentPadding = paddingValues,
padding = LibraryDefaults.libraryPadding(licensePadding = LibraryDefaults.chipPadding(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp))),
textStyles = LibraryDefaults.libraryTextStyles(licensesTextStyle = MaterialTheme.typography.labelSmall),
onLibraryClick = { library ->
library.website?.let { openBrowser(it) }
badges = LibraryBadges(funding = false),
variant = LibrariesVariant.Refined,
onActionClick = { lib, kind ->
val open = { url: String -> url.isNotBlank().also { if (it) openBrowser(url) } }
val website = lib.website.orEmpty()
val source = lib.scm?.url.orEmpty()
val license = lib.licenses.firstOrNull()?.url.orEmpty()
when (kind) {
LibraryActionKind.Source -> open(source)
LibraryActionKind.Website -> open(website)
LibraryActionKind.License -> open(license)
LibraryActionKind.Sponsor -> false
}
},
)
}
@@ -21,7 +21,9 @@ import androidx.compose.ui.res.integerArrayResource
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import com.ehviewer.core.preferences.PrefDelegate
import com.ehviewer.core.ui.component.RollingNumber
import com.ehviewer.core.ui.component.rememberIntPrefSliderState
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.util.ProgressDialog
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -71,19 +73,16 @@ fun SwitchPreference(title: String, summary: String? = null, state: MutableState
}
@Composable
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, title: String, summary: String? = null, state: MutableState<Int>, enabled: Boolean = true, display: (Int) -> Int = { it }) {
val sliderState = remember { mutableIntStateOf(state.value) }
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, title: String, summary: String? = null, pref: PrefDelegate<Int>, enabled: Boolean = true, display: (Int) -> Int = { it }) {
val state = rememberIntPrefSliderState(pref, minValue..maxValue, step)
IntSliderPreference(
state = state,
title = { Text(title) },
valueRange = minValue..maxValue,
valueSteps = step,
sliderState = sliderState,
enabled = enabled,
summary = summary?.let { { Text(it) } },
valueText = {
RollingNumber(
number = display(sliderState.intValue),
number = display(state.value),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
length = 5,
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
@@ -17,6 +18,7 @@ import com.ehviewer.core.util.launch
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.isAuthenticationSupported
import com.hippo.ehviewer.ui.main.NavigationIcon
@@ -48,17 +50,17 @@ fun AnimatedVisibilityScope.PrivacyScreen(navigator: DestinationsNavigator) = Sc
enabled = isAuthenticationSupported(),
)
AnimatedVisibility(visible = security.value) {
val securityDelay = Settings.securityDelay.asMutableState()
val summary = if (securityDelay.value == 0) {
val delay by Settings.securityDelay.collectAsState()
val summary = if (delay == 0) {
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary_immediately)
} else {
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary, securityDelay.value)
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary, delay)
}
IntSliderPreference(
maxValue = 30,
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
summary = summary,
state = securityDelay,
pref = Settings.securityDelay,
enabled = isAuthenticationSupported(),
)
}
@@ -16,11 +16,11 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.ehviewer.core.i18n.R
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.util.launch
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewNavigator
import com.google.accompanist.web.rememberWebViewState
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.main.NavigationIcon
@@ -94,7 +94,7 @@ fun AnnotatedString.normalizeSpan(): AnnotatedString {
val italic = filterMerged(SpanStyle(fontStyle = FontStyle.Italic)) { it.fontStyle == FontStyle.Italic }
val underline = filterMerged(SpanStyle(textDecoration = TextDecoration.Underline)) { it.textDecoration == TextDecoration.Underline }
val linethrough = filterMerged(SpanStyle(textDecoration = TextDecoration.LineThrough)) { it.textDecoration == TextDecoration.LineThrough }
listOf(bold, italic, underline, linethrough).flatten()
bold + italic + underline + linethrough
}
return AnnotatedString(text, spans)
}
@@ -42,6 +42,7 @@ import androidx.compose.material3.DatePickerDefaults
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenu
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
@@ -684,10 +685,10 @@ suspend fun awaitSelectItemWithIcon(
}
itemsIndexed(items) { index, (icon, text) ->
ListItem(
headlineContent = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) },
modifier = Modifier.clickable { resume(index) }.padding(horizontal = 8.dp),
leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) },
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
content = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) },
)
}
}
@@ -756,17 +757,17 @@ private fun CheckableItem(text: String, checked: Boolean, modifier: Modifier = M
val textStyle = MaterialTheme.typography.titleMedium
val checkedColor = MaterialTheme.colorScheme.primary
ListItem(
headlineContent = {
Text(
text = text,
style = if (checked) textStyle.copy(color = checkedColor) else textStyle,
)
},
modifier = modifier,
trailingContent = checked.ifTrueThen {
Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = checkedColor)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
content = {
Text(
text = text,
style = if (checked) textStyle.copy(color = checkedColor) else textStyle,
)
},
)
}
@@ -9,7 +9,6 @@ import com.hippo.ehviewer.spider.timeoutBySpeed
import com.hippo.ehviewer.util.copyTo
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.accept
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.ContentType
@@ -21,17 +20,18 @@ import kotlin.time.Instant
import kotlinx.io.asSource
import moe.tarsin.coroutines.runSuspendCatching
import okio.Path
import tachiyomi.data.release.GithubArtifacts
import tachiyomi.data.release.GithubCommitComparison
import tachiyomi.data.release.GithubRelease
import tachiyomi.data.release.GithubRepo
import tachiyomi.data.release.GithubWorkflowRuns
import tachiyomi.data.release.Artifacts
import tachiyomi.data.release.CommitComparison
import tachiyomi.data.release.Release
import tachiyomi.data.release.Repository
import tachiyomi.data.release.WorkflowRuns
import tachiyomi.data.release.downloadLink
private const val API_URL = "https://api.github.com/repos/${BuildConfig.REPO_NAME}"
private const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
const val API_URL = "https://codeberg.org/api/v1/repos/${BuildConfig.REPO_NAME}"
const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
object AppUpdater {
suspend fun checkForUpdate(forceCheck: Boolean = false): Release? {
suspend fun checkForUpdate(forceCheck: Boolean = false): EhRelease? {
val now = Clock.System.now()
val last = Instant.fromEpochSeconds(Settings.lastUpdateTime)
val interval = Settings.updateIntervalDays.value
@@ -39,31 +39,30 @@ object AppUpdater {
Settings.lastUpdateTime = now.epochSeconds
if (Settings.useCIUpdateChannel.value) {
val curSha = BuildConfig.COMMIT_SHA
val branch = ghStatement(API_URL).executeAndParseAs<GithubRepo>().defaultBranch
val workflowRunsUrl = "$API_URL/actions/workflows/ci.yml/runs?branch=$branch&event=push&status=success&per_page=1"
val workflowRun = ghStatement(workflowRunsUrl).executeAndParseAs<GithubWorkflowRuns>().workflowRuns[0]
val shortSha = workflowRun.headSha.take(7)
val branch = statement(API_URL).executeAndParseAs<Repository>().defaultBranch
val run = statement("$API_URL/actions/runs?event=push&status=success&ref=refs/heads/$branch").executeAndParseAs<WorkflowRuns>().workflowRuns[0]
val shortSha = run.headSha.take(7)
if (shortSha != curSha) {
val artifacts = ghStatement(workflowRun.artifactsUrl).executeAndParseAs<GithubArtifacts>()
val archiveUrl = artifacts.getDownloadLink()
val artifacts = statement("$API_URL/actions/runs/${run.id}/artifacts").executeAndParseAs<Artifacts>()
val archiveUrl = artifacts.downloadLink
val changelog = runSuspendCatching {
val commitComparisonUrl = "$API_URL/compare/$curSha...$shortSha"
val result = ghStatement(commitComparisonUrl).executeAndParseAs<GithubCommitComparison>()
val result = statement(commitComparisonUrl).executeAndParseAs<CommitComparison>()
// TODO: Prettier format, Markdown?
result.commits.joinToString("\n") { commit ->
"${commit.commit.message.takeWhile { it != '\n' }} (@${commit.commit.author.name})"
}
}.getOrDefault(workflowRun.title)
return Release(shortSha, changelog, archiveUrl)
}.getOrDefault(run.title)
return EhRelease(shortSha, changelog, archiveUrl)
}
} else {
val curVersion = BuildConfig.RAW_VERSION_NAME
val release = ghStatement(LATEST_RELEASE_URL).executeAndParseAs<GithubRelease>()
val release = statement(LATEST_RELEASE_URL).executeAndParseAs<Release>()
val latestVersion = release.version
val description = release.info
val downloadUrl = release.getDownloadLink()
if (latestVersion != curVersion) {
return Release(latestVersion, description, downloadUrl)
return EhRelease(latestVersion, description, downloadUrl)
}
}
}
@@ -75,7 +74,7 @@ object AppUpdater {
timeoutBySpeed(
url,
{
ghStatement(url) {
statement(url) {
// https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#get-a-release-asset
if (!isZip) accept(ContentType.Application.OctetStream)
it()
@@ -98,22 +97,14 @@ object AppUpdater {
}
}
private suspend inline fun ghStatement(
suspend inline fun statement(
url: String,
builder: HttpRequestBuilder.() -> Unit = {},
) = ktorClient.prepareGet(url) {
bearerAuth(GithubTokenParts.joinToString("_"))
apply(builder)
}
private val GithubTokenParts = arrayOf(
"github",
"pat",
"11A4H2ACI0iGDuL1O6wPYW",
"OTFg8xaCNUwR1NHaJE1AT3LoYPfz6bouI7E7ReLf8GjIRFHCL5UsHL9EnWP",
)
data class Release(
data class EhRelease(
val version: String,
val changelog: String,
val downloadLink: String,
@@ -39,18 +39,14 @@ object AppConfig {
private const val CRASH = "crash"
private const val TAG_TRANSLATIONS = "tag-translations"
private val abi = Build.SUPPORTED_ABIS[0].takeIf {
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a")
} ?: "universal"
private val abi = Build.SUPPORTED_ABIS[0].takeIf { it in ["arm64-v8a", "x86_64", "armeabi-v7a"] } ?: "universal"
val isBenchmark = "nonMinified" in BuildConfig.BUILD_TYPE || "benchmark" in BuildConfig.BUILD_TYPE
fun matchVariant(name: String) = name.contains(BuildConfig.FLAVOR) && name.contains(abi)
fun matchVariant(name: String) = abi in name
val commitTime = ParserUtils.formatDate(BuildConfig.COMMIT_TIME * 1000)
val isSnapshot = "SNAPSHOT" in BuildConfig.VERSION_NAME
private val externalAppDir: File?
get() {
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
@@ -22,8 +22,8 @@ import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.os.PersistableBundle
import android.view.textclassifier.TextClassifier
import androidx.core.os.persistableBundleOf
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.ui.MainActivity
import moe.tarsin.tip
@@ -33,7 +33,9 @@ fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
clipboardManager.setPrimaryClip(
ClipData.newPlainText(null, text).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isSensitive) {
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
description.extras = PersistableBundle().apply {
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
}
}
},
)
@@ -79,9 +79,9 @@ object FileUtils {
*/
// From https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/FileUtils.java;l=1142;drc=7b647e4ea0e92f33c19b315eaed364ee067ba0aa
fun sanitizeFilename(name: String): String {
if (name.isEmpty() || "." == name || ".." == name) {
return "(invalid)"
}
// Windows does not support filenames ending with a space or a period
val name = name.trimEnd(' ', '.')
check(name.isNotEmpty()) { "Invalid filename" }
return buildString(name.length) {
name.forEach {
if (isValidFatFilenameChar(it)) {
@@ -16,11 +16,11 @@
package com.hippo.ehviewer.util
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalLocale
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import com.ehviewer.core.i18n.R
import com.ehviewer.core.util.toLocalDateTime
import java.util.Locale
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.datetime.LocalDate
@@ -127,7 +127,7 @@ object ReadableTime {
val timeDate = time.toLocalDateTime(timeZone).date
val nowYear = nowDate.year
val timeYear = timeDate.year
val isZh = Locale.getDefault().language == "zh"
val isZh = LocalLocale.current.language == "zh"
if (nowYear == timeYear) {
if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR
} else {
@@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenu
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.FlexibleBottomAppBar
@@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -19,6 +18,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.IntSliderState
import com.ehviewer.core.ui.component.RollingNumber
import com.ehviewer.core.ui.component.RollingNumberPlaceholder
import com.ehviewer.core.ui.component.Slider
import com.ehviewer.core.ui.component.defaultMaxTickCount
import com.ehviewer.core.ui.util.HapticFeedbackType
@@ -29,7 +31,7 @@ import com.ehviewer.core.ui.util.rememberHapticFeedback
@Composable
fun ChapterNavigator(
isRtl: Boolean,
currentPage: Int,
slider: IntSliderState,
totalPages: Int,
onSliderValueChange: (Int) -> Unit,
containerColor: Color,
@@ -40,14 +42,13 @@ fun ChapterNavigator(
modifier = Modifier.padding(horizontal = horizontalPadding).clip(CircleShape).background(containerColor).padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = "$currentPage")
val steps = totalPages - 2
RollingNumber(slider.value, length = "$totalPages".length)
val maxTickCount = defaultMaxTickCount()
val interactionSource = remember { MutableInteractionSource() }
if (steps < maxTickCount) {
if (slider.state.steps < maxTickCount) {
val sliderDragged by interactionSource.collectIsDraggedAsState()
val hapticFeedback = rememberHapticFeedback()
LaunchedEffect(currentPage) {
LaunchedEffect(slider.value) {
if (sliderDragged) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
}
@@ -55,13 +56,11 @@ fun ChapterNavigator(
}
Slider(
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
value = currentPage,
valueRange = 1..totalPages,
steps = steps,
state = slider,
onValueChange = onSliderValueChange,
maxTickCount = maxTickCount,
interactionSource = interactionSource,
)
Text(text = "$totalPages")
RollingNumberPlaceholder(totalPages)
}
}
@@ -6,47 +6,73 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.systemBarsIgnoringVisibility
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.IntSliderState
import com.hippo.ehviewer.ui.main.NavigationIcon
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
private val animationSpec = tween<IntOffset>(200)
@Composable
fun ReaderAppBars(
context(navigator: DestinationsNavigator)
fun BoxScope.ReaderAppBars(
visible: Boolean,
title: String,
isRtl: Boolean,
showSeekBar: Boolean,
currentPage: Int,
slider: IntSliderState,
totalPages: Int,
onSliderValueChange: (Int) -> Unit,
onClickSettings: () -> Unit,
modifier: Modifier = Modifier,
) = AnimatedVisibility(
) {
val backgroundColor = BottomAppBarDefaults.containerColor.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
AnimatedVisibility(
visible = visible,
modifier = modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)),
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBarsIgnoringVisibility.only(WindowInsetsSides.Horizontal)).align(Alignment.TopStart),
enter = slideInVertically(initialOffsetY = { -it }, animationSpec = animationSpec),
exit = slideOutVertically(targetOffsetY = { -it }, animationSpec = animationSpec),
) {
TopAppBar(
title = { Text(text = title, maxLines = 2, overflow = TextOverflow.Ellipsis) },
colors = TopAppBarDefaults.topAppBarColors(containerColor = backgroundColor),
navigationIcon = { NavigationIcon() },
)
}
AnimatedVisibility(
visible = visible,
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBarsIgnoringVisibility.only(WindowInsetsSides.Horizontal)).align(Alignment.BottomStart),
enter = slideInVertically(initialOffsetY = { it }, animationSpec = animationSpec),
exit = slideOutVertically(targetOffsetY = { it }, animationSpec = animationSpec),
) {
val toolbarColor = BottomAppBarDefaults.containerColor.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
if (showSeekBar && totalPages > 1) {
ChapterNavigator(
isRtl = isRtl,
currentPage = currentPage,
slider = slider,
totalPages = totalPages,
onSliderValueChange = onSliderValueChange,
containerColor = toolbarColor,
containerColor = backgroundColor,
)
}
BottomReaderBar(onClickSettings = onClickSettings, containerColor = toolbarColor)
BottomReaderBar(onClickSettings = onClickSettings, containerColor = backgroundColor)
}
}
}
@@ -1,54 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.WavyProgressIndicatorDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
@Composable
fun CombinedCircularProgressIndicator(progress: Float) {
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = WavyProgressIndicatorDefaults.ProgressAnimationSpec,
label = "progress",
)
AnimatedContent(
targetState = progress == 0f,
transitionSpec = { fadeIn() togetherWith fadeOut() },
label = "progressState",
) { indeterminate ->
if (indeterminate) {
// Indeterminate
CircularWavyProgressIndicator()
} else {
// Determinate
val infiniteTransition = rememberInfiniteTransition(label = "infiniteRotation")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "rotation",
)
CircularWavyProgressIndicator(
progress = { animatedProgress },
modifier = Modifier.rotate(rotation),
)
}
}
}
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff. Show more