Compare commits

..
176 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
126 changed files with 1808 additions and 2008 deletions

No files matched your search

File renamed without changes.
File renamed without changes.
@@ -13,17 +13,8 @@ on:
jobs: jobs:
baseline-profile: baseline-profile:
runs-on: ubuntu-latest runs-on: nekomoee
steps: 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 - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -31,13 +22,19 @@ jobs:
uses: actions/setup-java@v5 uses: actions/setup-java@v5
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version-file: .java-version
- name: Install Rust Toolchain - name: Setup Android SDK
run: rustup target add armv7-linux-androideabi 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 - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: https://github.com/Swatinem/rust-cache@v2
with: with:
workspaces: "./app/src/main/rust" workspaces: "./app/src/main/rust"
@@ -48,7 +45,7 @@ jobs:
key: cmake- key: cmake-
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@v6
with: with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }} cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true cache-read-only: true
@@ -59,7 +56,7 @@ jobs:
- name: Upload reports - name: Upload reports
if: failure() if: failure()
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v3
with: with:
name: reports name: reports
path: benchmark/build/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
-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@v6
- 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@v6
- 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@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-
# 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@v6
with:
name: default-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v6
with:
name: default-x86_64-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v6
with:
name: default-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
- name: Upload mapping
if: always()
uses: actions/upload-artifact@v6
with:
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease
- name: Upload native debug symbols
uses: actions/upload-artifact@v6
with:
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
marshmallow:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- 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@v5
with:
path: app/.cxx
key: cmake-marshmallow-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
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@v6
with:
name: marshmallow-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v6
with:
name: marshmallow-x86_64-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v6
with:
name: marshmallow-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
- name: Upload mapping
if: always()
uses: actions/upload-artifact@v6
with:
name: marshmallow-mapping-${{ github.sha }}
path: app/build/outputs/mapping/marshmallowRelease
- name: Upload native debug symbols
uses: actions/upload-artifact@v6
with:
name: marshmallow-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
-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@v6
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@v6
- 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@v5
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
+11 -89
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"> <h1 align="center">
<img src="https://github.com/FooIbar/EhViewer-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> <br>EhViewer<br>
</h1> </h1>
<p align="center"> <p align="center">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml"> Standalone fork of
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions"> <a href="https://codeberg.org/EhViewer/EhViewer">EhViewer (codeberg)</a>
</a> maintained at <a href="https://gitea.yrkaierx.com/yenru0/EhViewer">gitea.yrkaierx.com/yenru0/EhViewer</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>
</p> </p>
<div align="center"> # Changes over upstream
<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>
# 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/) 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.
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/FooIbar/EhViewer-art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
# Screenshot
![screenshots-01](https://github.com/FooIbar/EhViewer-art/blob/master/screenshots-01.webp)
![screenshots-02](https://github.com/FooIbar/EhViewer-art/blob/master/screenshots-02.webp)
# Thanks
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/)
# License # License
+12 -7
View File
@@ -49,15 +49,20 @@ android {
val repoName = providers.exec { val repoName = providers.exec {
commandLine = "git remote get-url origin".split(' ') commandLine = "git remote get-url origin".split(' ')
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:") }.standardOutput.asText.get().trim().removeSuffix(".git").removePrefix("https://codeberg.org/")
.removeSuffix(".git")
val snapshot = !hasProperty("release")
defaultConfig { defaultConfig {
applicationId = "moe.tarsin.ehviewer" applicationId = "yenru0.yrkaierx.ehviewer"
versionCode = 180063 versionCode = 180064
versionName = "1.14.6" versionName = if (snapshot) {
// versionNameSuffix = "-SNAPSHOT" "1.15.0-SNAPSHOT"
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"") } else {
"1.15.0"
}
buildConfigField("boolean", "SNAPSHOT", "$snapshot")
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName\"")
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"") buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("long", "COMMIT_TIME", commitTime) buildConfigField("long", "COMMIT_TIME", commitTime)
buildConfigField("String", "REPO_NAME", "\"$repoName\"") buildConfigField("String", "REPO_NAME", "\"$repoName\"")
+4
View File
@@ -15,6 +15,10 @@
java.nio.ByteBuffer source; java.nio.ByteBuffer source;
} }
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
static boolean isLinkBufferComposerEnabled() return true;
}
# Ktor logger # Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder -dontwarn org.slf4j.impl.StaticLoggerBinder
-4
View File
@@ -118,10 +118,6 @@
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings" />
<activity <activity
android:name=".shortcuts.ShortcutsActivity" android:name=".shortcuts.ShortcutsActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
+10 -8
View File
@@ -32,7 +32,7 @@ option(XZ_TOOL_XZDEC OFF)
FetchContent_Declare( FetchContent_Declare(
liblzma liblzma
GIT_REPOSITORY https://github.com/tukaani-project/xz.git GIT_REPOSITORY https://github.com/tukaani-project/xz.git
GIT_TAG v5.8.1 GIT_TAG v5.8.3
GIT_SHALLOW 1 GIT_SHALLOW 1
) )
@@ -47,7 +47,7 @@ if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
else () else ()
set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin") set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin")
endif () endif ()
set(CONFIGURE_COMMAND ./configure --disable-public-key --disable-dependency-tracking set(CONFIGURE_COMMAND ${WSL} ./.bootstrap && ${WSL} ./configure --disable-public-key --disable-dependency-tracking
"--host=${CMAKE_C_COMPILER_TARGET}" "--host=${CMAKE_C_COMPILER_TARGET}"
"CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments" "CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments"
"AR=${TOOLCHAIN}/llvm-ar" "AR=${TOOLCHAIN}/llvm-ar"
@@ -56,9 +56,11 @@ set(CONFIGURE_COMMAND ./configure --disable-public-key --disable-dependency-trac
) )
ExternalProject_Add( ExternalProject_Add(
nettle nettle
URL https://ftpmirror.gnu.org/nettle/nettle-3.10.2.tar.gz GIT_REPOSITORY https://gitlab.com/gnutls/nettle.git
URL_MD5 b28bcbf6f045ff007940a9401673600d GIT_TAG nettle_4.0_release_20260205
CONFIGURE_COMMAND ${WSL} ${CONFIGURE_COMMAND} GIT_SHALLOW 1
GIT_CONFIG core.autocrlf=input
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
BUILD_COMMAND ${WSL} make -j4 libnettle.a BUILD_COMMAND ${WSL} make -j4 libnettle.a
INSTALL_COMMAND "" INSTALL_COMMAND ""
BUILD_IN_SOURCE 1 BUILD_IN_SOURCE 1
@@ -101,7 +103,7 @@ set(LIBARCHIVE_PATCH
FetchContent_Declare( FetchContent_Declare(
libarchive libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive.git GIT_REPOSITORY https://github.com/libarchive/libarchive.git
GIT_TAG v3.8.4 GIT_TAG v3.8.7
GIT_SHALLOW 1 GIT_SHALLOW 1
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH} PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
) )
@@ -138,8 +140,8 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ../../../../rust-
unset(Rust_TOOLCHAIN CACHE) unset(Rust_TOOLCHAIN CACHE)
FetchContent_Declare( FetchContent_Declare(
Corrosion Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git GIT_REPOSITORY https://github.com/FooIbar/corrosion.git
GIT_TAG v0.6 GIT_TAG v0.6.1
GIT_SHALLOW 1 GIT_SHALLOW 1
) )
+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]; byte digest[SHA1_DIGEST_SIZE];
sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest); sha1_digest(&ctx, digest);
byte byte; byte byte;
char hex_digest[2 * SHA1_DIGEST_SIZE + 1]; char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
@@ -67,7 +67,6 @@ import com.hippo.ehviewer.ktor.configureClient
import com.hippo.ehviewer.ktor.configureCommon import com.hippo.ehviewer.ktor.configureCommon
import com.hippo.ehviewer.ktor.isCronetAvailable import com.hippo.ehviewer.ktor.isCronetAvailable
import com.hippo.ehviewer.ui.keepNoMediaFileStatus import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.lockObserver
import com.hippo.ehviewer.ui.screen.detailCache import com.hippo.ehviewer.ui.screen.detailCache
import com.hippo.ehviewer.ui.tools.dataStateFlow import com.hippo.ehviewer.ui.tools.dataStateFlow
import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.AppConfig
@@ -109,7 +108,6 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
} }
} }
} }
lifecycle.addObserver(lockObserver)
CrashHandler.install() CrashHandler.install()
super.onCreate() super.onCreate()
System.loadLibrary("ehviewer") System.loadLibrary("ehviewer")
@@ -124,6 +122,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
} }
launchIO { launchIO {
EhTagDatabase.launchUpdate() EhTagDatabase.launchUpdate()
@Suppress("UNUSED_EXPRESSION")
launch { EhDB } launch { EhDB }
launch { dataStateFlow.value } launch { dataStateFlow.value }
launch { OSUtils.totalMemory } launch { OSUtils.totalMemory }
@@ -197,7 +196,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
add(GifDecoder.Factory()) add(GifDecoder.Factory())
} }
} }
diskCache { imageCache } diskCache { thumbCache }
crossfade(300) crossfade(300)
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed) val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
if (drawable != null) error(drawable.asImage(true)) if (drawable != null) error(drawable.asImage(true))
@@ -220,7 +219,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
private set private set
val ktorClient by lazy { val ktorClient by lazy {
if (isCronetAvailable && Settings.enableCronet.value) { if (isCronetAvailable && Settings.enableQuic.value) {
HttpClient(Cronet) { HttpClient(Cronet) {
engine { configureClient(Settings.enableQuic.value) } engine { configureClient(Settings.enableQuic.value) }
configureCommon() configureCommon()
@@ -239,10 +238,10 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
} }
} }
val imageCache by lazy { val thumbCache by lazy {
diskCache { diskCache {
directory(appCtx.cacheDir.toOkioPath() / "image_cache") directory(appCtx.cacheDir.toOkioPath() / "thumb")
maxSizeBytes(Settings.readCacheSize.value.coerceIn(320, 5120).toLong() * 1024 * 1024) maxSizeBytes(80L * 1024 * 1024)
} }
} }
@@ -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
}
}
@@ -16,7 +16,6 @@ import com.ehviewer.core.preferences.edit
import com.hippo.ehviewer.client.data.FavListUrlBuilder import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.download.DownloadsFilterMode import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.download.SortMode import com.hippo.ehviewer.download.SortMode
import com.hippo.ehviewer.util.AppConfig
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import java.util.Locale import java.util.Locale
@@ -112,7 +111,7 @@ object Settings : DataStorePreferences(null) {
val saveCrashLog = boolPref("save_crash_log", true) val saveCrashLog = boolPref("save_crash_log", true)
val readCacheSize = intPref("read_cache_size_2", 640) val readCacheSize = intPref("read_cache_size_2", 640)
val enableCronet = boolPref("enable_cronet", true) 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 hardwareBitmapThreshold = intPref("hardware_bitmap_threshold", 16384)
val preloadThumbAggressively = boolPref("preload_thumb_aggressively", false) val preloadThumbAggressively = boolPref("preload_thumb_aggressively", false)
val animateItems = boolPref("animate_items", true) val animateItems = boolPref("animate_items", true)
@@ -120,8 +119,8 @@ object Settings : DataStorePreferences(null) {
// About // About
val backupBeforeUpdate = boolPref("backup_before_update", false) val backupBeforeUpdate = boolPref("backup_before_update", false)
val useCIUpdateChannel = boolPref("ci_update_channel", AppConfig.isSnapshot) val useCIUpdateChannel = boolPref("ci_update_channel", BuildConfig.SNAPSHOT)
val updateIntervalDays = intPref("update_interval_days", 7) val updateIntervalDays = intPref("update_interval_days", 0)
// Misc // Misc
val languageFilter = intPref("language_filter", -1) val languageFilter = intPref("language_filter", -1)
@@ -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()
}
@@ -124,7 +124,7 @@ private fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuff
{ !it.hasRemaining() }, { !it.hasRemaining() },
) )
if (empty) { if (empty) {
val message = if (EhUtils.isExHentai) { val message = if (response.request.url.host == EhUrl.DOMAIN_EX) {
"Sad Panda\n(without panda)" "Sad Panda\n(without panda)"
} else { } else {
appCtx.getString(R.string.error_ip_banned) appCtx.getString(R.string.error_ip_banned)
@@ -194,7 +194,12 @@ private suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline
object EhEngine { object EhEngine {
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response -> 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) 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 { object EhTagDatabase : CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job() override val coroutineContext = Dispatchers.IO + Job()
private const val NAMESPACE_PREFIX = "n" private const val NAMESPACE_PREFIX = "n"
private val PREFIXES = arrayOf( private val PREFIXES = ["-", "~", "tag:", "weak:"]
"-",
"~",
"tag:",
"weak:",
)
private lateinit var tagGroups: Map<String, Map<String, String>> private lateinit var tagGroups: Map<String, Map<String, String>>
private val updateLock = Mutex() private val updateLock = Mutex()
var initialized by mutableStateOf(false) var initialized by mutableStateOf(false)
@@ -107,14 +107,14 @@ object EhUrl {
else -> URL_IMAGE_SEARCH_E 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 (index != 0) addQueryParameter("p", "$index")
if (allComment) addQueryParameter("hc", "1") if (allComment) addQueryParameter("hc", "1")
}.buildString() }.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) addQueryParameterIfNotBlank("nl", nl)
}.buildString() }.buildString()
@@ -143,7 +143,7 @@ object EhUrl {
} }
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl( inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
path = if (path.isNullOrBlank()) emptyList() else listOf(path), path = if (path.isNullOrBlank()) [] else [path],
host = host, host = host,
parameters = Parameters.build(builder), parameters = Parameters.build(builder),
) )
@@ -35,7 +35,7 @@ data class ListUrlBuilder(
private var jumpTo: String? = null, private var jumpTo: String? = null,
private var range: Int = 0, private var range: Int = 0,
var category: Int = EhUtils.NONE, var category: Int = EhUtils.NONE,
private var mKeyword: String? = null, var keyword: String? = null,
var hash: String? = null, var hash: String? = null,
var language: Int = -1, var language: Int = -1,
var advanceSearch: Int = -1, var advanceSearch: Int = -1,
@@ -69,16 +69,10 @@ data class ListUrlBuilder(
jumpTo = date jumpTo = date
} }
var keyword: String?
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
set(keyword) {
mKeyword = keyword
}
constructor(q: QuickSearch) : this( constructor(q: QuickSearch) : this(
mode = q.mode, mode = q.mode,
category = q.category, category = q.category,
mKeyword = q.keyword, keyword = q.keyword,
advanceSearch = q.advanceSearch, advanceSearch = q.advanceSearch,
minRating = q.minRating, minRating = q.minRating,
pageFrom = q.pageFrom, pageFrom = q.pageFrom,
@@ -90,7 +84,7 @@ data class ListUrlBuilder(
name = name, name = name,
mode = mode, mode = mode,
category = category, category = category,
keyword = mKeyword, keyword = keyword,
advanceSearch = advanceSearch, advanceSearch = advanceSearch,
minRating = minRating, minRating = minRating,
pageFrom = pageFrom, pageFrom = pageFrom,
@@ -107,7 +101,7 @@ data class ListUrlBuilder(
if (q.category != this.category) { if (q.category != this.category) {
return false return false
} }
if (q.keyword != mKeyword) { if (q.keyword != keyword) {
return false return false
} }
if (q.advanceSearch != advanceSearch) { if (q.advanceSearch != advanceSearch) {
@@ -129,7 +123,7 @@ data class ListUrlBuilder(
jumpTo = params["seek"], jumpTo = params["seek"],
range = params["range"]?.toIntOrNull() ?: 0, range = params["range"]?.toIntOrNull() ?: 0,
category = params["f_cats"]?.toIntOrNull()?.let(EhUtils::invCategory) ?: EhUtils.NONE, category = params["f_cats"]?.toIntOrNull()?.let(EhUtils::invCategory) ?: EhUtils.NONE,
mKeyword = params["f_search"], keyword = params["f_search"],
) { ) {
if (params["advsearch"] == "1") { if (params["advsearch"] == "1") {
advanceSearch = 0 advanceSearch = 0
@@ -160,7 +154,7 @@ data class ListUrlBuilder(
if (category > 0) { if (category > 0) {
addQueryParameter("f_cats", "${EhUtils.invCategory(category)}") 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) { if (language == -1 || "gid:" in keyword || "l:" in keyword || "language:" in keyword) {
keyword keyword
} else { } else {
@@ -214,7 +208,7 @@ data class ListUrlBuilder(
}.buildString() }.buildString()
MODE_UPLOADER, MODE_TAG -> { MODE_UPLOADER, MODE_TAG -> {
val path = if (mode == MODE_UPLOADER) "uploader" else "tag" val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
ehUrl(listOf(path, requireNotNull(mKeyword))) { ehUrl([path, requireNotNull(keyword)]) {
addQueryParameterIfNotBlank("prev", prev) addQueryParameterIfNotBlank("prev", prev)
addQueryParameterIfNotBlank("next", next) addQueryParameterIfNotBlank("next", next)
addQueryParameterIfNotBlank("seek", jumpTo) addQueryParameterIfNotBlank("seek", jumpTo)
@@ -226,7 +220,7 @@ data class ListUrlBuilder(
addQueryParameter("f_shash", requireNotNull(hash)) addQueryParameter("f_shash", requireNotNull(hash))
}.buildString() }.buildString()
MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) { MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) {
addQueryParameter("tl", requireNotNull(mKeyword)) addQueryParameter("tl", requireNotNull(keyword))
addQueryParameterIfNotBlank("p", jumpTo) addQueryParameterIfNotBlank("p", jumpTo)
}.buildString() }.buildString()
else -> throw IllegalStateException("Unexpected value: $mode") else -> throw IllegalStateException("Unexpected value: $mode")
@@ -13,14 +13,14 @@ object GalleryListParser {
throw ParseException("Can't parse gallery list", it) throw ParseException("Can't parse gallery list", it)
} }
val emptyResult = GalleryListResult(null, null, arrayListOf()) val emptyResult = GalleryListResult(null, null, [])
} }
@Serializable @Serializable
data class GalleryListResult( data class GalleryListResult(
val prev: String?, val prev: String?,
val next: String?, val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>, val galleryInfoList: MutableList<BaseGalleryInfo>,
) )
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
@@ -48,7 +48,7 @@ object GalleryListUrlParser {
ListUrlBuilder( ListUrlBuilder(
mode = ListUrlBuilder.MODE_TOPLIST, mode = ListUrlBuilder.MODE_TOPLIST,
jumpTo = url.parameters["p"], jumpTo = url.parameters["p"],
mKeyword = tl, keyword = tl,
) )
} else { } else {
null null
@@ -12,7 +12,7 @@ import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.isFile import com.ehviewer.core.files.isFile
import com.ehviewer.core.files.sendTo import com.ehviewer.core.files.sendTo
import com.ehviewer.core.files.toUri 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.EhDB
import com.hippo.ehviewer.client.getThumbKey import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.download.downloadLocation import com.hippo.ehviewer.download.downloadLocation
@@ -53,7 +53,7 @@ object DownloadThumbInterceptor : Interceptor {
// Accessing the recreated file immediately after deleting it throws // Accessing the recreated file immediately after deleting it throws
// FileNotFoundException, so we just overwrite the existing file. // FileNotFoundException, so we just overwrite the existing file.
val key = requireNotNull(chain.request.memoryCacheKey) val key = requireNotNull(chain.request.memoryCacheKey)
imageCache.read(key) { thumbCache.read(key) {
data sendTo thumb data sendTo thumb
} }
if (thumb != v1Thumb) v1Thumb.delete() if (thumb != v1Thumb) v1Thumb.delete()
@@ -52,8 +52,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class DownloadService : class DownloadService :
@@ -375,8 +373,8 @@ class DownloadService :
} }
suspend fun run() { suspend fun run() {
channel.receiveAsFlow().sample(DELAY).collect { while (true) {
when (it) { when (channel.receive()) {
Ops.Notify -> { Ops.Notify -> {
if (ActivityCompat.checkSelfPermission( if (ActivityCompat.checkSelfPermission(
service, service,
@@ -389,6 +387,7 @@ class DownloadService :
Ops.Cancel -> notifyManager.cancel(id) Ops.Cancel -> notifyManager.cancel(id)
Ops.StartForeground -> service.startForeground(id, builder.build()) 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) return SortMode(field, order, groupByDownloadLabel)
} }
val Default = SortMode(Field.TIME, Order.DESC) val Default = SortMode(Field.TIME, Order.DESC)
val All = listOf( val All = [
Default, Default,
SortMode(Field.TIME, Order.ASC), SortMode(Field.TIME, Order.ASC),
SortMode(Field.GID, Order.DESC), 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.TITLE, Order.DESC),
SortMode(Field.PAGES, Order.ASC), SortMode(Field.PAGES, Order.ASC),
SortMode(Field.PAGES, Order.DESC), SortMode(Field.PAGES, Order.DESC),
) ]
} }
} }
@@ -32,8 +32,9 @@ import coil3.request.CachePolicy
import coil3.request.ErrorResult import coil3.request.ErrorResult
import coil3.request.SuccessResult import coil3.request.SuccessResult
import coil3.request.allowHardware import coil3.request.allowHardware
import coil3.size.Dimension import coil3.request.maxBitmapSize
import coil3.size.Precision import coil3.size.Precision
import coil3.size.Scale
import coil3.size.Size import coil3.size.Size
import coil3.size.SizeResolver import coil3.size.SizeResolver
import com.ehviewer.core.files.openFileDescriptor import com.ehviewer.core.files.openFileDescriptor
@@ -90,8 +91,10 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
} }
companion object { companion object {
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3 private val sizeResolver = with(appCtx.resources.displayMetrics) {
private val sizeResolver = SizeResolver(Size(targetWidth, Dimension.Undefined)) val targetSize = minOf(widthPixels, heightPixels) * 4 / 3
SizeResolver(Size(targetSize, targetSize))
}
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage { private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
val request = with(appCtx) { val request = with(appCtx) {
@@ -99,7 +102,9 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
onLeft { data(it.source) } onLeft { data(it.source) }
onRight { data(it.source.toUri()) } onRight { data(it.source.toUri()) }
size(sizeResolver) size(sizeResolver)
scale(Scale.FILL)
precision(Precision.INEXACT) precision(Precision.INEXACT)
maxBitmapSize(Size.ORIGINAL)
allowHardware(false) allowHardware(false)
hardwareThreshold(Settings.hardwareBitmapThreshold.value) hardwareThreshold(Settings.hardwareBitmapThreshold.value)
maybeCropBorder(Settings.cropBorder.value) maybeCropBorder(Settings.cropBorder.value)
@@ -29,7 +29,7 @@ fun <T : HttpClientEngineConfig> HttpClientConfig<T>.configureCommon(redirect: B
} }
install(HttpTimeout) { install(HttpTimeout) {
reset() reset()
requestTimeoutMillis = 10_000 requestTimeoutMillis = 30_000
} }
install(UserAgent) install(UserAgent)
defaultRequest { defaultRequest {
@@ -32,20 +32,21 @@ import java.nio.ByteBuffer
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.job import kotlinx.coroutines.job
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") { class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
// Limit thread to 1 since we are async & non-blocking // 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) override val supportedCapabilities = setOf(HttpTimeoutCapability)
private val executor = dispatcher.asExecutor() private val executor = dispatcher.limitedParallelism(1).asExecutor()
private val pool = DirectByteBufferPool(32) private val pool = DirectByteBufferPool(32)
private val client by lazy { private val client by lazy {
with(config) { HttpEngine.Builder(context).apply(config).build() } with(config) { HttpEngine.Builder(context).apply(config).build() }
@@ -92,7 +93,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
} }
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) { override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
chunkChan.trySend(byteBuffer).getOrThrow() chunkChan.trySendBlocking(byteBuffer).getOrThrow()
} }
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) { override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
@@ -1,14 +1,18 @@
package com.hippo.ehviewer.ktor 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 io.ktor.client.engine.okhttp.OkHttpConfig
import okhttp3.AsyncDns import java.net.Proxy
import okhttp3.android.AndroidAsyncDns
fun OkHttpConfig.configureClient() { fun OkHttpConfig.configureClient() {
config { config {
if (isAtLeastQ) { dns(EhDns)
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6)) proxy(Proxy.NO_PROXY)
if (!Settings.enableQuic.value) {
install(EhSSLSocketFactory)
} }
} }
} }
@@ -43,7 +43,7 @@ private val xml = XML {
} }
xmlDeclMode = XmlDeclMode.Charset xmlDeclMode = XmlDeclMode.Charset
xmlVersion = XmlVersion.XML10 xmlVersion = XmlVersion.XML10
indent = 2 setIndent(2)
} }
fun GalleryInfo.getComicInfo(): ComicInfo { fun GalleryInfo.getComicInfo(): ComicInfo {
@@ -33,7 +33,6 @@ import com.ehviewer.core.files.sendTo
import com.ehviewer.core.model.GalleryDetail import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.logcat import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine 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.download.tempDownloadDir
import com.hippo.ehviewer.image.PathSource import com.hippo.ehviewer.image.PathSource
import com.hippo.ehviewer.jni.archiveFdBatch import com.hippo.ehviewer.jni.archiveFdBatch
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.util.FileUtils import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.copyTo import com.hippo.ehviewer.util.copyTo
import com.hippo.ehviewer.util.sha1 import com.hippo.ehviewer.util.sha1
@@ -57,6 +57,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read import kotlin.concurrent.read
import kotlin.concurrent.write import kotlin.concurrent.write
import okio.Path import okio.Path
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
class SpiderDen(val info: GalleryInfo) { class SpiderDen(val info: GalleryInfo) {
private val gid = info.gid private val gid = info.gid
@@ -151,6 +153,10 @@ class SpiderDen(val info: GalleryInfo) {
removeTempFile(index) removeTempFile(index)
} }
fun clearCache() {
repeat(info.pages, ::removeFromCache)
}
private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) { private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) {
val name = perFilename(index, extension) val name = perFilename(index, extension)
read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } } read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } }
@@ -328,7 +334,7 @@ class SpiderDen(val info: GalleryInfo) {
downloadDir?.run { downloadDir?.run {
resolve(COMIC_INFO_FILE).also { resolve(COMIC_INFO_FILE).also {
if (info !is GalleryDetail && fetchMetadata) { if (info !is GalleryDetail && fetchMetadata) {
EhEngine.fillGalleryListByApi(listOf(info)) EhEngine.fillGalleryListByApi([info])
} }
info.getComicInfo().apply { info.getComicInfo().apply {
writeComicInfo(this, it) writeComicInfo(this, it)
@@ -342,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 const val TEMP_SUFFIX = ".tmp"
private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}") private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
private val FileHashRegex = Regex("/h/([0-9a-f]{40})") private val FileHashRegex = Regex("/h/([0-9a-f]{40})")
@@ -594,6 +594,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
previousPToken, previousPToken,
) )
}.getOrElse { }.getOrElse {
if (it is FatalException) throw it
forceHtml = true forceHtml = true
return@repeat return@repeat
}.let { }.let {
@@ -394,7 +394,7 @@ suspend fun showMoveDownloadLabel(info: GalleryInfo) {
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title) val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
val label = if (selected == 0) null else labels[selected] val label = if (selected == 0) null else labels[selected]
DownloadManager.changeLabel(listOf(downloadInfo), label) DownloadManager.changeLabel([downloadInfo], label)
} }
context(_: DialogState) 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.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
@@ -63,7 +66,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberDrawerState2 import androidx.compose.material3.rememberDrawerState2
import androidx.compose.runtime.Composable 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp import androidx.compose.ui.util.lerp
import androidx.compose.ui.zIndex
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.LifecycleResumeEffect 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.LocalSnackBarFabPadding
import com.ehviewer.core.ui.util.LocalWindowSizeClass import com.ehviewer.core.ui.util.LocalWindowSizeClass
import com.ehviewer.core.util.isAtLeastQ import com.ehviewer.core.util.isAtLeastQ
import com.ehviewer.core.util.isAtLeastR
import com.ehviewer.core.util.isAtLeastS import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.withIOContext import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhApplication.Companion.initialized 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.spec.Direction
import com.ramcosta.composedestinations.utils.currentDestinationAsState import com.ramcosta.composedestinations.utils.currentDestinationAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import eu.kanade.tachiyomi.util.view.setSecureScreen
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -175,7 +181,7 @@ private val navItems = arrayOf<Triple<Direction, Int, ImageVector>>(
Triple(SettingsScreenDestination, R.string.settings, Icons.Default.Settings), Triple(SettingsScreenDestination, R.string.settings, Icons.Default.Settings),
) )
class MainActivity : EhActivity() { class MainActivity : AppCompatActivity() {
private var shareUrl: String? = null private var shareUrl: String? = null
@@ -203,6 +209,15 @@ class MainActivity : EhActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen().setKeepOnScreenCondition { !initialized } installSplashScreen().setKeepOnScreenCondition { !initialized }
super.onCreate(savedInstanceState) 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 { setMD3Content {
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val navDrawerState = rememberDrawerState(DrawerValue.Closed) 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) val cannotParse = stringResource(R.string.error_cannot_parse_the_url)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
intentFlow.collect { intent -> intentFlow.collect { intent ->
@@ -276,7 +300,7 @@ class MainActivity : EhActivity() {
if ("text/plain" == type) { if ("text/plain" == type) {
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT) val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
if (keyword != null && !navWithUrl(keyword)) { if (keyword != null && !navWithUrl(keyword)) {
navigate(ListUrlBuilder(mKeyword = keyword).asDst()) navigate(ListUrlBuilder(keyword = keyword).asDst())
} }
} else if (type != null && type.startsWith("image/")) { } else if (type != null && type.startsWith("image/")) {
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM) val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
@@ -356,7 +380,7 @@ class MainActivity : EhActivity() {
var snackbarFabPadding by remember { mutableStateOf(0.dp) } var snackbarFabPadding by remember { mutableStateOf(0.dp) }
val drawerEnabled = drawerHandle.isNotEmpty() val drawerEnabled = drawerHandle.isNotEmpty()
val density = LocalDensity.current val density = LocalDensity.current
val adaptiveInfo = currentWindowAdaptiveInfo() val adaptiveInfo = currentWindowAdaptiveInfoV2()
val needSignIn by Settings.needSignIn.collectAsState() val needSignIn by Settings.needSignIn.collectAsState()
val launchPage by Settings.launchPage.collectAsState() val launchPage by Settings.launchPage.collectAsState()
CompositionLocalProvider( 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.imageLoader
import coil3.request.crossfade import coil3.request.crossfade
import coil3.size.Scale import coil3.size.Scale
import coil3.size.Size
import com.ehviewer.core.util.toIntOrDefault import com.ehviewer.core.util.toIntOrDefault
import com.hippo.ehviewer.ktbuilder.imageRequest import com.hippo.ehviewer.ktbuilder.imageRequest
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -21,7 +22,7 @@ class CoilImageGetter(private val onSuccess: () -> Unit) : Html.ImageGetter {
val srcHeight = groupValues[2].toInt() val srcHeight = groupValues[2].toInt()
val dstWidth = groupValues[3].toIntOrDefault(200) val dstWidth = groupValues[3].toIntOrDefault(200)
val dstHeight = dstWidth / 2 * 3 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, (srcWidth * multiplier).roundToInt(), (srcHeight * multiplier).roundToInt())
} ?: setBounds(0, 0, 200, 300) } ?: setBounds(0, 0, 200, 300)
with(appCtx) { with(appCtx) {
@@ -31,6 +31,7 @@ fun AnimatedVisibilityScope.WebViewSignInScreen(navigator: DestinationsNavigator
Settings.desktopSite.value = false Settings.desktopSite.value = false
} }
if (EhCookieStore.hasSignedIn()) { if (EhCookieStore.hasSignedIn()) {
EhCookieStore.flush()
postLogin() postLogin()
state.webView?.destroy() state.webView?.destroy()
bgWork { awaitCancellation() } bgWork { awaitCancellation() }
@@ -47,7 +47,7 @@ import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitResult import com.hippo.ehviewer.ui.tools.awaitResult
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
private val categoryTable = arrayOf( private val categoryTable = [
EhUtils.DOUJINSHI to R.string.doujinshi, EhUtils.DOUJINSHI to R.string.doujinshi,
EhUtils.MANGA to R.string.manga, EhUtils.MANGA to R.string.manga,
EhUtils.ARTIST_CG to R.string.artist_cg, EhUtils.ARTIST_CG to R.string.artist_cg,
@@ -58,7 +58,7 @@ private val categoryTable = arrayOf(
EhUtils.COSPLAY to R.string.cosplay, EhUtils.COSPLAY to R.string.cosplay,
EhUtils.ASIAN_PORN to R.string.asian_porn, EhUtils.ASIAN_PORN to R.string.asian_porn,
EhUtils.MISC to R.string.misc, EhUtils.MISC to R.string.misc,
) ]
@Composable @Composable
context(_: DialogState, _: CoroutineScope, _: Context) context(_: DialogState, _: CoroutineScope, _: Context)
@@ -193,9 +193,6 @@ fun SearchFilter(
} }
if (error != null) { if (error != null) {
ListItem( ListItem(
headlineContent = {
Text(text = error, style = MaterialTheme.typography.bodySmall)
},
leadingContent = { leadingContent = {
Icon(imageVector = Icons.Default.Info, contentDescription = null) Icon(imageVector = Icons.Default.Info, contentDescription = null)
}, },
@@ -204,6 +201,9 @@ fun SearchFilter(
headlineColor = MaterialTheme.colorScheme.error, headlineColor = MaterialTheme.colorScheme.error,
leadingIconColor = 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.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -23,6 +19,8 @@ import androidx.core.graphics.green
import androidx.core.graphics.red import androidx.core.graphics.red
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.component.RollingNumber 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.Settings
import com.hippo.ehviewer.asMutableState import com.hippo.ehviewer.asMutableState
@@ -34,12 +32,11 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
field = customBrightness, field = customBrightness,
) )
AnimatedVisibility(visible = customBrightness.value) { AnimatedVisibility(visible = customBrightness.value) {
val brightness = Settings.customBrightnessValue.asMutableState() val brightness = rememberIntPrefSliderState(Settings.customBrightnessValue, -75..100)
SliderChoice( SliderChoice(
startSlot = { Icon(imageVector = Icons.Default.Brightness5, contentDescription = null) }, startSlot = { Icon(imageVector = Icons.Default.Brightness5, contentDescription = null) },
endSlot = { RollingNumber(number = brightness.value, length = 3) }, endSlot = { RollingNumber(number = brightness.value, length = 3) },
range = -75..100, state = brightness,
field = brightness,
) )
} }
val colorFilter = Settings.colorFilter.asMutableState() val colorFilter = Settings.colorFilter.asMutableState()
@@ -48,47 +45,39 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
field = colorFilter, field = colorFilter,
) )
AnimatedVisibility(visible = colorFilter.value) { AnimatedVisibility(visible = colorFilter.value) {
var color by Settings.colorFilterValue.asMutableState() var color by Settings.colorFilterValue
val rf = remember { mutableIntStateOf(color.red) } val r = rememberIntSliderState(color.red, 0..255)
val r by rf val g = rememberIntSliderState(color.green, 0..255)
val gf = remember { mutableIntStateOf(color.green) } val b = rememberIntSliderState(color.blue, 0..255)
val g by gf val a = rememberIntSliderState(color.alpha, 0..255)
val bf = remember { mutableIntStateOf(color.blue) } color = (a.value shl 24) or (r.value shl 16) or (g.value shl 8) or (b.value)
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)
Column { Column {
SliderChoice( SliderChoice(
startSlot = { Text(text = "R") }, startSlot = { Text(text = "R") },
endSlot = { RollingNumber(number = r, length = 3) }, endSlot = { RollingNumber(number = r.value, length = 3) },
range = 0..255, state = r,
field = rf,
) )
SliderChoice( SliderChoice(
startSlot = { Text(text = "G") }, startSlot = { Text(text = "G") },
endSlot = { RollingNumber(number = g, length = 3) }, endSlot = { RollingNumber(number = g.value, length = 3) },
range = 0..255, state = g,
field = gf,
) )
SliderChoice( SliderChoice(
startSlot = { Text(text = "B") }, startSlot = { Text(text = "B") },
endSlot = { RollingNumber(number = b, length = 3) }, endSlot = { RollingNumber(number = b.value, length = 3) },
range = 0..255, state = b,
field = bf,
) )
SliderChoice( SliderChoice(
startSlot = { Text(text = "A") }, startSlot = { Text(text = "A") },
endSlot = { RollingNumber(number = a, length = 3) }, endSlot = { RollingNumber(number = a.value, length = 3) },
range = 0..255, state = a,
field = af,
) )
} }
} }
SpinnerChoice( SpinnerChoice(
title = stringResource(id = R.string.pref_color_filter_mode), title = stringResource(id = R.string.pref_color_filter_mode),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.color_filter_modes), entries = stringArrayResource(id = com.hippo.ehviewer.R.array.color_filter_modes),
values = listOf(0, 1, 2, 3, 4, 5), values = [0, 1, 2, 3, 4, 5],
field = Settings.colorFilterMode.asMutableState(), field = Settings.colorFilterMode.asMutableState(),
) )
SwitchChoice( SwitchChoice(
@@ -1,6 +1,11 @@
package com.hippo.ehviewer.ui.reader package com.hippo.ehviewer.ui.reader
import android.graphics.drawable.Animatable 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.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.WavyProgressIndicatorDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect 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.gallery.statusObserved
import com.hippo.ehviewer.image.Image import com.hippo.ehviewer.image.Image
import com.hippo.ehviewer.util.AdsPlaceholderFile import com.hippo.ehviewer.util.AdsPlaceholderFile
import eu.kanade.tachiyomi.ui.reader.viewer.CombinedCircularProgressIndicator
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
@@ -83,7 +89,22 @@ fun PagerItem(
modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT), modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT),
contentAlignment = Alignment.Center, 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 -> { is PageStatus.Ready -> {
@@ -58,7 +58,7 @@ private fun PagerSetting() = Column {
SpinnerChoice( SpinnerChoice(
title = stringResource(id = R.string.pref_viewer_nav), title = stringResource(id = R.string.pref_viewer_nav),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.pager_nav), entries = stringArrayResource(id = com.hippo.ehviewer.R.array.pager_nav),
values = listOf(0, 1, 2, 3, 4, 5), values = [0, 1, 2, 3, 4, 5],
field = pagerNav, field = pagerNav,
) )
AnimatedVisibility(visible = pagerNav.value != 5) { AnimatedVisibility(visible = pagerNav.value != 5) {
@@ -66,7 +66,7 @@ private fun PagerSetting() = Column {
SpinnerChoice( SpinnerChoice(
title = stringResource(id = R.string.pref_read_with_tapping_inverted), title = stringResource(id = R.string.pref_read_with_tapping_inverted),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode), entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
values = listOf(0, 1, 2, 3), values = [0, 1, 2, 3],
field = Settings.readerPagerNavInverted.asMutableState(), field = Settings.readerPagerNavInverted.asMutableState(),
) )
SwitchChoice( SwitchChoice(
@@ -79,7 +79,7 @@ private fun PagerSetting() = Column {
SpinnerChoice( SpinnerChoice(
title = stringResource(id = R.string.pref_image_scale_type), title = stringResource(id = R.string.pref_image_scale_type),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.image_scale_type), entries = stringArrayResource(id = com.hippo.ehviewer.R.array.image_scale_type),
values = listOf(1, 2, 3, 4, 5, 6), values = [1, 2, 3, 4, 5, 6],
field = scaleType, field = scaleType,
) )
AnimatedVisibility(visible = scaleType.value == 1) { AnimatedVisibility(visible = scaleType.value == 1) {
@@ -91,7 +91,7 @@ private fun PagerSetting() = Column {
SpinnerChoice( SpinnerChoice(
title = stringResource(id = R.string.pref_zoom_start), title = stringResource(id = R.string.pref_zoom_start),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.zoom_start), entries = stringArrayResource(id = com.hippo.ehviewer.R.array.zoom_start),
values = listOf(1, 2, 3, 4), values = [1, 2, 3, 4],
field = Settings.zoomStart.asMutableState(), field = Settings.zoomStart.asMutableState(),
) )
SwitchChoice( SwitchChoice(
@@ -111,14 +111,14 @@ private fun WebtoonSetting() = Column {
SpinnerChoice( SpinnerChoice(
title = stringResource(id = R.string.pref_viewer_nav), title = stringResource(id = R.string.pref_viewer_nav),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.webtoon_nav), entries = stringArrayResource(id = com.hippo.ehviewer.R.array.webtoon_nav),
values = listOf(0, 1, 2, 3, 4, 5), values = [0, 1, 2, 3, 4, 5],
field = webtoonNav, field = webtoonNav,
) )
AnimatedVisibility(visible = webtoonNav.value != 5) { AnimatedVisibility(visible = webtoonNav.value != 5) {
SpinnerChoice( SpinnerChoice(
title = stringResource(id = R.string.pref_read_with_tapping_inverted), title = stringResource(id = R.string.pref_read_with_tapping_inverted),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode), entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
values = listOf(0, 1, 2, 3), values = [0, 1, 2, 3],
field = Settings.readerWebtoonNavInverted.asMutableState(), field = Settings.readerWebtoonNavInverted.asMutableState(),
) )
} }
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.BottomSheetDefaults
@@ -26,9 +25,10 @@ import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetValue
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -92,6 +92,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderContentOverlay
import eu.kanade.tachiyomi.ui.reader.ReaderPageSheetMeta import eu.kanade.tachiyomi.ui.reader.ReaderPageSheetMeta
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@@ -214,9 +215,9 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
uiController.showTransientSystemBarsBySwipe = false 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 pagerState = rememberPagerState(pageLoader.startPage) { pageLoader.size }
val syncState = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader) val sync = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
var appbarVisible by remember { mutableStateOf(false) } var appbarVisible by remember { mutableStateOf(false) }
val isWebtoon by rememberUpdatedState(ReadingModeType.isWebtoon(readingMode)) val isWebtoon by rememberUpdatedState(ReadingModeType.isWebtoon(readingMode))
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@@ -231,7 +232,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
syncState.Sync(isWebtoon) { appbarVisible = false } sync.Sync(isWebtoon) { appbarVisible = false }
val bgColor by collectBackgroundColorAsState() val bgColor by collectBackgroundColorAsState()
val isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
LaunchedEffect(isDarkTheme) { LaunchedEffect(isDarkTheme) {
@@ -251,7 +252,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
val blocked = page.status is PageStatus.Blocked val blocked = page.status is PageStatus.Blocked
dialog { cont -> dialog { cont ->
fun dispose() = cont.resume(Unit) fun dispose() = cont.resume(Unit)
val state = rememberModalBottomSheetState() val state = rememberBottomSheetState(SheetValue.Hidden)
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { dispose() }, onDismissRequest = { dispose() },
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)), modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
@@ -292,10 +293,10 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
onNavigationModeChange = { showNavigationOverlay = true }, onNavigationModeChange = { showNavigationOverlay = true },
onSelectPage = onSelectPage, onSelectPage = onSelectPage,
onMenuRegionClick = { appbarVisible = !appbarVisible }, onMenuRegionClick = { appbarVisible = !appbarVisible },
modifier = Modifier.background(bgColor).pointerInput(syncState) { modifier = Modifier.background(bgColor).pointerInput(sync) {
awaitEachGesture { awaitEachGesture {
waitForUpOrCancellation() waitForUpOrCancellation()
syncState.reset() sync.reset()
showNavigationOverlay = false showNavigationOverlay = false
} }
}.fillMaxSize().windowInsetsPadding(insets), }.fillMaxSize().windowInsetsPadding(insets),
@@ -323,7 +324,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
) )
if (brightness) { if (brightness) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
Settings.customBrightnessValue.valueFlow().sample(100) Settings.customBrightnessValue.valueFlow().sample(100.milliseconds)
.onCompletion { activity.setCustomBrightnessValue(0) } .onCompletion { activity.setCustomBrightnessValue(0) }
.collect { activity.setCustomBrightnessValue(it) } .collect { activity.setCustomBrightnessValue(it) }
} }
@@ -332,7 +333,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
if (showPageNumber && !appbarVisible) { if (showPageNumber && !appbarVisible) {
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) { CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
PageIndicatorText( PageIndicatorText(
currentPage = syncState.sliderValue, currentPage = sync.slider.value,
totalPages = pageLoader.size, totalPages = pageLoader.size,
modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(), modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
) )
@@ -343,9 +344,9 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
title = pageLoader.title, title = pageLoader.title,
isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT, isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT,
showSeekBar = showSeekbar, showSeekBar = showSeekbar,
currentPage = syncState.sliderValue, slider = sync.slider,
totalPages = pageLoader.size, totalPages = pageLoader.size,
onSliderValueChange = syncState::sliderScrollTo, onSliderValueChange = sync::sliderScrollTo,
onClickSettings = { onClickSettings = {
launch { launch {
dialog { cont -> dialog { cont ->
@@ -6,11 +6,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import com.ehviewer.core.ui.component.IntSliderState
import com.hippo.ehviewer.gallery.PageLoader import com.hippo.ehviewer.gallery.PageLoader
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
@@ -23,12 +23,11 @@ class SliderPagerDoubleSync(
private val pageLoader: PageLoader, private val pageLoader: PageLoader,
) { ) {
private var sliderFollowPager by mutableStateOf(true) private var sliderFollowPager by mutableStateOf(true)
var sliderValue by mutableIntStateOf(pageLoader.startPage + 1) val slider = IntSliderState(pageLoader.startPage + 1, 1..pageLoader.size)
private set
fun sliderScrollTo(index: Int) { fun sliderScrollTo(index: Int) {
sliderFollowPager = false sliderFollowPager = false
sliderValue = index.coerceIn(1, pageLoader.size) slider.value = index
} }
fun reset() { fun reset() {
@@ -50,7 +49,7 @@ class SliderPagerDoubleSync(
@Composable @Composable
fun Sync(webtoon: Boolean, onPageSelected: () -> Unit) { fun Sync(webtoon: Boolean, onPageSelected: () -> Unit) {
val currentIndexFlow = remember(webtoon) { val currentIndexFlow = remember(webtoon) {
val initialIndex = sliderValue - 1 val initialIndex = slider.value - 1
sliderFollowPager = if (webtoon) { sliderFollowPager = if (webtoon) {
lazyListState.firstVisibleItemIndex == initialIndex lazyListState.firstVisibleItemIndex == initialIndex
} else { } else {
@@ -61,14 +60,14 @@ class SliderPagerDoubleSync(
if (sliderFollowPager) { if (sliderFollowPager) {
LaunchedEffect(currentIndexFlow) { LaunchedEffect(currentIndexFlow) {
currentIndexFlow.drop(1).collect { index -> currentIndexFlow.drop(1).collect { index ->
sliderValue = index + 1 slider.value = index + 1
pageLoader.startPage = index pageLoader.startPage = index
onPageSelected() onPageSelected()
} }
} }
} else { } else {
LaunchedEffect(webtoon) { LaunchedEffect(webtoon) {
snapshotFlow { sliderValue - 1 }.collectLatest { index -> snapshotFlow { slider.value - 1 }.collectLatest { index ->
if (webtoon) { if (webtoon) {
lazyListState.scrollToItem(index) lazyListState.scrollToItem(index)
} else { } else {
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -81,6 +82,7 @@ fun WebtoonViewer(
state = lazyListState, state = lazyListState,
contentPadding = PaddingValues(horizontal = sidePadding), contentPadding = PaddingValues(horizontal = sidePadding),
verticalArrangement = Arrangement.spacedBy(if (withGaps) 15.dp else 0.dp), verticalArrangement = Arrangement.spacedBy(if (withGaps) 15.dp else 0.dp),
cacheWindow = LazyLayoutCacheWindow(SCROLL_FRACTION, SCROLL_FRACTION),
) { ) {
items(items, key = { it.index }) { page -> items(items, key = { it.index }) { page ->
PagerItem( PagerItem(
@@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.IntSliderState
import com.ehviewer.core.ui.component.Slider import com.ehviewer.core.ui.component.Slider
@Composable @Composable
@@ -84,20 +85,17 @@ fun SwitchChoice(title: String, field: MutableState<Boolean>) {
fun SliderChoice( fun SliderChoice(
startSlot: @Composable () -> Unit, startSlot: @Composable () -> Unit,
endSlot: @Composable () -> Unit, endSlot: @Composable () -> Unit,
range: IntRange, state: IntSliderState,
field: MutableState<Int>,
) { ) {
var value by field
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
startSlot() startSlot()
Slider( Slider(
value = value, state = state,
onValueChange = { value = it }, onValueChange = { state.value = it },
modifier = Modifier.weight(1f).padding(8.dp), modifier = Modifier.weight(1f).padding(8.dp),
valueRange = range,
) )
endSlot() endSlot()
} }
@@ -159,7 +159,6 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
) { ) {
faves.forEachIndexed { index, (name, count) -> faves.forEachIndexed { index, (name, count) ->
ListItem( ListItem(
headlineContent = { Text(text = name) },
trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) }, trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) },
modifier = Modifier.clip(CardDefaults.shape).clickable { modifier = Modifier.clip(CardDefaults.shape).clickable {
val newCat = index - 2 val newCat = index - 2
@@ -169,6 +168,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
launch { sheetState.close() } launch { sheetState.close() }
}, },
colors = listItemOnDrawerColor(urlBuilder.favCat == index - 2), colors = listItemOnDrawerColor(urlBuilder.favCat == index - 2),
content = { Text(text = name) },
) )
} }
} }
@@ -260,12 +260,8 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
Column { Column {
data.forEach { (name, vote) -> data.forEach { (name, vote) ->
ListItem( ListItem(
headlineContent = { trailingContent = { Text(text = vote) },
Text(text = name) content = { Text(text = name) },
},
trailingContent = {
Text(text = vote)
},
) )
} }
} }
@@ -409,7 +405,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
navigate( navigate(
ListUrlBuilder( ListUrlBuilder(
mode = ListUrlBuilder.MODE_UPLOADER, mode = ListUrlBuilder.MODE_UPLOADER,
mKeyword = item.user, keyword = item.user,
).asDst(), ).asDst(),
) )
}, },
@@ -215,7 +215,7 @@ fun GalleryDetailContent(
if (uploader.isNullOrEmpty() || disowned) { if (uploader.isNullOrEmpty() || disowned) {
return return
} }
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, mKeyword = uploader).asDst()) navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, keyword = uploader).asDst())
} }
fun onGalleryInfoCardClick() { fun onGalleryInfoCardClick() {
@@ -529,21 +529,21 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
navigate( navigate(
ListUrlBuilder( ListUrlBuilder(
mode = ListUrlBuilder.MODE_NORMAL, mode = ListUrlBuilder.MODE_NORMAL,
mKeyword = "\"" + keyword + "\"", keyword = "\"" + keyword + "\"",
).asDst(), ).asDst(),
) )
} else if (artistTag != null) { } else if (artistTag != null) {
navigate( navigate(
ListUrlBuilder( ListUrlBuilder(
mode = ListUrlBuilder.MODE_TAG, mode = ListUrlBuilder.MODE_TAG,
mKeyword = artistTag, keyword = artistTag,
).asDst(), ).asDst(),
) )
} else if (null != galleryDetail.uploader) { } else if (null != galleryDetail.uploader) {
navigate( navigate(
ListUrlBuilder( ListUrlBuilder(
mode = ListUrlBuilder.MODE_UPLOADER, mode = ListUrlBuilder.MODE_UPLOADER,
mKeyword = galleryDetail.uploader, keyword = galleryDetail.uploader,
).asDst(), ).asDst(),
) )
} }
@@ -721,7 +721,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
val upTag = stringResource(R.string.tag_vote_up) val upTag = stringResource(R.string.tag_vote_up)
val downTag = stringResource(R.string.tag_vote_down) val downTag = stringResource(R.string.tag_vote_down)
val withDraw = stringResource(R.string.tag_vote_withdraw) val withDraw = stringResource(R.string.tag_vote_withdraw)
fun search(tag: String) = navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, mKeyword = tag).asDst()) fun search(tag: String) = navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, keyword = tag).asDst())
GalleryTags( GalleryTags(
tagGroups = tags, tagGroups = tags,
onTagClick = ::search, onTagClick = ::search,
@@ -49,14 +49,12 @@ import com.ehviewer.core.ui.util.rememberInVM
import com.ehviewer.core.util.launchIO import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.logcat import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withIOContext import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.fillInfo import com.hippo.ehviewer.client.data.fillInfo
import com.hippo.ehviewer.client.getImageKey
import com.hippo.ehviewer.coil.justDownload import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ktbuilder.executeIn 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)) Text(text = stringResource(id = R.string.clear_image_cache_confirm))
} }
repeat(gd.pages) { SpiderDen(gd).clearCache()
val key = getImageKey(gd.gid, it)
imageCache.remove(key)
}
snackbar(cacheCleared) snackbar(cacheCleared)
} }
}, },
@@ -147,7 +147,7 @@ fun AnimatedVisibilityScope.WhatshotScreen(navigator: DestinationsNavigator) = G
@Destination<RootGraph> @Destination<RootGraph>
@Composable @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> @Destination<RootGraph>
@Composable @Composable
@@ -172,10 +172,14 @@ fun AnimatedVisibilityScope.GalleryListScreen(
LaunchedEffect(urlBuilder) { LaunchedEffect(urlBuilder) {
if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category
var keyword = urlBuilder.keyword.takeUnless { urlBuilder.mode == MODE_TOPLIST }.orEmpty() val keyword = urlBuilder.keyword?.let { keyword ->
if (urlBuilder.mode == MODE_TAG) { when (urlBuilder.mode) {
keyword = wrapTagKeyword(keyword) MODE_UPLOADER -> "uploader:\"$keyword\""
} MODE_TAG -> wrapTagKeyword(keyword)
MODE_TOPLIST -> ""
else -> keyword
}
}.orEmpty()
searchFieldState.setTextAndPlaceCursorAtEnd(keyword) searchFieldState.setTextAndPlaceCursorAtEnd(keyword)
} }
@@ -217,15 +221,13 @@ fun AnimatedVisibilityScope.GalleryListScreen(
ListItem( ListItem(
modifier = Modifier.padding(horizontal = 4.dp).clip(CardDefaults.shape).clickable { modifier = Modifier.padding(horizontal = 4.dp).clip(CardDefaults.shape).clickable {
Settings.recentToplist = keyword Settings.recentToplist = keyword
urlBuilder = ListUrlBuilder(MODE_TOPLIST, mKeyword = keyword) urlBuilder = ListUrlBuilder(MODE_TOPLIST, keyword = keyword)
data.refresh() data.refresh()
fabHidden = false fabHidden = false
launch { sheetState.close() } launch { sheetState.close() }
}, },
headlineContent = {
Text(text = name)
},
colors = listItemOnDrawerColor(urlBuilder.keyword == keyword), colors = listItemOnDrawerColor(urlBuilder.keyword == keyword),
content = { Text(text = name) },
) )
} }
} }
@@ -480,9 +482,9 @@ fun AnimatedVisibilityScope.GalleryListScreen(
searchFieldState = searchFieldState, searchFieldState = searchFieldState,
suggestionProvider = { suggestionProvider = {
GalleryDetailUrlParser.parse(it, false)?.run { GalleryDetailUrlParser.parse(it, false)?.run {
listOf(GalleryDetailUrlSuggestion(gid, token)) [GalleryDetailUrlSuggestion(gid, token)]
} ?: GalleryPageUrlParser.parse(it, false)?.run { } ?: GalleryPageUrlParser.parse(it, false)?.run {
listOf(GalleryPageUrlSuggestion(gid, pToken, page)) [GalleryPageUrlSuggestion(gid, pToken, page)]
}.orEmpty() }.orEmpty()
}, },
localSearch = false, localSearch = false,
@@ -578,7 +580,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
onExpandChanged = { fabExpanded = it }, onExpandChanged = { fabExpanded = it },
autoCancel = true, 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) { onClick(Icons.Default.Shuffle) {
urlBuilder.setRange(Random.nextInt(100)) urlBuilder.setRange(Random.nextInt(100))
data.refresh() data.refresh()
@@ -641,6 +643,7 @@ private fun getSuitableTitleForUrlBuilder(urlBuilder: ListUrlBuilder, appName: B
val canTranslate = Settings.showTagTranslations.value && EhTagDatabase.translatable && EhTagDatabase.initialized val canTranslate = Settings.showTagTranslations.value && EhTagDatabase.translatable && EhTagDatabase.initialized
wrapTagKeyword(keyword, canTranslate) wrapTagKeyword(keyword, canTranslate)
} }
MODE_UPLOADER -> "uploader:\"$keyword\""
else -> keyword else -> keyword
} }
} else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) { } else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) {
@@ -294,7 +294,6 @@ fun SearchBarScreen(
item {} item {}
items(mSuggestionList, key = { it.keyword.hashCode() * 31 + it.canDelete.hashCode() }) { items(mSuggestionList, key = { it.keyword.hashCode() * 31 + it.canDelete.hashCode() }) {
ListItem( ListItem(
headlineContent = { Text(text = it.keyword) },
supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) }, supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) },
leadingContent = it.canOpenDirectly.ifTrueThen { leadingContent = it.canOpenDirectly.ifTrueThen {
Icon( Icon(
@@ -312,6 +311,7 @@ fun SearchBarScreen(
}, },
colors = ListItemDefaults.colors(containerColor = Color.Transparent), colors = ListItemDefaults.colors(containerColor = Color.Transparent),
modifier = Modifier.clickable { it.onClick() }.thenIf(animateItems) { animateItem() }, 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.DialogState
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.updater.AppUpdater 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.AppConfig
import com.hippo.ehviewer.util.ReadableTime import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.displayString import com.hippo.ehviewer.util.displayString
@@ -49,8 +49,8 @@ import moe.tarsin.navigate
import moe.tarsin.snackbar import moe.tarsin.snackbar
import moe.tarsin.string import moe.tarsin.string
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}" const val REPO_URL = "https://codeberg.org/${BuildConfig.REPO_NAME}"
private const val RELEASE_URL = "$REPO_URL/releases" const val RELEASE_URL = "$REPO_URL/releases"
@Composable @Composable
@Stable @Stable
@@ -124,7 +124,7 @@ fun AnimatedVisibilityScope.AboutScreen(navigator: DestinationsNavigator) = Scre
} }
context(_: Context, _: DialogState) context(_: Context, _: DialogState)
suspend fun showNewVersion(release: Release) { suspend fun showNewVersion(release: EhRelease) {
awaitConfirmationOrCancel( awaitConfirmationOrCancel(
confirmText = R.string.download, confirmText = R.string.download,
title = R.string.new_version_available, title = R.string.new_version_available,
@@ -63,6 +63,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import java.io.File import java.io.File
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@@ -214,7 +215,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
step = 3, step = 3,
title = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold), title = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold),
summary = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold_summary), summary = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold_summary),
state = Settings.hardwareBitmapThreshold.asMutableState(), pref = Settings.hardwareBitmapThreshold,
) )
} }
SwitchPreference( SwitchPreference(
@@ -289,7 +290,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
EhDB.putLocalFavorites(result.galleryInfoList) EhDB.putLocalFavorites(result.galleryInfoList)
launchSnackbar(string(R.string.settings_advanced_backup_favorite_start, status)) launchSnackbar(string(R.string.settings_advanced_backup_favorite_start, status))
if (result.next != null) { if (result.next != null) {
delay(Settings.downloadDelay.value.toLong()) delay(Settings.downloadDelay.value.milliseconds)
favListUrlBuilder.setIndex(result.next, true) favListUrlBuilder.setIndex(result.next, true)
doBackup() doBackup()
} }
@@ -195,7 +195,7 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
maxValue = 10, maxValue = 10,
minValue = MIN_SPEED_LEVEL, minValue = MIN_SPEED_LEVEL,
title = stringResource(id = R.string.settings_download_timeout_speed), title = stringResource(id = R.string.settings_download_timeout_speed),
state = Settings.timeoutSpeed.asMutableState(), pref = Settings.timeoutSpeed,
display = ::speedLevelToSpeed, display = ::speedLevelToSpeed,
) )
val preloadImage = Settings.preloadImage.asMutableState() val preloadImage = Settings.preloadImage.asMutableState()
@@ -193,7 +193,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
minValue = 20, minValue = 20,
step = 7, step = 7,
title = stringResource(id = R.string.list_tile_thumb_size), title = stringResource(id = R.string.list_tile_thumb_size),
state = Settings.listThumbSize.asMutableState(), pref = Settings.listThumbSize,
) )
SimpleMenuPreferenceInt( SimpleMenuPreferenceInt(
title = stringResource(id = R.string.settings_eh_detail_size), title = stringResource(id = R.string.settings_eh_detail_size),
@@ -207,7 +207,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
maxValue = 10, maxValue = 10,
minValue = 1, minValue = 1,
title = stringResource(id = R.string.settings_eh_thumb_columns), title = stringResource(id = R.string.settings_eh_thumb_columns),
state = Settings.thumbColumns.asMutableState(), pref = Settings.thumbColumns,
) )
SwitchPreference( SwitchPreference(
title = stringResource(id = R.string.settings_eh_show_gallery_pages), title = stringResource(id = R.string.settings_eh_show_gallery_pages),
@@ -235,7 +235,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
minValue = -101, minValue = -101,
title = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold), title = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold),
summary = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold_summary), summary = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold_summary),
state = Settings.commentThreshold.asMutableState(), pref = Settings.commentThreshold,
) )
} }
if (EhTagDatabase.translatable) { if (EhTagDatabase.translatable) {
@@ -26,6 +26,7 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenu
import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
@@ -1,8 +1,6 @@
package com.hippo.ehviewer.ui.settings package com.hippo.ehviewer.ui.settings
import androidx.compose.animation.AnimatedVisibilityScope 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.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -12,14 +10,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.ui.Screen import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.main.NavigationIcon import com.hippo.ehviewer.ui.main.NavigationIcon
import com.hippo.ehviewer.ui.openBrowser 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.android.produceLibraries
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer 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.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -42,10 +41,19 @@ fun AnimatedVisibilityScope.LicenseScreen(navigator: DestinationsNavigator) = Sc
libraries = libraries, libraries = libraries,
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
contentPadding = paddingValues, contentPadding = paddingValues,
padding = LibraryDefaults.libraryPadding(licensePadding = LibraryDefaults.chipPadding(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp))), badges = LibraryBadges(funding = false),
textStyles = LibraryDefaults.libraryTextStyles(licensesTextStyle = MaterialTheme.typography.labelSmall), variant = LibrariesVariant.Refined,
onLibraryClick = { library -> onActionClick = { lib, kind ->
library.website?.let { openBrowser(it) } 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.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import com.ehviewer.core.preferences.PrefDelegate
import com.ehviewer.core.ui.component.RollingNumber import com.ehviewer.core.ui.component.RollingNumber
import com.ehviewer.core.ui.component.rememberIntPrefSliderState
import com.hippo.ehviewer.ui.openBrowser import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.util.ProgressDialog import com.hippo.ehviewer.util.ProgressDialog
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -71,19 +73,16 @@ fun SwitchPreference(title: String, summary: String? = null, state: MutableState
} }
@Composable @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 }) { 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 sliderState = remember { mutableIntStateOf(state.value) } val state = rememberIntPrefSliderState(pref, minValue..maxValue, step)
IntSliderPreference( IntSliderPreference(
state = state, state = state,
title = { Text(title) }, title = { Text(title) },
valueRange = minValue..maxValue,
valueSteps = step,
sliderState = sliderState,
enabled = enabled, enabled = enabled,
summary = summary?.let { { Text(it) } }, summary = summary?.let { { Text(it) } },
valueText = { valueText = {
RollingNumber( RollingNumber(
number = display(sliderState.intValue), number = display(state.value),
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
length = 5, length = 5,
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource 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.EhApplication.Companion.searchDatabase
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.Screen import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.isAuthenticationSupported import com.hippo.ehviewer.ui.isAuthenticationSupported
import com.hippo.ehviewer.ui.main.NavigationIcon import com.hippo.ehviewer.ui.main.NavigationIcon
@@ -48,17 +50,17 @@ fun AnimatedVisibilityScope.PrivacyScreen(navigator: DestinationsNavigator) = Sc
enabled = isAuthenticationSupported(), enabled = isAuthenticationSupported(),
) )
AnimatedVisibility(visible = security.value) { AnimatedVisibility(visible = security.value) {
val securityDelay = Settings.securityDelay.asMutableState() val delay by Settings.securityDelay.collectAsState()
val summary = if (securityDelay.value == 0) { val summary = if (delay == 0) {
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary_immediately) stringResource(id = R.string.settings_privacy_require_unlock_delay_summary_immediately)
} else { } 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( IntSliderPreference(
maxValue = 30, maxValue = 30,
title = stringResource(id = R.string.settings_privacy_require_unlock_delay), title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
summary = summary, summary = summary,
state = securityDelay, pref = Settings.securityDelay,
enabled = isAuthenticationSupported(), enabled = isAuthenticationSupported(),
) )
} }
@@ -94,7 +94,7 @@ fun AnnotatedString.normalizeSpan(): AnnotatedString {
val italic = filterMerged(SpanStyle(fontStyle = FontStyle.Italic)) { it.fontStyle == FontStyle.Italic } val italic = filterMerged(SpanStyle(fontStyle = FontStyle.Italic)) { it.fontStyle == FontStyle.Italic }
val underline = filterMerged(SpanStyle(textDecoration = TextDecoration.Underline)) { it.textDecoration == TextDecoration.Underline } val underline = filterMerged(SpanStyle(textDecoration = TextDecoration.Underline)) { it.textDecoration == TextDecoration.Underline }
val linethrough = filterMerged(SpanStyle(textDecoration = TextDecoration.LineThrough)) { it.textDecoration == TextDecoration.LineThrough } 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) return AnnotatedString(text, spans)
} }
@@ -42,6 +42,7 @@ import androidx.compose.material3.DatePickerDefaults
import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DisplayMode import androidx.compose.material3.DisplayMode
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenu
import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
@@ -684,10 +685,10 @@ suspend fun awaitSelectItemWithIcon(
} }
itemsIndexed(items) { index, (icon, text) -> itemsIndexed(items) { index, (icon, text) ->
ListItem( ListItem(
headlineContent = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) },
modifier = Modifier.clickable { resume(index) }.padding(horizontal = 8.dp), modifier = Modifier.clickable { resume(index) }.padding(horizontal = 8.dp),
leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) }, leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) },
colors = ListItemDefaults.colors(containerColor = Color.Transparent), 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 textStyle = MaterialTheme.typography.titleMedium
val checkedColor = MaterialTheme.colorScheme.primary val checkedColor = MaterialTheme.colorScheme.primary
ListItem( ListItem(
headlineContent = {
Text(
text = text,
style = if (checked) textStyle.copy(color = checkedColor) else textStyle,
)
},
modifier = modifier, modifier = modifier,
trailingContent = checked.ifTrueThen { trailingContent = checked.ifTrueThen {
Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = checkedColor) Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = checkedColor)
}, },
colors = ListItemDefaults.colors(containerColor = Color.Transparent), 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 com.hippo.ehviewer.util.copyTo
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.accept import io.ktor.client.request.accept
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.prepareGet import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.ContentType import io.ktor.http.ContentType
@@ -21,17 +20,18 @@ import kotlin.time.Instant
import kotlinx.io.asSource import kotlinx.io.asSource
import moe.tarsin.coroutines.runSuspendCatching import moe.tarsin.coroutines.runSuspendCatching
import okio.Path import okio.Path
import tachiyomi.data.release.GithubArtifacts import tachiyomi.data.release.Artifacts
import tachiyomi.data.release.GithubCommitComparison import tachiyomi.data.release.CommitComparison
import tachiyomi.data.release.GithubRelease import tachiyomi.data.release.Release
import tachiyomi.data.release.GithubRepo import tachiyomi.data.release.Repository
import tachiyomi.data.release.GithubWorkflowRuns import tachiyomi.data.release.WorkflowRuns
import tachiyomi.data.release.downloadLink
private const val API_URL = "https://api.github.com/repos/${BuildConfig.REPO_NAME}" const val API_URL = "https://codeberg.org/api/v1/repos/${BuildConfig.REPO_NAME}"
private const val LATEST_RELEASE_URL = "$API_URL/releases/latest" const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
object AppUpdater { object AppUpdater {
suspend fun checkForUpdate(forceCheck: Boolean = false): Release? { suspend fun checkForUpdate(forceCheck: Boolean = false): EhRelease? {
val now = Clock.System.now() val now = Clock.System.now()
val last = Instant.fromEpochSeconds(Settings.lastUpdateTime) val last = Instant.fromEpochSeconds(Settings.lastUpdateTime)
val interval = Settings.updateIntervalDays.value val interval = Settings.updateIntervalDays.value
@@ -39,31 +39,30 @@ object AppUpdater {
Settings.lastUpdateTime = now.epochSeconds Settings.lastUpdateTime = now.epochSeconds
if (Settings.useCIUpdateChannel.value) { if (Settings.useCIUpdateChannel.value) {
val curSha = BuildConfig.COMMIT_SHA val curSha = BuildConfig.COMMIT_SHA
val branch = ghStatement(API_URL).executeAndParseAs<GithubRepo>().defaultBranch val branch = statement(API_URL).executeAndParseAs<Repository>().defaultBranch
val workflowRunsUrl = "$API_URL/actions/workflows/ci.yml/runs?branch=$branch&event=push&status=success&per_page=1" val run = statement("$API_URL/actions/runs?event=push&status=success&ref=refs/heads/$branch").executeAndParseAs<WorkflowRuns>().workflowRuns[0]
val workflowRun = ghStatement(workflowRunsUrl).executeAndParseAs<GithubWorkflowRuns>().workflowRuns[0] val shortSha = run.headSha.take(7)
val shortSha = workflowRun.headSha.take(7)
if (shortSha != curSha) { if (shortSha != curSha) {
val artifacts = ghStatement(workflowRun.artifactsUrl).executeAndParseAs<GithubArtifacts>() val artifacts = statement("$API_URL/actions/runs/${run.id}/artifacts").executeAndParseAs<Artifacts>()
val archiveUrl = artifacts.getDownloadLink() val archiveUrl = artifacts.downloadLink
val changelog = runSuspendCatching { val changelog = runSuspendCatching {
val commitComparisonUrl = "$API_URL/compare/$curSha...$shortSha" val commitComparisonUrl = "$API_URL/compare/$curSha...$shortSha"
val result = ghStatement(commitComparisonUrl).executeAndParseAs<GithubCommitComparison>() val result = statement(commitComparisonUrl).executeAndParseAs<CommitComparison>()
// TODO: Prettier format, Markdown? // TODO: Prettier format, Markdown?
result.commits.joinToString("\n") { commit -> result.commits.joinToString("\n") { commit ->
"${commit.commit.message.takeWhile { it != '\n' }} (@${commit.commit.author.name})" "${commit.commit.message.takeWhile { it != '\n' }} (@${commit.commit.author.name})"
} }
}.getOrDefault(workflowRun.title) }.getOrDefault(run.title)
return Release(shortSha, changelog, archiveUrl) return EhRelease(shortSha, changelog, archiveUrl)
} }
} else { } else {
val curVersion = BuildConfig.RAW_VERSION_NAME 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 latestVersion = release.version
val description = release.info val description = release.info
val downloadUrl = release.getDownloadLink() val downloadUrl = release.getDownloadLink()
if (latestVersion != curVersion) { if (latestVersion != curVersion) {
return Release(latestVersion, description, downloadUrl) return EhRelease(latestVersion, description, downloadUrl)
} }
} }
} }
@@ -75,7 +74,7 @@ object AppUpdater {
timeoutBySpeed( timeoutBySpeed(
url, url,
{ {
ghStatement(url) { statement(url) {
// https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#get-a-release-asset // https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#get-a-release-asset
if (!isZip) accept(ContentType.Application.OctetStream) if (!isZip) accept(ContentType.Application.OctetStream)
it() it()
@@ -98,22 +97,14 @@ object AppUpdater {
} }
} }
private suspend inline fun ghStatement( suspend inline fun statement(
url: String, url: String,
builder: HttpRequestBuilder.() -> Unit = {}, builder: HttpRequestBuilder.() -> Unit = {},
) = ktorClient.prepareGet(url) { ) = ktorClient.prepareGet(url) {
bearerAuth(GithubTokenParts.joinToString("_"))
apply(builder) apply(builder)
} }
private val GithubTokenParts = arrayOf( data class EhRelease(
"github",
"pat",
"11A4H2ACI0iGDuL1O6wPYW",
"OTFg8xaCNUwR1NHaJE1AT3LoYPfz6bouI7E7ReLf8GjIRFHCL5UsHL9EnWP",
)
data class Release(
val version: String, val version: String,
val changelog: String, val changelog: String,
val downloadLink: String, val downloadLink: String,
@@ -39,18 +39,14 @@ object AppConfig {
private const val CRASH = "crash" private const val CRASH = "crash"
private const val TAG_TRANSLATIONS = "tag-translations" private const val TAG_TRANSLATIONS = "tag-translations"
private val abi = Build.SUPPORTED_ABIS[0].takeIf { private val abi = Build.SUPPORTED_ABIS[0].takeIf { it in ["arm64-v8a", "x86_64", "armeabi-v7a"] } ?: "universal"
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a")
} ?: "universal"
val isBenchmark = "nonMinified" in BuildConfig.BUILD_TYPE || "benchmark" in BuildConfig.BUILD_TYPE 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 commitTime = ParserUtils.formatDate(BuildConfig.COMMIT_TIME * 1000)
val isSnapshot = "SNAPSHOT" in BuildConfig.VERSION_NAME
private val externalAppDir: File? private val externalAppDir: File?
get() { get() {
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) { if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
@@ -22,8 +22,8 @@ import android.content.ClipDescription
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.PersistableBundle
import android.view.textclassifier.TextClassifier import android.view.textclassifier.TextClassifier
import androidx.core.os.persistableBundleOf
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.ui.MainActivity import com.hippo.ehviewer.ui.MainActivity
import moe.tarsin.tip import moe.tarsin.tip
@@ -33,7 +33,9 @@ fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
clipboardManager.setPrimaryClip( clipboardManager.setPrimaryClip(
ClipData.newPlainText(null, text).apply { ClipData.newPlainText(null, text).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isSensitive) { 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)
}
} }
}, },
) )
@@ -16,11 +16,11 @@
package com.hippo.ehviewer.util package com.hippo.ehviewer.util
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalLocale
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.ehviewer.core.util.toLocalDateTime import com.ehviewer.core.util.toLocalDateTime
import java.util.Locale
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
@@ -127,7 +127,7 @@ object ReadableTime {
val timeDate = time.toLocalDateTime(timeZone).date val timeDate = time.toLocalDateTime(timeZone).date
val nowYear = nowDate.year val nowYear = nowDate.year
val timeYear = timeDate.year val timeYear = timeDate.year
val isZh = Locale.getDefault().language == "zh" val isZh = LocalLocale.current.language == "zh"
if (nowYear == timeYear) { if (nowYear == timeYear) {
if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR
} else { } else {
@@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenu
import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.FlexibleBottomAppBar 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.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -19,6 +18,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import 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.Slider
import com.ehviewer.core.ui.component.defaultMaxTickCount import com.ehviewer.core.ui.component.defaultMaxTickCount
import com.ehviewer.core.ui.util.HapticFeedbackType import com.ehviewer.core.ui.util.HapticFeedbackType
@@ -29,7 +31,7 @@ import com.ehviewer.core.ui.util.rememberHapticFeedback
@Composable @Composable
fun ChapterNavigator( fun ChapterNavigator(
isRtl: Boolean, isRtl: Boolean,
currentPage: Int, slider: IntSliderState,
totalPages: Int, totalPages: Int,
onSliderValueChange: (Int) -> Unit, onSliderValueChange: (Int) -> Unit,
containerColor: Color, containerColor: Color,
@@ -40,14 +42,13 @@ fun ChapterNavigator(
modifier = Modifier.padding(horizontal = horizontalPadding).clip(CircleShape).background(containerColor).padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = horizontalPadding).clip(CircleShape).background(containerColor).padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text(text = "$currentPage") RollingNumber(slider.value, length = "$totalPages".length)
val steps = totalPages - 2
val maxTickCount = defaultMaxTickCount() val maxTickCount = defaultMaxTickCount()
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
if (steps < maxTickCount) { if (slider.state.steps < maxTickCount) {
val sliderDragged by interactionSource.collectIsDraggedAsState() val sliderDragged by interactionSource.collectIsDraggedAsState()
val hapticFeedback = rememberHapticFeedback() val hapticFeedback = rememberHapticFeedback()
LaunchedEffect(currentPage) { LaunchedEffect(slider.value) {
if (sliderDragged) { if (sliderDragged) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE) hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
} }
@@ -55,13 +56,11 @@ fun ChapterNavigator(
} }
Slider( Slider(
modifier = Modifier.weight(1f).padding(horizontal = 8.dp), modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
value = currentPage, state = slider,
valueRange = 1..totalPages,
steps = steps,
onValueChange = onSliderValueChange, onValueChange = onSliderValueChange,
maxTickCount = maxTickCount, maxTickCount = maxTickCount,
interactionSource = interactionSource, interactionSource = interactionSource,
) )
Text(text = "$totalPages") RollingNumberPlaceholder(totalPages)
} }
} }
@@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.IntSliderState
import com.hippo.ehviewer.ui.main.NavigationIcon import com.hippo.ehviewer.ui.main.NavigationIcon
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -35,7 +36,7 @@ fun BoxScope.ReaderAppBars(
title: String, title: String,
isRtl: Boolean, isRtl: Boolean,
showSeekBar: Boolean, showSeekBar: Boolean,
currentPage: Int, slider: IntSliderState,
totalPages: Int, totalPages: Int,
onSliderValueChange: (Int) -> Unit, onSliderValueChange: (Int) -> Unit,
onClickSettings: () -> Unit, onClickSettings: () -> Unit,
@@ -65,7 +66,7 @@ fun BoxScope.ReaderAppBars(
if (showSeekBar && totalPages > 1) { if (showSeekBar && totalPages > 1) {
ChapterNavigator( ChapterNavigator(
isRtl = isRtl, isRtl = isRtl,
currentPage = currentPage, slider = slider,
totalPages = totalPages, totalPages = totalPages,
onSliderValueChange = onSliderValueChange, onSliderValueChange = onSliderValueChange,
containerColor = backgroundColor, 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),
)
}
}
}
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* +---+---+---+ * +---+---+---+
*/ */
object EdgeNavigation : ViewerNavigation() { object EdgeNavigation : ViewerNavigation() {
override val originalRegions = listOf( override val originalRegions = [
Region( Region(
rect = Rect(0f, 0f, 0.33f, 1f), rect = Rect(0f, 0f, 0.33f, 1f),
type = NavigationRegion.NEXT, type = NavigationRegion.NEXT,
@@ -27,5 +27,5 @@ object EdgeNavigation : ViewerNavigation() {
rect = Rect(0.66f, 0f, 1f, 1f), rect = Rect(0.66f, 0f, 1f, 1f),
type = NavigationRegion.NEXT, type = NavigationRegion.NEXT,
), ),
) ]
} }
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* +---+---+---+ * +---+---+---+
*/ */
object KindlishNavigation : ViewerNavigation() { object KindlishNavigation : ViewerNavigation() {
override val originalRegions = listOf( override val originalRegions = [
Region( Region(
rect = Rect(0.33f, 0.33f, 1f, 1f), rect = Rect(0.33f, 0.33f, 1f, 1f),
type = NavigationRegion.NEXT, type = NavigationRegion.NEXT,
@@ -23,5 +23,5 @@ object KindlishNavigation : ViewerNavigation() {
rect = Rect(0f, 0.33f, 0.33f, 1f), rect = Rect(0f, 0.33f, 0.33f, 1f),
type = NavigationRegion.PREV, type = NavigationRegion.PREV,
), ),
) ]
} }
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* +---+---+---+ * +---+---+---+
*/ */
object LNavigation : ViewerNavigation() { object LNavigation : ViewerNavigation() {
override val originalRegions = listOf( override val originalRegions = [
Region( Region(
rect = Rect(0f, 0.33f, 0.33f, 0.66f), rect = Rect(0f, 0.33f, 0.33f, 0.66f),
type = NavigationRegion.PREV, type = NavigationRegion.PREV,
@@ -31,5 +31,5 @@ object LNavigation : ViewerNavigation() {
rect = Rect(0f, 0.66f, 1f, 1f), rect = Rect(0f, 0.66f, 1f, 1f),
type = NavigationRegion.NEXT, type = NavigationRegion.NEXT,
), ),
) ]
} }
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* +---+---+---+ * +---+---+---+
*/ */
object RightAndLeftNavigation : ViewerNavigation() { object RightAndLeftNavigation : ViewerNavigation() {
override val originalRegions = listOf( override val originalRegions = [
Region( Region(
rect = Rect(0f, 0f, 0.33f, 1f), rect = Rect(0f, 0f, 0.33f, 1f),
type = NavigationRegion.LEFT, type = NavigationRegion.LEFT,
@@ -23,5 +23,5 @@ object RightAndLeftNavigation : ViewerNavigation() {
rect = Rect(0.66f, 0f, 1f, 1f), rect = Rect(0.66f, 0f, 1f, 1f),
type = NavigationRegion.RIGHT, type = NavigationRegion.RIGHT,
), ),
) ]
} }
@@ -21,39 +21,21 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.ehviewer.core.ui.component.IntSliderState
import com.ehviewer.core.ui.component.Slider import com.ehviewer.core.ui.component.Slider
@Composable @Composable
fun IntSliderPreference( fun IntSliderPreference(
state: MutableState<Int>, state: IntSliderState,
title: @Composable () -> Unit, title: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
valueRange: IntRange = 0..1,
valueSteps: Int = 0,
sliderState: MutableIntState = remember { mutableIntStateOf(state.value) },
enabled: Boolean = true, enabled: Boolean = true,
icon: @Composable (() -> Unit)? = null, icon: @Composable (() -> Unit)? = null,
summary: @Composable (() -> Unit)? = null, summary: @Composable (() -> Unit)? = null,
valueText: @Composable (() -> Unit)? = null, valueText: @Composable (() -> Unit)? = null,
) { ) {
var value by state
var sliderValue by sliderState
var lastValue by remember { mutableIntStateOf(value) }
SideEffect {
if (value != lastValue) {
sliderValue = value
lastValue = value
}
}
Preference( Preference(
title = title, title = title,
modifier = modifier, modifier = modifier,
@@ -64,13 +46,10 @@ fun IntSliderPreference(
summary?.invoke() summary?.invoke()
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Slider( Slider(
value = sliderValue, state = state,
onValueChange = { sliderValue = it }, onValueChange = { state.value = it },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = enabled, enabled = enabled,
valueRange = valueRange,
steps = valueSteps,
onValueChangeFinished = { value = sliderValue },
) )
if (valueText != null) { if (valueText != null) {
val theme = LocalPreferenceTheme.current val theme = LocalPreferenceTheme.current
@@ -40,6 +40,7 @@ class AndroidAsyncDns(
private val network: Network? = null, private val network: Network? = null,
) : AsyncDns { ) : AsyncDns {
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
@Suppress("DEPRECATION")
internal val resolver = DnsResolver.getInstance() internal val resolver = DnsResolver.getInstance()
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
@@ -0,0 +1,15 @@
package tachiyomi.data.release
import kotlinx.serialization.Serializable
@Serializable
data class CommitComparison(val commits: List<Commit>)
@Serializable
data class Commit(val commit: CommitDetail)
@Serializable
data class CommitDetail(val author: CommitAuthor, val message: String)
@Serializable
data class CommitAuthor(val name: String)
@@ -1,15 +0,0 @@
package tachiyomi.data.release
import kotlinx.serialization.Serializable
@Serializable
data class GithubCommitComparison(val commits: List<GithubCommit>)
@Serializable
data class GithubCommit(val commit: GithubCommitDetail)
@Serializable
data class GithubCommitDetail(val author: GithubCommitAuthor, val message: String)
@Serializable
data class GithubCommitAuthor(val name: String)
@@ -1,40 +0,0 @@
package tachiyomi.data.release
import com.hippo.ehviewer.util.AppConfig
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GithubRepo(
@SerialName("default_branch") val defaultBranch: String,
)
@Serializable
data class GithubWorkflowRuns(
@SerialName("workflow_runs") val workflowRuns: List<GithubWorkflowRun>,
)
@Serializable
data class GithubWorkflowRun(
@SerialName("head_sha") val headSha: String,
@SerialName("display_title") val title: String,
@SerialName("artifacts_url") val artifactsUrl: String,
)
@Serializable
data class GithubArtifacts(
@SerialName("artifacts") val artifacts: List<GithubArtifact>,
) {
fun getDownloadLink(): String {
// The default order is upload order, so we need to sort it
return artifacts.sortedBy { it.name }.run {
find { AppConfig.matchVariant(it.name) } ?: this[0]
}.downloadLink
}
}
@Serializable
data class GithubArtifact(
val name: String,
@SerialName("archive_download_url") val downloadLink: String,
)
@@ -4,15 +4,12 @@ import com.hippo.ehviewer.util.AppConfig
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/**
* Contains information about the latest release from GitHub.
*/
@Serializable @Serializable
data class GithubRelease( data class Release(
@SerialName("tag_name") val version: String, @SerialName("tag_name") val version: String,
@SerialName("body") val info: String, @SerialName("body") val info: String,
@SerialName("html_url") val releaseLink: String, @SerialName("html_url") val releaseLink: String,
@SerialName("assets") val assets: List<GitHubAssets>, @SerialName("assets") val assets: List<Assets>,
) { ) {
fun getDownloadLink(): String { fun getDownloadLink(): String {
val asset = assets.find { AppConfig.matchVariant(it.name) } ?: assets[0] val asset = assets.find { AppConfig.matchVariant(it.name) } ?: assets[0]
@@ -20,8 +17,10 @@ data class GithubRelease(
} }
} }
/**
* Assets class containing download url.
*/
@Serializable @Serializable
data class GitHubAssets(val url: String, val name: String) data class Assets(
@SerialName("browser_download_url")
val url: String,
val name: String,
)
@@ -0,0 +1,31 @@
package tachiyomi.data.release
import com.ehviewer.core.util.unreachable
import com.hippo.ehviewer.util.AppConfig
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Repository(@SerialName("default_branch") val defaultBranch: String)
@Serializable
data class WorkflowRuns(@SerialName("workflow_runs") val workflowRuns: List<WorkflowRun>)
@Serializable
data class WorkflowRun(
@SerialName("commit_sha") val headSha: String,
@SerialName("title") val title: String,
@SerialName("id") val id: Int,
)
typealias Artifacts = List<Artifact>
val Artifacts.downloadLink
get() = (find { AppConfig.matchVariant(it.name) } ?: unreachable()).downloadLink
@Serializable
data class Artifact(
val name: String,
@SerialName("archive_download_url")
val downloadLink: String,
)
+653 -748
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -16,7 +16,7 @@ regex-lite = "0.1"
jni = { version = "0.21", optional = true } jni = { version = "0.21", optional = true }
jni_fn = { version = "0.1", optional = true } jni_fn = { version = "0.1", optional = true }
chrono = { version = "0.4", default-features = false, features = ["std"] } chrono = { version = "0.4", default-features = false, features = ["std"] }
quick-xml = { default-features = false, version = "0.38" } quick-xml = { default-features = false, version = "0.42" }
tl = { git = "https://github.com/FooIbar/tl.git", features = ["simd"] } tl = { git = "https://github.com/FooIbar/tl.git", features = ["simd"] }
log = "0.4" log = "0.4"
android_logger = { default-features = false, version = "0.15" } android_logger = { default-features = false, version = "0.15" }
@@ -25,12 +25,12 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0" anyhow = "1.0"
image = { default-features = false, version = "0.25" } image = { default-features = false, version = "0.25" }
rxing = { default-features = false, version = "0.8" } rxing = { version = "0.9", default-features = false, features = ["decoders", "qrcode", "no_character_set_support"] }
ndk = { version = "0.9", optional = true } ndk = { version = "0.9", optional = true }
libwebp-sys2 = { git = "https://github.com/FooIbar/libwebp-sys2-rs.git", features = ["1_5", "demux", "extern-types"] } libwebp-sys2 = { git = "https://github.com/FooIbar/libwebp-sys2-rs.git", features = ["1_5", "demux", "extern-types"] }
[dev-dependencies] [dev-dependencies]
reqwest = "0.12" reqwest = "0.13"
tokio = { version = "1", features = ["macros"] } tokio = { version = "1", features = ["macros"] }
[profile.release] [profile.release]
+1
View File
@@ -256,6 +256,7 @@ where
R: Serialize, R: Serialize,
{ {
jni_throwing(env, |env| { jni_throwing(env, |env| {
ensure!(limit > 0, "Empty response");
let buffer = deref_mut_direct_bytebuffer(env, str)?; let buffer = deref_mut_direct_bytebuffer(env, str)?;
// SAFETY: ktor client ensure html content is valid utf-8. // SAFETY: ktor client ensure html content is valid utf-8.
+3 -3
View File
@@ -9,20 +9,20 @@ use std::f32::consts::FRAC_1_SQRT_2;
fn image_buffer_to_luma8<P: CustomPixel>( fn image_buffer_to_luma8<P: CustomPixel>(
src: &ImageBuffer<P, &[P::Subpixel]>, src: &ImageBuffer<P, &[P::Subpixel]>,
) -> Luma8LuminanceSource { ) -> Result<Luma8LuminanceSource> {
let (w, h) = src.dimensions(); let (w, h) = src.dimensions();
let mut dst = GrayImage::new(w, h); let mut dst = GrayImage::new(w, h);
for (to, from) in dst.pixels_mut().zip(src.pixels()) { for (to, from) in dst.pixels_mut().zip(src.pixels()) {
*to = from.to_luma8(); *to = from.to_luma8();
} }
Luma8LuminanceSource::new(dst.into_raw(), w, h) Ok(Luma8LuminanceSource::new(dst.into_raw(), w, h)?)
} }
pub struct QrCode; pub struct QrCode;
impl ImageConsumer<bool> for QrCode { impl ImageConsumer<bool> for QrCode {
fn apply<P: CustomPixel>(self, image: &ImageBuffer<P, &[P::Subpixel]>) -> Result<bool> { fn apply<P: CustomPixel>(self, image: &ImageBuffer<P, &[P::Subpixel]>) -> Result<bool> {
let source = image_buffer_to_luma8(image); let source = image_buffer_to_luma8(image)?;
let image = BinaryBitmap::new(HybridBinarizer::new(source)); let image = BinaryBitmap::new(HybridBinarizer::new(source));
match FinderPatternFinder::new(image.get_black_matrix()).find(&DecodeHints::default()) { match FinderPatternFinder::new(image.get_black_matrix()).find(&DecodeHints::default()) {
Ok(info) => { Ok(info) => {
-1
View File
@@ -1,5 +1,4 @@
#![feature(f16)] #![feature(f16)]
#![feature(never_type)]
mod ffi; mod ffi;
pub mod img; pub mod img;
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::parser::utils::unescape;
use crate::regex; use crate::regex;
use quick_xml::escape::unescape;
pub fn parse_fav_cat(html: &str) -> Vec<String> { pub fn parse_fav_cat(html: &str) -> Vec<String> {
regex!("<input type=\"text\" name=\"favorite_\\d\" value=\"([^\"]+)\"") regex!("<input type=\"text\" name=\"favorite_\\d\" value=\"([^\"]+)\"")
+1 -1
View File
@@ -1,9 +1,9 @@
use crate::parser::list::{BaseGalleryInfo, get_thumb_key, parse_token_and_gid, to_category_i32}; use crate::parser::list::{BaseGalleryInfo, get_thumb_key, parse_token_and_gid, to_category_i32};
use crate::parser::utils::unescape;
use crate::{EhError, get_first_child, get_first_element_by_class_name, get_node_handle_attr}; use crate::{EhError, get_first_child, get_first_element_by_class_name, get_node_handle_attr};
use crate::{get_tag_attr, get_vdom_first_element_by_class_name, regex}; use crate::{get_tag_attr, get_vdom_first_element_by_class_name, regex};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use chrono::naive::NaiveDateTime; use chrono::naive::NaiveDateTime;
use quick_xml::escape::unescape;
use serde::Serialize; use serde::Serialize;
use tl::{Bytes, Node, NodeHandle, Parser, VDom}; use tl::{Bytes, Node, NodeHandle, Parser, VDom};
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::parser::list::{GalleryListResult, parse_info_list}; use crate::parser::list::{GalleryListResult, parse_info_list};
use crate::parser::utils::unescape;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use quick_xml::escape::unescape;
use serde::Serialize; use serde::Serialize;
use tl::Parser; use tl::Parser;
use tl::VDom; use tl::VDom;
+6 -6
View File
@@ -1,10 +1,10 @@
use crate::EhError; use crate::EhError;
use crate::parser::utils::unescape;
use crate::{EHGT_PREFIX, EX_PREFIX}; use crate::{EHGT_PREFIX, EX_PREFIX};
use crate::{get_element_by_id, get_vdom_first_element_by_class_name}; use crate::{get_element_by_id, get_vdom_first_element_by_class_name};
use crate::{get_first_element_by_class_name, query_childs_first_match_attr}; use crate::{get_first_element_by_class_name, query_childs_first_match_attr};
use crate::{get_node_attr, get_node_handle_attr, regex}; use crate::{get_node_attr, get_node_handle_attr, regex};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use quick_xml::escape::unescape;
use serde::Serialize; use serde::Serialize;
use std::borrow::Cow; use std::borrow::Cow;
use std::ops::Index; use std::ops::Index;
@@ -128,13 +128,13 @@ fn parse_gallery_info(node: &Node, parser: &Parser) -> Option<BaseGalleryInfo> {
.collect(); .collect();
let (thumb, (thumb_height, thumb_width)) = let (thumb, (thumb_height, thumb_width)) =
match tag.query_selector(parser, "[data-src]")?.next() { match tag.query_selector(parser, "[data-src]")?.next() {
None => match tag.query_selector(parser, "[src]")?.next() { None => {
None => return None, let thumb = tag.query_selector(parser, "[src]")?.next()?;
Some(thumb) => ( (
get_node_handle_attr(&thumb, parser, "src")?, get_node_handle_attr(&thumb, parser, "src")?,
parse_thumb_resolution(get_node_handle_attr(&thumb, parser, "style")?), parse_thumb_resolution(get_node_handle_attr(&thumb, parser, "style")?),
), )
}, }
Some(thumb) => ( Some(thumb) => (
get_node_handle_attr(&thumb, parser, "data-src")?, get_node_handle_attr(&thumb, parser, "data-src")?,
parse_thumb_resolution(get_node_handle_attr(&thumb, parser, "style")?), parse_thumb_resolution(get_node_handle_attr(&thumb, parser, "style")?),
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::parser::utils::unescape;
use crate::{EhError, get_node_handle_attr, get_tag_attr, select_first}; use crate::{EhError, get_node_handle_attr, get_tag_attr, select_first};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use quick_xml::escape::unescape;
use serde::Serialize; use serde::Serialize;
use tl::{Parser, VDom}; use tl::{Parser, VDom};
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::parser::utils::unescape;
use crate::regex; use crate::regex;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use quick_xml::escape::unescape;
use serde::Serialize; use serde::Serialize;
use tl::{Parser, VDom}; use tl::{Parser, VDom};
+16
View File
@@ -1,3 +1,19 @@
use quick_xml::escape::{EscapeError, resolve_xml_entity, unescape_with};
use std::borrow::Cow;
pub fn parse_int_lenient(s: &str, default: i32) -> i32 { pub fn parse_int_lenient(s: &str, default: i32) -> i32 {
s.replace(",", "").parse().unwrap_or(default) s.replace(",", "").parse().unwrap_or(default)
} }
pub fn unescape(raw: &str) -> Result<Cow<'_, str>, EscapeError> {
unescape_with(raw, resolve_predefined_entity)
}
#[inline]
const fn resolve_predefined_entity(entity: &str) -> Option<&'static str> {
match entity.as_bytes() {
b"nbsp" => Some(" "),
b"times" => Some("×"),
_ => resolve_xml_entity(entity),
}
}
+2 -2
View File
@@ -1,6 +1,5 @@
plugins { plugins {
alias(libs.plugins.android.test) alias(libs.plugins.android.test)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.baselineprofile) alias(libs.plugins.baselineprofile)
} }
@@ -30,7 +29,8 @@ android {
} }
kotlin { kotlin {
jvmToolchain(21) val javaVersion = libs.versions.java.get().toInt()
jvmToolchain(javaVersion)
} }
baselineProfile { baselineProfile {
+2 -1
View File
@@ -30,7 +30,8 @@ gradlePlugin {
} }
kotlin { kotlin {
jvmToolchain(21) val javaVersion = libs.versions.java.get().toInt()
jvmToolchain(javaVersion)
} }
spotless { spotless {
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more