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:
baseline-profile:
runs-on: ubuntu-latest
runs-on: nekomoee
steps:
- name: Free disk space
run: rm -rf $AGENT_TOOLSDIRECTORY
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v6
@@ -31,13 +22,19 @@ jobs:
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
java-version-file: .java-version
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
@@ -48,7 +45,7 @@ jobs:
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
@@ -59,7 +56,7 @@ jobs:
- name: Upload reports
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v3
with:
name: reports
path: benchmark/build/reports
+128
View File
@@ -0,0 +1,128 @@
name: CI
on:
push:
branches:
- '*'
pull_request:
workflow_dispatch:
jobs:
check:
runs-on: nekomoee
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
- name: Clippy Check
working-directory: ./app/src/main/rust
run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: true
- name: Check build-logic
run: ./gradlew :build-logic:convention:check
- name: Spotless Check
run: ./gradlew spotlessCheck
default:
runs-on: nekomoee
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v5
with:
path: app/.cxx
key: cmake-default-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
restore-keys: cmake-default-
- name: Build
run: ./gradlew assembleDefaultRelease
- name: Upload ARM64
uses: actions/upload-artifact@v3
with:
name: default-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v3
with:
name: default-x86_64-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v3
with:
name: default-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
- name: Upload mapping
uses: actions/upload-artifact@v3
with:
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease/*.txt
- name: Upload native debug symbols
uses: actions/upload-artifact@v3
with:
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
+79
View File
@@ -0,0 +1,79 @@
name: Releases
on:
push:
tags:
- "*"
jobs:
build:
runs-on: nekomoee
steps:
- name: Free disk space
run: rm -rf $AGENT_TOOLSDIRECTORY
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache/restore@v5
with:
path: app/.cxx
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
- name: Generate Baseline Profile
run: ./gradlew generateBaselineProfile
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
-Prelease
- name: Build
run: ./gradlew assembleRelease
-Prelease
- name: Rename Apks
run: |
mkdir release
mv app/build/outputs/apk/default/release/app-default-universal-release.apk release/EhViewer-${{ github.ref_name }}-default-universal.apk
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk release/EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk release/EhViewer-${{ github.ref_name }}-default-x86_64.apk
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk release/EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultRelease/mapping.txt release/EhViewer-${{ github.ref_name }}-default-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip release/EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
- name: Releases
uses: actions/forgejo-release@v2
with:
direction: upload
url: https://codeberg.org
repo: EhViewer/EhViewer
release-dir: release
body: Bump Version
-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">
<img src="https://github.com/FooIbar/EhViewer-art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<br>EhViewer<br>
</h1>
<p align="center">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="/LICENSE">
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/FooIbar/EhViewer/releases">
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
</a>
<a href="https://github.com/FooIbar/EhViewer/issues">
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
</a>
Standalone fork of
<a href="https://codeberg.org/EhViewer/EhViewer">EhViewer (codeberg)</a>
maintained at <a href="https://gitea.yrkaierx.com/yenru0/EhViewer">gitea.yrkaierx.com/yenru0/EhViewer</a>
</p>
<div align="center">
<h3>
<a href="#description">
Description
</a>
<span> | </span>
<a href="#download">
Download
</a>
<span> | </span>
<a href="#screenshot">
Screenshot
</a>
<span> | </span>
<a href="#thanks">
Thanks
</a>
<span> | </span>
<a href="#license">
License
</a>
</h3>
</div>
# Changes over upstream
# Description
- Built-in hosts, Cloudflare CDN fallback and domain fronting with SNI spoofing for censored networks (`EhHostsConfig.kt`, `EhDns.kt`, `EhSSLSocketFactory.kt`)
- Cronet engine disabled by default (bypasses the anti-DPI OkHttp stack; `enable_quic` re-enables it)
- Update check disabled by default (manual check still available)
- API request timeout raised to 30 s
- Own application id (`yenru0.yrkaierx.ehviewer`), installs side by side with upstream builds
A modern EhViewer fork dedicated to high-performance
# Upstream
with [Material Design 3](https://m3.material.io/)
and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview) Support
# Download
| Flavor | Minimum Android Version | Notes |
|-------------|-------------------------|--------------------------------|
| Default | 8.0 | Full support |
| Marshmallow | 6.0* | Limited support, no guarantees |
*Devices running Android 6 need to have the [ISRG Root X1](https://letsencrypt.org/certs/isrgrootx1.pem) certificate installed
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/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/)
Synced from [EhViewer/EhViewer on codeberg](https://codeberg.org/EhViewer/EhViewer), the continuation of FooIbar's EhViewer. Feature work happens there; this fork tracks it and carries the network/resilience patches above.
# License
+12 -7
View File
@@ -49,15 +49,20 @@ android {
val repoName = providers.exec {
commandLine = "git remote get-url origin".split(' ')
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
.removeSuffix(".git")
}.standardOutput.asText.get().trim().removeSuffix(".git").removePrefix("https://codeberg.org/")
val snapshot = !hasProperty("release")
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
versionCode = 180063
versionName = "1.14.6"
// versionNameSuffix = "-SNAPSHOT"
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
applicationId = "yenru0.yrkaierx.ehviewer"
versionCode = 180064
versionName = if (snapshot) {
"1.15.0-SNAPSHOT"
} else {
"1.15.0"
}
buildConfigField("boolean", "SNAPSHOT", "$snapshot")
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName\"")
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("long", "COMMIT_TIME", commitTime)
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
+4
View File
@@ -15,6 +15,10 @@
java.nio.ByteBuffer source;
}
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
static boolean isLinkBufferComposerEnabled() return true;
}
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
-4
View File
@@ -118,10 +118,6 @@
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name=".ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings" />
<activity
android:name=".shortcuts.ShortcutsActivity"
android:excludeFromRecents="true"
+10 -8
View File
@@ -32,7 +32,7 @@ option(XZ_TOOL_XZDEC OFF)
FetchContent_Declare(
liblzma
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
GIT_TAG v5.8.1
GIT_TAG v5.8.3
GIT_SHALLOW 1
)
@@ -47,7 +47,7 @@ if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
else ()
set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin")
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}"
"CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments"
"AR=${TOOLCHAIN}/llvm-ar"
@@ -56,9 +56,11 @@ set(CONFIGURE_COMMAND ./configure --disable-public-key --disable-dependency-trac
)
ExternalProject_Add(
nettle
URL https://ftpmirror.gnu.org/nettle/nettle-3.10.2.tar.gz
URL_MD5 b28bcbf6f045ff007940a9401673600d
CONFIGURE_COMMAND ${WSL} ${CONFIGURE_COMMAND}
GIT_REPOSITORY https://gitlab.com/gnutls/nettle.git
GIT_TAG nettle_4.0_release_20260205
GIT_SHALLOW 1
GIT_CONFIG core.autocrlf=input
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
BUILD_COMMAND ${WSL} make -j4 libnettle.a
INSTALL_COMMAND ""
BUILD_IN_SOURCE 1
@@ -101,7 +103,7 @@ set(LIBARCHIVE_PATCH
FetchContent_Declare(
libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
GIT_TAG v3.8.4
GIT_TAG v3.8.7
GIT_SHALLOW 1
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)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.6
GIT_REPOSITORY https://github.com/FooIbar/corrosion.git
GIT_TAG v0.6.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];
sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest);
sha1_digest(&ctx, digest);
byte byte;
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.isCronetAvailable
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.lockObserver
import com.hippo.ehviewer.ui.screen.detailCache
import com.hippo.ehviewer.ui.tools.dataStateFlow
import com.hippo.ehviewer.util.AppConfig
@@ -109,7 +108,6 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
}
}
}
lifecycle.addObserver(lockObserver)
CrashHandler.install()
super.onCreate()
System.loadLibrary("ehviewer")
@@ -124,6 +122,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
}
launchIO {
EhTagDatabase.launchUpdate()
@Suppress("UNUSED_EXPRESSION")
launch { EhDB }
launch { dataStateFlow.value }
launch { OSUtils.totalMemory }
@@ -197,7 +196,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
add(GifDecoder.Factory())
}
}
diskCache { imageCache }
diskCache { thumbCache }
crossfade(300)
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
if (drawable != null) error(drawable.asImage(true))
@@ -220,7 +219,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
private set
val ktorClient by lazy {
if (isCronetAvailable && Settings.enableCronet.value) {
if (isCronetAvailable && Settings.enableQuic.value) {
HttpClient(Cronet) {
engine { configureClient(Settings.enableQuic.value) }
configureCommon()
@@ -239,10 +238,10 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
}
}
val imageCache by lazy {
val thumbCache by lazy {
diskCache {
directory(appCtx.cacheDir.toOkioPath() / "image_cache")
maxSizeBytes(Settings.readCacheSize.value.coerceIn(320, 5120).toLong() * 1024 * 1024)
directory(appCtx.cacheDir.toOkioPath() / "thumb")
maxSizeBytes(80L * 1024 * 1024)
}
}
@@ -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.download.DownloadsFilterMode
import com.hippo.ehviewer.download.SortMode
import com.hippo.ehviewer.util.AppConfig
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import java.util.Locale
@@ -112,7 +111,7 @@ object Settings : DataStorePreferences(null) {
val saveCrashLog = boolPref("save_crash_log", true)
val readCacheSize = intPref("read_cache_size_2", 640)
val enableCronet = boolPref("enable_cronet", true)
val enableQuic = boolPref("enable_quic", true)
val enableQuic = boolPref("enable_quic", false)
val hardwareBitmapThreshold = intPref("hardware_bitmap_threshold", 16384)
val preloadThumbAggressively = boolPref("preload_thumb_aggressively", false)
val animateItems = boolPref("animate_items", true)
@@ -120,8 +119,8 @@ object Settings : DataStorePreferences(null) {
// About
val backupBeforeUpdate = boolPref("backup_before_update", false)
val useCIUpdateChannel = boolPref("ci_update_channel", AppConfig.isSnapshot)
val updateIntervalDays = intPref("update_interval_days", 7)
val useCIUpdateChannel = boolPref("ci_update_channel", BuildConfig.SNAPSHOT)
val updateIntervalDays = intPref("update_interval_days", 0)
// Misc
val languageFilter = intPref("language_filter", -1)
@@ -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() },
)
if (empty) {
val message = if (EhUtils.isExHentai) {
val message = if (response.request.url.host == EhUrl.DOMAIN_EX) {
"Sad Panda\n(without panda)"
} else {
appCtx.getString(R.string.error_ip_banned)
@@ -194,7 +194,12 @@ private suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline
object EhEngine {
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
response.headers["Location"]?.takeUnless { Url(it).isLogin } ?: throw InsufficientGpException()
val location = response.headers["Location"] ?: run {
val body = response.bodyAsUtf8Text()
rethrowExactly(response, body.left(), InsufficientGpException())
}
if (Url(location).isLogin) throw InsufficientGpException()
location
}
suspend fun getPTokenListFromMpv(gid: Long, token: String) = ehRequest(EhUrl.getGalleryMultiPageViewerUrl(gid, token), EhUrl.referer)
@@ -0,0 +1,41 @@
package com.hippo.ehviewer.client
import android.util.Log
import com.hippo.ehviewer.Settings
import java.net.InetAddress
import java.net.Socket
import java.security.KeyStore
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import okhttp3.OkHttpClient
const val EXCEPTIONAL_DOMAIN = "hath.network"
private val sslSocketFactory: SSLSocketFactory = SSLContext.getInstance("TLS").apply {
init(null, null, null)
}.socketFactory
object EhSSLSocketFactory : SSLSocketFactory() {
override fun getDefaultCipherSuites(): Array<String> = sslSocketFactory.defaultCipherSuites
override fun getSupportedCipherSuites(): Array<String> = sslSocketFactory.supportedCipherSuites
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket = sslSocketFactory.createSocket(s, resolveHost(s, host), port, autoClose) as SSLSocket
override fun createSocket(host: String, port: Int): Socket = sslSocketFactory.createSocket(host, port)
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket = sslSocketFactory.createSocket(host, port, localHost, localPort)
override fun createSocket(host: InetAddress, port: Int): Socket = sslSocketFactory.createSocket(host, port)
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket = sslSocketFactory.createSocket(address, port, localAddress, localPort)
private fun resolveHost(socket: Socket, host: String): String = socket.inetAddress.hostAddress.takeIf {
host in dFEnabledDomains || EXCEPTIONAL_DOMAIN in host
} ?: host
}
fun OkHttpClient.Builder.install(sslSocketFactory: SSLSocketFactory) = apply {
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())!!
factory.init(null as KeyStore?)
val manager = factory.trustManagers!!
val trustManager = manager.filterIsInstance<X509TrustManager>().first()
sslSocketFactory(sslSocketFactory, trustManager)
}
@@ -53,12 +53,7 @@ import splitties.init.appCtx
object EhTagDatabase : CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
private const val NAMESPACE_PREFIX = "n"
private val PREFIXES = arrayOf(
"-",
"~",
"tag:",
"weak:",
)
private val PREFIXES = ["-", "~", "tag:", "weak:"]
private lateinit var tagGroups: Map<String, Map<String, String>>
private val updateLock = Mutex()
var initialized by mutableStateOf(false)
@@ -107,14 +107,14 @@ object EhUrl {
else -> URL_IMAGE_SEARCH_E
}
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) = ehUrl(listOf("g", "$gid", token)) {
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) = ehUrl(["g", "$gid", token]) {
if (index != 0) addQueryParameter("p", "$index")
if (allComment) addQueryParameter("hc", "1")
}.buildString()
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", "$gid", token)).buildString()
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(["mpv", "$gid", token]).buildString()
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(listOf("s", pToken, "$gid-${index + 1}")) {
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(["s", pToken, "$gid-${index + 1}"]) {
addQueryParameterIfNotBlank("nl", nl)
}.buildString()
@@ -143,7 +143,7 @@ object EhUrl {
}
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
path = if (path.isNullOrBlank()) emptyList() else listOf(path),
path = if (path.isNullOrBlank()) [] else [path],
host = host,
parameters = Parameters.build(builder),
)
@@ -35,7 +35,7 @@ data class ListUrlBuilder(
private var jumpTo: String? = null,
private var range: Int = 0,
var category: Int = EhUtils.NONE,
private var mKeyword: String? = null,
var keyword: String? = null,
var hash: String? = null,
var language: Int = -1,
var advanceSearch: Int = -1,
@@ -69,16 +69,10 @@ data class ListUrlBuilder(
jumpTo = date
}
var keyword: String?
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
set(keyword) {
mKeyword = keyword
}
constructor(q: QuickSearch) : this(
mode = q.mode,
category = q.category,
mKeyword = q.keyword,
keyword = q.keyword,
advanceSearch = q.advanceSearch,
minRating = q.minRating,
pageFrom = q.pageFrom,
@@ -90,7 +84,7 @@ data class ListUrlBuilder(
name = name,
mode = mode,
category = category,
keyword = mKeyword,
keyword = keyword,
advanceSearch = advanceSearch,
minRating = minRating,
pageFrom = pageFrom,
@@ -107,7 +101,7 @@ data class ListUrlBuilder(
if (q.category != this.category) {
return false
}
if (q.keyword != mKeyword) {
if (q.keyword != keyword) {
return false
}
if (q.advanceSearch != advanceSearch) {
@@ -129,7 +123,7 @@ data class ListUrlBuilder(
jumpTo = params["seek"],
range = params["range"]?.toIntOrNull() ?: 0,
category = params["f_cats"]?.toIntOrNull()?.let(EhUtils::invCategory) ?: EhUtils.NONE,
mKeyword = params["f_search"],
keyword = params["f_search"],
) {
if (params["advsearch"] == "1") {
advanceSearch = 0
@@ -160,7 +154,7 @@ data class ListUrlBuilder(
if (category > 0) {
addQueryParameter("f_cats", "${EhUtils.invCategory(category)}")
}
val query = mKeyword?.let { keyword ->
val query = keyword?.let { keyword ->
if (language == -1 || "gid:" in keyword || "l:" in keyword || "language:" in keyword) {
keyword
} else {
@@ -214,7 +208,7 @@ data class ListUrlBuilder(
}.buildString()
MODE_UPLOADER, MODE_TAG -> {
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
ehUrl(listOf(path, requireNotNull(mKeyword))) {
ehUrl([path, requireNotNull(keyword)]) {
addQueryParameterIfNotBlank("prev", prev)
addQueryParameterIfNotBlank("next", next)
addQueryParameterIfNotBlank("seek", jumpTo)
@@ -226,7 +220,7 @@ data class ListUrlBuilder(
addQueryParameter("f_shash", requireNotNull(hash))
}.buildString()
MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) {
addQueryParameter("tl", requireNotNull(mKeyword))
addQueryParameter("tl", requireNotNull(keyword))
addQueryParameterIfNotBlank("p", jumpTo)
}.buildString()
else -> throw IllegalStateException("Unexpected value: $mode")
@@ -13,14 +13,14 @@ object GalleryListParser {
throw ParseException("Can't parse gallery list", it)
}
val emptyResult = GalleryListResult(null, null, arrayListOf())
val emptyResult = GalleryListResult(null, null, [])
}
@Serializable
data class GalleryListResult(
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
val galleryInfoList: MutableList<BaseGalleryInfo>,
)
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
@@ -48,7 +48,7 @@ object GalleryListUrlParser {
ListUrlBuilder(
mode = ListUrlBuilder.MODE_TOPLIST,
jumpTo = url.parameters["p"],
mKeyword = tl,
keyword = tl,
)
} else {
null
@@ -12,7 +12,7 @@ import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.isFile
import com.ehviewer.core.files.sendTo
import com.ehviewer.core.files.toUri
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhApplication.Companion.thumbCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.download.downloadLocation
@@ -53,7 +53,7 @@ object DownloadThumbInterceptor : Interceptor {
// Accessing the recreated file immediately after deleting it throws
// FileNotFoundException, so we just overwrite the existing file.
val key = requireNotNull(chain.request.memoryCacheKey)
imageCache.read(key) {
thumbCache.read(key) {
data sendTo thumb
}
if (thumb != v1Thumb) v1Thumb.delete()
@@ -52,8 +52,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class DownloadService :
@@ -375,8 +373,8 @@ class DownloadService :
}
suspend fun run() {
channel.receiveAsFlow().sample(DELAY).collect {
when (it) {
while (true) {
when (channel.receive()) {
Ops.Notify -> {
if (ActivityCompat.checkSelfPermission(
service,
@@ -389,6 +387,7 @@ class DownloadService :
Ops.Cancel -> notifyManager.cancel(id)
Ops.StartForeground -> service.startForeground(id, builder.build())
}
delay(DELAY)
}
}
@@ -48,7 +48,7 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
return SortMode(field, order, groupByDownloadLabel)
}
val Default = SortMode(Field.TIME, Order.DESC)
val All = listOf(
val All = [
Default,
SortMode(Field.TIME, Order.ASC),
SortMode(Field.GID, Order.DESC),
@@ -57,6 +57,6 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
SortMode(Field.TITLE, Order.DESC),
SortMode(Field.PAGES, Order.ASC),
SortMode(Field.PAGES, Order.DESC),
)
]
}
}
@@ -32,8 +32,9 @@ import coil3.request.CachePolicy
import coil3.request.ErrorResult
import coil3.request.SuccessResult
import coil3.request.allowHardware
import coil3.size.Dimension
import coil3.request.maxBitmapSize
import coil3.size.Precision
import coil3.size.Scale
import coil3.size.Size
import coil3.size.SizeResolver
import com.ehviewer.core.files.openFileDescriptor
@@ -90,8 +91,10 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
}
companion object {
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
private val sizeResolver = SizeResolver(Size(targetWidth, Dimension.Undefined))
private val sizeResolver = with(appCtx.resources.displayMetrics) {
val targetSize = minOf(widthPixels, heightPixels) * 4 / 3
SizeResolver(Size(targetSize, targetSize))
}
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
val request = with(appCtx) {
@@ -99,7 +102,9 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
onLeft { data(it.source) }
onRight { data(it.source.toUri()) }
size(sizeResolver)
scale(Scale.FILL)
precision(Precision.INEXACT)
maxBitmapSize(Size.ORIGINAL)
allowHardware(false)
hardwareThreshold(Settings.hardwareBitmapThreshold.value)
maybeCropBorder(Settings.cropBorder.value)
@@ -29,7 +29,7 @@ fun <T : HttpClientEngineConfig> HttpClientConfig<T>.configureCommon(redirect: B
}
install(HttpTimeout) {
reset()
requestTimeoutMillis = 10_000
requestTimeoutMillis = 30_000
}
install(UserAgent)
defaultRequest {
@@ -32,20 +32,21 @@ import java.nio.ByteBuffer
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.job
import kotlinx.coroutines.suspendCancellableCoroutine
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
// Limit thread to 1 since we are async & non-blocking
override val dispatcher = Dispatchers.Default.limitedParallelism(1)
// TODO: Investigate https://github.com/FooIbar/EhViewer/issues/2854
// override val dispatcher = Dispatchers.Default.limitedParallelism(1)
override val supportedCapabilities = setOf(HttpTimeoutCapability)
private val executor = dispatcher.asExecutor()
private val executor = dispatcher.limitedParallelism(1).asExecutor()
private val pool = DirectByteBufferPool(32)
private val client by lazy {
with(config) { HttpEngine.Builder(context).apply(config).build() }
@@ -92,7 +93,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
}
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
chunkChan.trySend(byteBuffer).getOrThrow()
chunkChan.trySendBlocking(byteBuffer).getOrThrow()
}
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
@@ -1,14 +1,18 @@
package com.hippo.ehviewer.ktor
import com.ehviewer.core.util.isAtLeastQ
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.install
import io.ktor.client.engine.okhttp.OkHttpConfig
import okhttp3.AsyncDns
import okhttp3.android.AndroidAsyncDns
import java.net.Proxy
fun OkHttpConfig.configureClient() {
config {
if (isAtLeastQ) {
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
dns(EhDns)
proxy(Proxy.NO_PROXY)
if (!Settings.enableQuic.value) {
install(EhSSLSocketFactory)
}
}
}
@@ -43,7 +43,7 @@ private val xml = XML {
}
xmlDeclMode = XmlDeclMode.Charset
xmlVersion = XmlVersion.XML10
indent = 2
setIndent(2)
}
fun GalleryInfo.getComicInfo(): ComicInfo {
@@ -33,7 +33,6 @@ import com.ehviewer.core.files.sendTo
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
@@ -47,6 +46,7 @@ import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.download.tempDownloadDir
import com.hippo.ehviewer.image.PathSource
import com.hippo.ehviewer.jni.archiveFdBatch
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.copyTo
import com.hippo.ehviewer.util.sha1
@@ -57,6 +57,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
import okio.Path
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
class SpiderDen(val info: GalleryInfo) {
private val gid = info.gid
@@ -151,6 +153,10 @@ class SpiderDen(val info: GalleryInfo) {
removeTempFile(index)
}
fun clearCache() {
repeat(info.pages, ::removeFromCache)
}
private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) {
val name = perFilename(index, extension)
read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } }
@@ -328,7 +334,7 @@ class SpiderDen(val info: GalleryInfo) {
downloadDir?.run {
resolve(COMIC_INFO_FILE).also {
if (info !is GalleryDetail && fetchMetadata) {
EhEngine.fillGalleryListByApi(listOf(info))
EhEngine.fillGalleryListByApi([info])
}
info.getComicInfo().apply {
writeComicInfo(this, it)
@@ -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 val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
private val FileHashRegex = Regex("/h/([0-9a-f]{40})")
@@ -594,6 +594,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
previousPToken,
)
}.getOrElse {
if (it is FatalException) throw it
forceHtml = true
return@repeat
}.let {
@@ -394,7 +394,7 @@ suspend fun showMoveDownloadLabel(info: GalleryInfo) {
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
val label = if (selected == 0) null else labels[selected]
DownloadManager.changeLabel(listOf(downloadInfo), label)
DownloadManager.changeLabel([downloadInfo], label)
}
context(_: DialogState)
@@ -1,51 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.ActivityNavigator
import com.ehviewer.core.util.isAtLeastR
import com.hippo.ehviewer.Settings
import eu.kanade.tachiyomi.util.view.setSecureScreen
abstract class EhActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// https://issuetracker.google.com/204791558
// Fix system bars insets still exist in fullscreen mode on API < 30
@Suppress("DEPRECATION")
if (!isAtLeastR) {
with(window.decorView) {
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
}
}
}
override fun onResume() {
interceptSecurityOrReturn()
super.onResume()
window.setSecureScreen(Settings.enabledSecurity.value)
}
override fun finish() {
super.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}
}
@@ -24,7 +24,10 @@ import android.content.pm.verify.domain.DomainVerificationManager
import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.animateDpAsState
@@ -63,7 +66,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberDrawerState2
import androidx.compose.runtime.Composable
@@ -98,6 +101,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import androidx.compose.ui.zIndex
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.LifecycleResumeEffect
@@ -113,6 +117,7 @@ import com.ehviewer.core.ui.icons.filled.Subscriptions
import com.ehviewer.core.ui.util.LocalSnackBarFabPadding
import com.ehviewer.core.ui.util.LocalWindowSizeClass
import com.ehviewer.core.util.isAtLeastQ
import com.ehviewer.core.util.isAtLeastR
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhApplication.Companion.initialized
@@ -153,6 +158,7 @@ import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.spec.Direction
import com.ramcosta.composedestinations.utils.currentDestinationAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import eu.kanade.tachiyomi.util.view.setSecureScreen
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
@@ -175,7 +181,7 @@ private val navItems = arrayOf<Triple<Direction, Int, ImageVector>>(
Triple(SettingsScreenDestination, R.string.settings, Icons.Default.Settings),
)
class MainActivity : EhActivity() {
class MainActivity : AppCompatActivity() {
private var shareUrl: String? = null
@@ -203,6 +209,15 @@ class MainActivity : EhActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen().setKeepOnScreenCondition { !initialized }
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// https://issuetracker.google.com/204791558
// Fix system bars insets still exist in fullscreen mode on API < 30
@Suppress("DEPRECATION")
if (!isAtLeastR) {
with(window.decorView) {
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
}
}
setMD3Content {
val configuration = LocalConfiguration.current
val navDrawerState = rememberDrawerState(DrawerValue.Closed)
@@ -253,6 +268,15 @@ class MainActivity : EhActivity() {
}
}
LaunchedEffect(Unit) {
Settings.enabledSecurity.valueFlow().collect {
window.setSecureScreen(it)
}
}
if (isAuthenticationSupported()) {
SecurityScreen(onError = { moveTaskToBack(true) }, modifier = Modifier.zIndex(1f))
}
val cannotParse = stringResource(R.string.error_cannot_parse_the_url)
LaunchedEffect(Unit) {
intentFlow.collect { intent ->
@@ -276,7 +300,7 @@ class MainActivity : EhActivity() {
if ("text/plain" == type) {
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
if (keyword != null && !navWithUrl(keyword)) {
navigate(ListUrlBuilder(mKeyword = keyword).asDst())
navigate(ListUrlBuilder(keyword = keyword).asDst())
}
} else if (type != null && type.startsWith("image/")) {
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
@@ -356,7 +380,7 @@ class MainActivity : EhActivity() {
var snackbarFabPadding by remember { mutableStateOf(0.dp) }
val drawerEnabled = drawerHandle.isNotEmpty()
val density = LocalDensity.current
val adaptiveInfo = currentWindowAdaptiveInfo()
val adaptiveInfo = currentWindowAdaptiveInfoV2()
val needSignIn by Settings.needSignIn.collectAsState()
val launchPage by Settings.launchPage.collectAsState()
CompositionLocalProvider(
@@ -1,102 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.AuthenticationRequest.Biometric
import androidx.biometric.AuthenticationRequest.Companion.biometricRequest
import androidx.biometric.AuthenticationResult
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.registerForAuthenticationResult
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.Settings
context(ctx: Context)
fun isAuthenticationSupported(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
return BiometricManager.from(ctx).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
class SecurityActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
if (isAuthenticationSupported()) {
startAuthentication()
} else {
onSuccess()
}
}
private val authLauncher = registerForAuthenticationResult { result ->
when (result) {
is AuthenticationResult.Success -> onSuccess()
is AuthenticationResult.Error -> onError()
}
}
@Synchronized
private fun startAuthentication() {
if (isAuthenticating) return
isAuthenticating = true
val request = biometricRequest(
title = getString(R.string.settings_privacy_require_unlock),
authFallback = Biometric.Fallback.DeviceCredential,
) {}
authLauncher.launch(request)
}
private fun onSuccess() {
isAuthenticating = false
locked = false
finish()
}
private fun onError() {
moveTaskToBack(true)
isAuthenticating = false
}
}
private var isAuthenticating = false
var locked = true
var lockedLastLeaveTime: Long = 0
val lockObserver = object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
if (!locked) {
lockedLastLeaveTime = System.currentTimeMillis() / 1000
}
locked = true
}
}
fun Context.interceptSecurityOrReturn() {
val lockedResumeTime = System.currentTimeMillis() / 1000
val lockedDelayTime = lockedResumeTime - lockedLastLeaveTime
if (lockedDelayTime < Settings.securityDelay.value * 60) {
locked = false
} else if (Settings.security.value && isAuthenticationSupported() && locked) {
startActivity(Intent(this, SecurityActivity::class.java))
}
}
@@ -0,0 +1,97 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui
import android.content.Context
import androidx.biometric.AuthenticationRequest.Biometric
import androidx.biometric.AuthenticationRequest.Companion.biometricRequest
import androidx.biometric.AuthenticationResult
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.compose.rememberAuthenticationLauncher
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.LifecycleResumeEffect
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.collectAsState
import kotlin.time.Duration.Companion.minutes
import kotlin.time.TimeSource
context(ctx: Context)
fun isAuthenticationSupported(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
return BiometricManager.from(ctx).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
@Composable
fun SecurityScreen(onError: () -> Unit, modifier: Modifier) {
val security by Settings.security.collectAsState()
var locked by remember { mutableStateOf(security) }
var isAuthenticating by remember { mutableStateOf(false) }
val title = stringResource(R.string.settings_privacy_require_unlock)
val launcher = rememberAuthenticationLauncher { result ->
isAuthenticating = false
when (result) {
is AuthenticationResult.Success -> locked = false
else -> onError()
}
}
@Synchronized
fun startAuthentication() {
if (isAuthenticating) return
isAuthenticating = true
val request = biometricRequest(title, Biometric.Fallback.DeviceCredential) {}
launcher.launch(request)
}
val securityDelay by Settings.securityDelay.collectAsState { it.minutes }
var leaveTime by remember { mutableStateOf(TimeSource.Monotonic.markNow()) }
LifecycleResumeEffect(Unit) {
if (security && leaveTime.elapsedNow() > securityDelay) {
locked = true
}
onPauseOrDispose {
if (!locked) {
leaveTime = TimeSource.Monotonic.markNow()
}
}
}
if (locked) {
val windowInfo = LocalWindowInfo.current
LaunchedEffect(windowInfo) {
snapshotFlow { windowInfo.isWindowFocused }.collect {
if (it) startAuthentication()
}
}
Surface(modifier = modifier.fillMaxSize()) {}
}
}
@@ -7,6 +7,7 @@ import coil3.decode.DecodeUtils
import coil3.imageLoader
import coil3.request.crossfade
import coil3.size.Scale
import coil3.size.Size
import com.ehviewer.core.util.toIntOrDefault
import com.hippo.ehviewer.ktbuilder.imageRequest
import kotlin.math.roundToInt
@@ -21,7 +22,7 @@ class CoilImageGetter(private val onSuccess: () -> Unit) : Html.ImageGetter {
val srcHeight = groupValues[2].toInt()
val dstWidth = groupValues[3].toIntOrDefault(200)
val dstHeight = dstWidth / 2 * 3
val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT)
val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT, Size.ORIGINAL)
setBounds(0, 0, (srcWidth * multiplier).roundToInt(), (srcHeight * multiplier).roundToInt())
} ?: setBounds(0, 0, 200, 300)
with(appCtx) {
@@ -31,6 +31,7 @@ fun AnimatedVisibilityScope.WebViewSignInScreen(navigator: DestinationsNavigator
Settings.desktopSite.value = false
}
if (EhCookieStore.hasSignedIn()) {
EhCookieStore.flush()
postLogin()
state.webView?.destroy()
bgWork { awaitCancellation() }
@@ -47,7 +47,7 @@ import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitResult
import kotlinx.coroutines.CoroutineScope
private val categoryTable = arrayOf(
private val categoryTable = [
EhUtils.DOUJINSHI to R.string.doujinshi,
EhUtils.MANGA to R.string.manga,
EhUtils.ARTIST_CG to R.string.artist_cg,
@@ -58,7 +58,7 @@ private val categoryTable = arrayOf(
EhUtils.COSPLAY to R.string.cosplay,
EhUtils.ASIAN_PORN to R.string.asian_porn,
EhUtils.MISC to R.string.misc,
)
]
@Composable
context(_: DialogState, _: CoroutineScope, _: Context)
@@ -193,9 +193,6 @@ fun SearchFilter(
}
if (error != null) {
ListItem(
headlineContent = {
Text(text = error, style = MaterialTheme.typography.bodySmall)
},
leadingContent = {
Icon(imageVector = Icons.Default.Info, contentDescription = null)
},
@@ -204,6 +201,9 @@ fun SearchFilter(
headlineColor = MaterialTheme.colorScheme.error,
leadingIconColor = MaterialTheme.colorScheme.error,
),
content = {
Text(text = error, style = MaterialTheme.typography.bodySmall)
},
)
}
}
@@ -10,10 +10,6 @@ import androidx.compose.material.icons.filled.Brightness5
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
@@ -23,6 +19,8 @@ import androidx.core.graphics.green
import androidx.core.graphics.red
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.component.RollingNumber
import com.ehviewer.core.ui.component.rememberIntPrefSliderState
import com.ehviewer.core.ui.component.rememberIntSliderState
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
@@ -34,12 +32,11 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
field = customBrightness,
)
AnimatedVisibility(visible = customBrightness.value) {
val brightness = Settings.customBrightnessValue.asMutableState()
val brightness = rememberIntPrefSliderState(Settings.customBrightnessValue, -75..100)
SliderChoice(
startSlot = { Icon(imageVector = Icons.Default.Brightness5, contentDescription = null) },
endSlot = { RollingNumber(number = brightness.value, length = 3) },
range = -75..100,
field = brightness,
state = brightness,
)
}
val colorFilter = Settings.colorFilter.asMutableState()
@@ -48,47 +45,39 @@ fun ColorFilterSetting() = Column(modifier = Modifier.verticalScroll(rememberScr
field = colorFilter,
)
AnimatedVisibility(visible = colorFilter.value) {
var color by Settings.colorFilterValue.asMutableState()
val rf = remember { mutableIntStateOf(color.red) }
val r by rf
val gf = remember { mutableIntStateOf(color.green) }
val g by gf
val bf = remember { mutableIntStateOf(color.blue) }
val b by bf
val af = remember { mutableIntStateOf(color.alpha) }
val a by af
color = (a shl 24) or (r shl 16) or (g shl 8) or (b)
var color by Settings.colorFilterValue
val r = rememberIntSliderState(color.red, 0..255)
val g = rememberIntSliderState(color.green, 0..255)
val b = rememberIntSliderState(color.blue, 0..255)
val a = rememberIntSliderState(color.alpha, 0..255)
color = (a.value shl 24) or (r.value shl 16) or (g.value shl 8) or (b.value)
Column {
SliderChoice(
startSlot = { Text(text = "R") },
endSlot = { RollingNumber(number = r, length = 3) },
range = 0..255,
field = rf,
endSlot = { RollingNumber(number = r.value, length = 3) },
state = r,
)
SliderChoice(
startSlot = { Text(text = "G") },
endSlot = { RollingNumber(number = g, length = 3) },
range = 0..255,
field = gf,
endSlot = { RollingNumber(number = g.value, length = 3) },
state = g,
)
SliderChoice(
startSlot = { Text(text = "B") },
endSlot = { RollingNumber(number = b, length = 3) },
range = 0..255,
field = bf,
endSlot = { RollingNumber(number = b.value, length = 3) },
state = b,
)
SliderChoice(
startSlot = { Text(text = "A") },
endSlot = { RollingNumber(number = a, length = 3) },
range = 0..255,
field = af,
endSlot = { RollingNumber(number = a.value, length = 3) },
state = a,
)
}
}
SpinnerChoice(
title = stringResource(id = R.string.pref_color_filter_mode),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.color_filter_modes),
values = listOf(0, 1, 2, 3, 4, 5),
values = [0, 1, 2, 3, 4, 5],
field = Settings.colorFilterMode.asMutableState(),
)
SwitchChoice(
@@ -1,6 +1,11 @@
package com.hippo.ehviewer.ui.reader
import android.graphics.drawable.Animatable
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -11,8 +16,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.WavyProgressIndicatorDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -50,7 +57,6 @@ import com.hippo.ehviewer.gallery.progressObserved
import com.hippo.ehviewer.gallery.statusObserved
import com.hippo.ehviewer.image.Image
import com.hippo.ehviewer.util.AdsPlaceholderFile
import eu.kanade.tachiyomi.ui.reader.viewer.CombinedCircularProgressIndicator
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.drop
@@ -83,7 +89,22 @@ fun PagerItem(
modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT),
contentAlignment = Alignment.Center,
) {
CombinedCircularProgressIndicator(progress = state.progressObserved)
AnimatedContent(
targetState = state is PageStatus.Loading,
transitionSpec = { fadeIn() togetherWith fadeOut() },
label = "progressState",
) { determinate ->
val animatedProgress by animateFloatAsState(
targetValue = state.progressObserved,
animationSpec = WavyProgressIndicatorDefaults.ProgressAnimationSpec,
label = "progress",
)
if (determinate) {
CircularWavyProgressIndicator(progress = { animatedProgress })
} else {
CircularWavyProgressIndicator()
}
}
}
}
is PageStatus.Ready -> {
@@ -58,7 +58,7 @@ private fun PagerSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_viewer_nav),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.pager_nav),
values = listOf(0, 1, 2, 3, 4, 5),
values = [0, 1, 2, 3, 4, 5],
field = pagerNav,
)
AnimatedVisibility(visible = pagerNav.value != 5) {
@@ -66,7 +66,7 @@ private fun PagerSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
values = listOf(0, 1, 2, 3),
values = [0, 1, 2, 3],
field = Settings.readerPagerNavInverted.asMutableState(),
)
SwitchChoice(
@@ -79,7 +79,7 @@ private fun PagerSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_image_scale_type),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.image_scale_type),
values = listOf(1, 2, 3, 4, 5, 6),
values = [1, 2, 3, 4, 5, 6],
field = scaleType,
)
AnimatedVisibility(visible = scaleType.value == 1) {
@@ -91,7 +91,7 @@ private fun PagerSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_zoom_start),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.zoom_start),
values = listOf(1, 2, 3, 4),
values = [1, 2, 3, 4],
field = Settings.zoomStart.asMutableState(),
)
SwitchChoice(
@@ -111,14 +111,14 @@ private fun WebtoonSetting() = Column {
SpinnerChoice(
title = stringResource(id = R.string.pref_viewer_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,
)
AnimatedVisibility(visible = webtoonNav.value != 5) {
SpinnerChoice(
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
entries = stringArrayResource(id = com.hippo.ehviewer.R.array.invert_tapping_mode),
values = listOf(0, 1, 2, 3),
values = [0, 1, 2, 3],
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.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.BottomSheetDefaults
@@ -26,9 +25,10 @@ import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetValue
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -92,6 +92,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderContentOverlay
import eu.kanade.tachiyomi.ui.reader.ReaderPageSheetMeta
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.merge
@@ -214,9 +215,9 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
uiController.showTransientSystemBarsBySwipe = false
}
}
val lazyListState = rememberLazyListState(LazyLayoutCacheWindow(SCROLL_FRACTION, SCROLL_FRACTION), pageLoader.startPage)
val lazyListState = rememberLazyListState(pageLoader.startPage)
val pagerState = rememberPagerState(pageLoader.startPage) { pageLoader.size }
val syncState = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
val sync = rememberSliderPagerDoubleSyncState(lazyListState, pagerState, pageLoader)
var appbarVisible by remember { mutableStateOf(false) }
val isWebtoon by rememberUpdatedState(ReadingModeType.isWebtoon(readingMode))
val focusRequester = remember { FocusRequester() }
@@ -231,7 +232,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
syncState.Sync(isWebtoon) { appbarVisible = false }
sync.Sync(isWebtoon) { appbarVisible = false }
val bgColor by collectBackgroundColorAsState()
val isDarkTheme = isSystemInDarkTheme()
LaunchedEffect(isDarkTheme) {
@@ -251,7 +252,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
val blocked = page.status is PageStatus.Blocked
dialog { cont ->
fun dispose() = cont.resume(Unit)
val state = rememberModalBottomSheetState()
val state = rememberBottomSheetState(SheetValue.Hidden)
ModalBottomSheet(
onDismissRequest = { dispose() },
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
@@ -292,10 +293,10 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
onNavigationModeChange = { showNavigationOverlay = true },
onSelectPage = onSelectPage,
onMenuRegionClick = { appbarVisible = !appbarVisible },
modifier = Modifier.background(bgColor).pointerInput(syncState) {
modifier = Modifier.background(bgColor).pointerInput(sync) {
awaitEachGesture {
waitForUpOrCancellation()
syncState.reset()
sync.reset()
showNavigationOverlay = false
}
}.fillMaxSize().windowInsetsPadding(insets),
@@ -323,7 +324,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
)
if (brightness) {
LaunchedEffect(Unit) {
Settings.customBrightnessValue.valueFlow().sample(100)
Settings.customBrightnessValue.valueFlow().sample(100.milliseconds)
.onCompletion { activity.setCustomBrightnessValue(0) }
.collect { activity.setCustomBrightnessValue(it) }
}
@@ -332,7 +333,7 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
if (showPageNumber && !appbarVisible) {
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodySmall) {
PageIndicatorText(
currentPage = syncState.sliderValue,
currentPage = sync.slider.value,
totalPages = pageLoader.size,
modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
)
@@ -343,9 +344,9 @@ fun ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?) {
title = pageLoader.title,
isRtl = readingMode == ReadingModeType.RIGHT_TO_LEFT,
showSeekBar = showSeekbar,
currentPage = syncState.sliderValue,
slider = sync.slider,
totalPages = pageLoader.size,
onSliderValueChange = syncState::sliderScrollTo,
onSliderValueChange = sync::sliderScrollTo,
onClickSettings = {
launch {
dialog { cont ->
@@ -6,11 +6,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import com.ehviewer.core.ui.component.IntSliderState
import com.hippo.ehviewer.gallery.PageLoader
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
@@ -23,12 +23,11 @@ class SliderPagerDoubleSync(
private val pageLoader: PageLoader,
) {
private var sliderFollowPager by mutableStateOf(true)
var sliderValue by mutableIntStateOf(pageLoader.startPage + 1)
private set
val slider = IntSliderState(pageLoader.startPage + 1, 1..pageLoader.size)
fun sliderScrollTo(index: Int) {
sliderFollowPager = false
sliderValue = index.coerceIn(1, pageLoader.size)
slider.value = index
}
fun reset() {
@@ -50,7 +49,7 @@ class SliderPagerDoubleSync(
@Composable
fun Sync(webtoon: Boolean, onPageSelected: () -> Unit) {
val currentIndexFlow = remember(webtoon) {
val initialIndex = sliderValue - 1
val initialIndex = slider.value - 1
sliderFollowPager = if (webtoon) {
lazyListState.firstVisibleItemIndex == initialIndex
} else {
@@ -61,14 +60,14 @@ class SliderPagerDoubleSync(
if (sliderFollowPager) {
LaunchedEffect(currentIndexFlow) {
currentIndexFlow.drop(1).collect { index ->
sliderValue = index + 1
slider.value = index + 1
pageLoader.startPage = index
onPageSelected()
}
}
} else {
LaunchedEffect(webtoon) {
snapshotFlow { sliderValue - 1 }.collectLatest { index ->
snapshotFlow { slider.value - 1 }.collectLatest { index ->
if (webtoon) {
lazyListState.scrollToItem(index)
} else {
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -81,6 +82,7 @@ fun WebtoonViewer(
state = lazyListState,
contentPadding = PaddingValues(horizontal = sidePadding),
verticalArrangement = Arrangement.spacedBy(if (withGaps) 15.dp else 0.dp),
cacheWindow = LazyLayoutCacheWindow(SCROLL_FRACTION, SCROLL_FRACTION),
) {
items(items, key = { it.index }) { page ->
PagerItem(
@@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.IntSliderState
import com.ehviewer.core.ui.component.Slider
@Composable
@@ -84,20 +85,17 @@ fun SwitchChoice(title: String, field: MutableState<Boolean>) {
fun SliderChoice(
startSlot: @Composable () -> Unit,
endSlot: @Composable () -> Unit,
range: IntRange,
field: MutableState<Int>,
state: IntSliderState,
) {
var value by field
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
startSlot()
Slider(
value = value,
onValueChange = { value = it },
state = state,
onValueChange = { state.value = it },
modifier = Modifier.weight(1f).padding(8.dp),
valueRange = range,
)
endSlot()
}
@@ -159,7 +159,6 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
) {
faves.forEachIndexed { index, (name, count) ->
ListItem(
headlineContent = { Text(text = name) },
trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) },
modifier = Modifier.clip(CardDefaults.shape).clickable {
val newCat = index - 2
@@ -169,6 +168,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator, v
launch { sheetState.close() }
},
colors = listItemOnDrawerColor(urlBuilder.favCat == index - 2),
content = { Text(text = name) },
)
}
}
@@ -260,12 +260,8 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
Column {
data.forEach { (name, vote) ->
ListItem(
headlineContent = {
Text(text = name)
},
trailingContent = {
Text(text = vote)
},
trailingContent = { Text(text = vote) },
content = { Text(text = name) },
)
}
}
@@ -409,7 +405,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_UPLOADER,
mKeyword = item.user,
keyword = item.user,
).asDst(),
)
},
@@ -215,7 +215,7 @@ fun GalleryDetailContent(
if (uploader.isNullOrEmpty() || disowned) {
return
}
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, mKeyword = uploader).asDst())
navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_UPLOADER, keyword = uploader).asDst())
}
fun onGalleryInfoCardClick() {
@@ -529,21 +529,21 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_NORMAL,
mKeyword = "\"" + keyword + "\"",
keyword = "\"" + keyword + "\"",
).asDst(),
)
} else if (artistTag != null) {
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_TAG,
mKeyword = artistTag,
keyword = artistTag,
).asDst(),
)
} else if (null != galleryDetail.uploader) {
navigate(
ListUrlBuilder(
mode = ListUrlBuilder.MODE_UPLOADER,
mKeyword = galleryDetail.uploader,
keyword = galleryDetail.uploader,
).asDst(),
)
}
@@ -721,7 +721,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) {
val upTag = stringResource(R.string.tag_vote_up)
val downTag = stringResource(R.string.tag_vote_down)
val withDraw = stringResource(R.string.tag_vote_withdraw)
fun search(tag: String) = navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, mKeyword = tag).asDst())
fun search(tag: String) = navigate(ListUrlBuilder(mode = ListUrlBuilder.MODE_TAG, keyword = tag).asDst())
GalleryTags(
tagGroups = tags,
onTagClick = ::search,
@@ -49,14 +49,12 @@ import com.ehviewer.core.ui.util.rememberInVM
import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.fillInfo
import com.hippo.ehviewer.client.getImageKey
import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ktbuilder.executeIn
@@ -250,10 +248,7 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
) {
Text(text = stringResource(id = R.string.clear_image_cache_confirm))
}
repeat(gd.pages) {
val key = getImageKey(gd.gid, it)
imageCache.remove(key)
}
SpiderDen(gd).clearCache()
snackbar(cacheCleared)
}
},
@@ -147,7 +147,7 @@ fun AnimatedVisibilityScope.WhatshotScreen(navigator: DestinationsNavigator) = G
@Destination<RootGraph>
@Composable
fun AnimatedVisibilityScope.ToplistScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_TOPLIST, mKeyword = Settings.recentToplist), navigator)
fun AnimatedVisibilityScope.ToplistScreen(navigator: DestinationsNavigator) = GalleryListScreen(ListUrlBuilder(MODE_TOPLIST, keyword = Settings.recentToplist), navigator)
@Destination<RootGraph>
@Composable
@@ -172,10 +172,14 @@ fun AnimatedVisibilityScope.GalleryListScreen(
LaunchedEffect(urlBuilder) {
if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category
var keyword = urlBuilder.keyword.takeUnless { urlBuilder.mode == MODE_TOPLIST }.orEmpty()
if (urlBuilder.mode == MODE_TAG) {
keyword = wrapTagKeyword(keyword)
}
val keyword = urlBuilder.keyword?.let { keyword ->
when (urlBuilder.mode) {
MODE_UPLOADER -> "uploader:\"$keyword\""
MODE_TAG -> wrapTagKeyword(keyword)
MODE_TOPLIST -> ""
else -> keyword
}
}.orEmpty()
searchFieldState.setTextAndPlaceCursorAtEnd(keyword)
}
@@ -217,15 +221,13 @@ fun AnimatedVisibilityScope.GalleryListScreen(
ListItem(
modifier = Modifier.padding(horizontal = 4.dp).clip(CardDefaults.shape).clickable {
Settings.recentToplist = keyword
urlBuilder = ListUrlBuilder(MODE_TOPLIST, mKeyword = keyword)
urlBuilder = ListUrlBuilder(MODE_TOPLIST, keyword = keyword)
data.refresh()
fabHidden = false
launch { sheetState.close() }
},
headlineContent = {
Text(text = name)
},
colors = listItemOnDrawerColor(urlBuilder.keyword == keyword),
content = { Text(text = name) },
)
}
}
@@ -480,9 +482,9 @@ fun AnimatedVisibilityScope.GalleryListScreen(
searchFieldState = searchFieldState,
suggestionProvider = {
GalleryDetailUrlParser.parse(it, false)?.run {
listOf(GalleryDetailUrlSuggestion(gid, token))
[GalleryDetailUrlSuggestion(gid, token)]
} ?: GalleryPageUrlParser.parse(it, false)?.run {
listOf(GalleryPageUrlSuggestion(gid, pToken, page))
[GalleryPageUrlSuggestion(gid, pToken, page)]
}.orEmpty()
},
localSearch = false,
@@ -578,7 +580,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(
onExpandChanged = { fabExpanded = it },
autoCancel = true,
) {
if (urlBuilder.mode in arrayOf(MODE_NORMAL, MODE_UPLOADER, MODE_TAG)) {
if (urlBuilder.mode in [MODE_NORMAL, MODE_UPLOADER, MODE_TAG]) {
onClick(Icons.Default.Shuffle) {
urlBuilder.setRange(Random.nextInt(100))
data.refresh()
@@ -641,6 +643,7 @@ private fun getSuitableTitleForUrlBuilder(urlBuilder: ListUrlBuilder, appName: B
val canTranslate = Settings.showTagTranslations.value && EhTagDatabase.translatable && EhTagDatabase.initialized
wrapTagKeyword(keyword, canTranslate)
}
MODE_UPLOADER -> "uploader:\"$keyword\""
else -> keyword
}
} else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) {
@@ -294,7 +294,6 @@ fun SearchBarScreen(
item {}
items(mSuggestionList, key = { it.keyword.hashCode() * 31 + it.canDelete.hashCode() }) {
ListItem(
headlineContent = { Text(text = it.keyword) },
supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) },
leadingContent = it.canOpenDirectly.ifTrueThen {
Icon(
@@ -312,6 +311,7 @@ fun SearchBarScreen(
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
modifier = Modifier.clickable { it.onClick() }.thenIf(animateItems) { animateItem() },
content = { Text(text = it.keyword) },
)
}
}
@@ -36,7 +36,7 @@ import com.hippo.ehviewer.ui.main.NavigationIcon
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.updater.AppUpdater
import com.hippo.ehviewer.updater.Release
import com.hippo.ehviewer.updater.EhRelease
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.displayString
@@ -49,8 +49,8 @@ import moe.tarsin.navigate
import moe.tarsin.snackbar
import moe.tarsin.string
private const val REPO_URL = "https://github.com/${BuildConfig.REPO_NAME}"
private const val RELEASE_URL = "$REPO_URL/releases"
const val REPO_URL = "https://codeberg.org/${BuildConfig.REPO_NAME}"
const val RELEASE_URL = "$REPO_URL/releases"
@Composable
@Stable
@@ -124,7 +124,7 @@ fun AnimatedVisibilityScope.AboutScreen(navigator: DestinationsNavigator) = Scre
}
context(_: Context, _: DialogState)
suspend fun showNewVersion(release: Release) {
suspend fun showNewVersion(release: EhRelease) {
awaitConfirmationOrCancel(
confirmText = R.string.download,
title = R.string.new_version_available,
@@ -63,6 +63,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.merge
@@ -214,7 +215,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
step = 3,
title = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold),
summary = stringResource(id = R.string.settings_advanced_hardware_bitmap_threshold_summary),
state = Settings.hardwareBitmapThreshold.asMutableState(),
pref = Settings.hardwareBitmapThreshold,
)
}
SwitchPreference(
@@ -289,7 +290,7 @@ fun AnimatedVisibilityScope.AdvancedScreen(navigator: DestinationsNavigator) = S
EhDB.putLocalFavorites(result.galleryInfoList)
launchSnackbar(string(R.string.settings_advanced_backup_favorite_start, status))
if (result.next != null) {
delay(Settings.downloadDelay.value.toLong())
delay(Settings.downloadDelay.value.milliseconds)
favListUrlBuilder.setIndex(result.next, true)
doBackup()
}
@@ -195,7 +195,7 @@ fun AnimatedVisibilityScope.DownloadScreen(navigator: DestinationsNavigator) = S
maxValue = 10,
minValue = MIN_SPEED_LEVEL,
title = stringResource(id = R.string.settings_download_timeout_speed),
state = Settings.timeoutSpeed.asMutableState(),
pref = Settings.timeoutSpeed,
display = ::speedLevelToSpeed,
)
val preloadImage = Settings.preloadImage.asMutableState()
@@ -193,7 +193,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
minValue = 20,
step = 7,
title = stringResource(id = R.string.list_tile_thumb_size),
state = Settings.listThumbSize.asMutableState(),
pref = Settings.listThumbSize,
)
SimpleMenuPreferenceInt(
title = stringResource(id = R.string.settings_eh_detail_size),
@@ -207,7 +207,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
maxValue = 10,
minValue = 1,
title = stringResource(id = R.string.settings_eh_thumb_columns),
state = Settings.thumbColumns.asMutableState(),
pref = Settings.thumbColumns,
)
SwitchPreference(
title = stringResource(id = R.string.settings_eh_show_gallery_pages),
@@ -235,7 +235,7 @@ fun AnimatedVisibilityScope.EhScreen(navigator: DestinationsNavigator) = Screen(
minValue = -101,
title = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold),
summary = stringResource(id = R.string.settings_eh_show_gallery_comment_threshold_summary),
state = Settings.commentThreshold.asMutableState(),
pref = Settings.commentThreshold,
)
}
if (EhTagDatabase.translatable) {
@@ -26,6 +26,7 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenu
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
@@ -1,8 +1,6 @@
package com.hippo.ehviewer.ui.settings
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@@ -12,14 +10,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.main.NavigationIcon
import com.hippo.ehviewer.ui.openBrowser
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.variant.LibrariesVariant
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryActionKind
import com.mikepenz.aboutlibraries.ui.compose.variant.LibraryBadges
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -42,10 +41,19 @@ fun AnimatedVisibilityScope.LicenseScreen(navigator: DestinationsNavigator) = Sc
libraries = libraries,
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
contentPadding = paddingValues,
padding = LibraryDefaults.libraryPadding(licensePadding = LibraryDefaults.chipPadding(contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp))),
textStyles = LibraryDefaults.libraryTextStyles(licensesTextStyle = MaterialTheme.typography.labelSmall),
onLibraryClick = { library ->
library.website?.let { openBrowser(it) }
badges = LibraryBadges(funding = false),
variant = LibrariesVariant.Refined,
onActionClick = { lib, kind ->
val open = { url: String -> url.isNotBlank().also { if (it) openBrowser(url) } }
val website = lib.website.orEmpty()
val source = lib.scm?.url.orEmpty()
val license = lib.licenses.firstOrNull()?.url.orEmpty()
when (kind) {
LibraryActionKind.Source -> open(source)
LibraryActionKind.Website -> open(website)
LibraryActionKind.License -> open(license)
LibraryActionKind.Sponsor -> false
}
},
)
}
@@ -21,7 +21,9 @@ import androidx.compose.ui.res.integerArrayResource
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import com.ehviewer.core.preferences.PrefDelegate
import com.ehviewer.core.ui.component.RollingNumber
import com.ehviewer.core.ui.component.rememberIntPrefSliderState
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.util.ProgressDialog
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -71,19 +73,16 @@ fun SwitchPreference(title: String, summary: String? = null, state: MutableState
}
@Composable
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, title: String, summary: String? = null, state: MutableState<Int>, enabled: Boolean = true, display: (Int) -> Int = { it }) {
val sliderState = remember { mutableIntStateOf(state.value) }
fun IntSliderPreference(maxValue: Int, minValue: Int = 0, step: Int = maxValue - minValue - 1, title: String, summary: String? = null, pref: PrefDelegate<Int>, enabled: Boolean = true, display: (Int) -> Int = { it }) {
val state = rememberIntPrefSliderState(pref, minValue..maxValue, step)
IntSliderPreference(
state = state,
title = { Text(title) },
valueRange = minValue..maxValue,
valueSteps = step,
sliderState = sliderState,
enabled = enabled,
summary = summary?.let { { Text(it) } },
valueText = {
RollingNumber(
number = display(sliderState.intValue),
number = display(state.value),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
length = 5,
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
@@ -17,6 +18,7 @@ import com.ehviewer.core.util.launch
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.asMutableState
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.isAuthenticationSupported
import com.hippo.ehviewer.ui.main.NavigationIcon
@@ -48,17 +50,17 @@ fun AnimatedVisibilityScope.PrivacyScreen(navigator: DestinationsNavigator) = Sc
enabled = isAuthenticationSupported(),
)
AnimatedVisibility(visible = security.value) {
val securityDelay = Settings.securityDelay.asMutableState()
val summary = if (securityDelay.value == 0) {
val delay by Settings.securityDelay.collectAsState()
val summary = if (delay == 0) {
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary_immediately)
} else {
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary, securityDelay.value)
stringResource(id = R.string.settings_privacy_require_unlock_delay_summary, delay)
}
IntSliderPreference(
maxValue = 30,
title = stringResource(id = R.string.settings_privacy_require_unlock_delay),
summary = summary,
state = securityDelay,
pref = Settings.securityDelay,
enabled = isAuthenticationSupported(),
)
}
@@ -94,7 +94,7 @@ fun AnnotatedString.normalizeSpan(): AnnotatedString {
val italic = filterMerged(SpanStyle(fontStyle = FontStyle.Italic)) { it.fontStyle == FontStyle.Italic }
val underline = filterMerged(SpanStyle(textDecoration = TextDecoration.Underline)) { it.textDecoration == TextDecoration.Underline }
val linethrough = filterMerged(SpanStyle(textDecoration = TextDecoration.LineThrough)) { it.textDecoration == TextDecoration.LineThrough }
listOf(bold, italic, underline, linethrough).flatten()
bold + italic + underline + linethrough
}
return AnnotatedString(text, spans)
}
@@ -42,6 +42,7 @@ import androidx.compose.material3.DatePickerDefaults
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenu
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
@@ -684,10 +685,10 @@ suspend fun awaitSelectItemWithIcon(
}
itemsIndexed(items) { index, (icon, text) ->
ListItem(
headlineContent = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) },
modifier = Modifier.clickable { resume(index) }.padding(horizontal = 8.dp),
leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) },
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
content = { Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium) },
)
}
}
@@ -756,17 +757,17 @@ private fun CheckableItem(text: String, checked: Boolean, modifier: Modifier = M
val textStyle = MaterialTheme.typography.titleMedium
val checkedColor = MaterialTheme.colorScheme.primary
ListItem(
headlineContent = {
Text(
text = text,
style = if (checked) textStyle.copy(color = checkedColor) else textStyle,
)
},
modifier = modifier,
trailingContent = checked.ifTrueThen {
Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = checkedColor)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
content = {
Text(
text = text,
style = if (checked) textStyle.copy(color = checkedColor) else textStyle,
)
},
)
}
@@ -9,7 +9,6 @@ import com.hippo.ehviewer.spider.timeoutBySpeed
import com.hippo.ehviewer.util.copyTo
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.accept
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.ContentType
@@ -21,17 +20,18 @@ import kotlin.time.Instant
import kotlinx.io.asSource
import moe.tarsin.coroutines.runSuspendCatching
import okio.Path
import tachiyomi.data.release.GithubArtifacts
import tachiyomi.data.release.GithubCommitComparison
import tachiyomi.data.release.GithubRelease
import tachiyomi.data.release.GithubRepo
import tachiyomi.data.release.GithubWorkflowRuns
import tachiyomi.data.release.Artifacts
import tachiyomi.data.release.CommitComparison
import tachiyomi.data.release.Release
import tachiyomi.data.release.Repository
import tachiyomi.data.release.WorkflowRuns
import tachiyomi.data.release.downloadLink
private const val API_URL = "https://api.github.com/repos/${BuildConfig.REPO_NAME}"
private const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
const val API_URL = "https://codeberg.org/api/v1/repos/${BuildConfig.REPO_NAME}"
const val LATEST_RELEASE_URL = "$API_URL/releases/latest"
object AppUpdater {
suspend fun checkForUpdate(forceCheck: Boolean = false): Release? {
suspend fun checkForUpdate(forceCheck: Boolean = false): EhRelease? {
val now = Clock.System.now()
val last = Instant.fromEpochSeconds(Settings.lastUpdateTime)
val interval = Settings.updateIntervalDays.value
@@ -39,31 +39,30 @@ object AppUpdater {
Settings.lastUpdateTime = now.epochSeconds
if (Settings.useCIUpdateChannel.value) {
val curSha = BuildConfig.COMMIT_SHA
val branch = ghStatement(API_URL).executeAndParseAs<GithubRepo>().defaultBranch
val workflowRunsUrl = "$API_URL/actions/workflows/ci.yml/runs?branch=$branch&event=push&status=success&per_page=1"
val workflowRun = ghStatement(workflowRunsUrl).executeAndParseAs<GithubWorkflowRuns>().workflowRuns[0]
val shortSha = workflowRun.headSha.take(7)
val branch = statement(API_URL).executeAndParseAs<Repository>().defaultBranch
val run = statement("$API_URL/actions/runs?event=push&status=success&ref=refs/heads/$branch").executeAndParseAs<WorkflowRuns>().workflowRuns[0]
val shortSha = run.headSha.take(7)
if (shortSha != curSha) {
val artifacts = ghStatement(workflowRun.artifactsUrl).executeAndParseAs<GithubArtifacts>()
val archiveUrl = artifacts.getDownloadLink()
val artifacts = statement("$API_URL/actions/runs/${run.id}/artifacts").executeAndParseAs<Artifacts>()
val archiveUrl = artifacts.downloadLink
val changelog = runSuspendCatching {
val commitComparisonUrl = "$API_URL/compare/$curSha...$shortSha"
val result = ghStatement(commitComparisonUrl).executeAndParseAs<GithubCommitComparison>()
val result = statement(commitComparisonUrl).executeAndParseAs<CommitComparison>()
// TODO: Prettier format, Markdown?
result.commits.joinToString("\n") { commit ->
"${commit.commit.message.takeWhile { it != '\n' }} (@${commit.commit.author.name})"
}
}.getOrDefault(workflowRun.title)
return Release(shortSha, changelog, archiveUrl)
}.getOrDefault(run.title)
return EhRelease(shortSha, changelog, archiveUrl)
}
} else {
val curVersion = BuildConfig.RAW_VERSION_NAME
val release = ghStatement(LATEST_RELEASE_URL).executeAndParseAs<GithubRelease>()
val release = statement(LATEST_RELEASE_URL).executeAndParseAs<Release>()
val latestVersion = release.version
val description = release.info
val downloadUrl = release.getDownloadLink()
if (latestVersion != curVersion) {
return Release(latestVersion, description, downloadUrl)
return EhRelease(latestVersion, description, downloadUrl)
}
}
}
@@ -75,7 +74,7 @@ object AppUpdater {
timeoutBySpeed(
url,
{
ghStatement(url) {
statement(url) {
// https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#get-a-release-asset
if (!isZip) accept(ContentType.Application.OctetStream)
it()
@@ -98,22 +97,14 @@ object AppUpdater {
}
}
private suspend inline fun ghStatement(
suspend inline fun statement(
url: String,
builder: HttpRequestBuilder.() -> Unit = {},
) = ktorClient.prepareGet(url) {
bearerAuth(GithubTokenParts.joinToString("_"))
apply(builder)
}
private val GithubTokenParts = arrayOf(
"github",
"pat",
"11A4H2ACI0iGDuL1O6wPYW",
"OTFg8xaCNUwR1NHaJE1AT3LoYPfz6bouI7E7ReLf8GjIRFHCL5UsHL9EnWP",
)
data class Release(
data class EhRelease(
val version: String,
val changelog: String,
val downloadLink: String,
@@ -39,18 +39,14 @@ object AppConfig {
private const val CRASH = "crash"
private const val TAG_TRANSLATIONS = "tag-translations"
private val abi = Build.SUPPORTED_ABIS[0].takeIf {
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a")
} ?: "universal"
private val abi = Build.SUPPORTED_ABIS[0].takeIf { it in ["arm64-v8a", "x86_64", "armeabi-v7a"] } ?: "universal"
val isBenchmark = "nonMinified" in BuildConfig.BUILD_TYPE || "benchmark" in BuildConfig.BUILD_TYPE
fun matchVariant(name: String) = name.contains(BuildConfig.FLAVOR) && name.contains(abi)
fun matchVariant(name: String) = abi in name
val commitTime = ParserUtils.formatDate(BuildConfig.COMMIT_TIME * 1000)
val isSnapshot = "SNAPSHOT" in BuildConfig.VERSION_NAME
private val externalAppDir: File?
get() {
if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
@@ -22,8 +22,8 @@ import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.os.PersistableBundle
import android.view.textclassifier.TextClassifier
import androidx.core.os.persistableBundleOf
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.ui.MainActivity
import moe.tarsin.tip
@@ -33,7 +33,9 @@ fun copyTextToClipboard(text: CharSequence?, isSensitive: Boolean) {
clipboardManager.setPrimaryClip(
ClipData.newPlainText(null, text).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isSensitive) {
description.extras = persistableBundleOf(ClipDescription.EXTRA_IS_SENSITIVE to true)
description.extras = PersistableBundle().apply {
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
}
}
},
)
@@ -16,11 +16,11 @@
package com.hippo.ehviewer.util
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalLocale
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import com.ehviewer.core.i18n.R
import com.ehviewer.core.util.toLocalDateTime
import java.util.Locale
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.datetime.LocalDate
@@ -127,7 +127,7 @@ object ReadableTime {
val timeDate = time.toLocalDateTime(timeZone).date
val nowYear = nowDate.year
val timeYear = timeDate.year
val isZh = Locale.getDefault().language == "zh"
val isZh = LocalLocale.current.language == "zh"
if (nowYear == timeYear) {
if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR
} else {
@@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenu
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.FlexibleBottomAppBar
@@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -19,6 +18,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.IntSliderState
import com.ehviewer.core.ui.component.RollingNumber
import com.ehviewer.core.ui.component.RollingNumberPlaceholder
import com.ehviewer.core.ui.component.Slider
import com.ehviewer.core.ui.component.defaultMaxTickCount
import com.ehviewer.core.ui.util.HapticFeedbackType
@@ -29,7 +31,7 @@ import com.ehviewer.core.ui.util.rememberHapticFeedback
@Composable
fun ChapterNavigator(
isRtl: Boolean,
currentPage: Int,
slider: IntSliderState,
totalPages: Int,
onSliderValueChange: (Int) -> Unit,
containerColor: Color,
@@ -40,14 +42,13 @@ fun ChapterNavigator(
modifier = Modifier.padding(horizontal = horizontalPadding).clip(CircleShape).background(containerColor).padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = "$currentPage")
val steps = totalPages - 2
RollingNumber(slider.value, length = "$totalPages".length)
val maxTickCount = defaultMaxTickCount()
val interactionSource = remember { MutableInteractionSource() }
if (steps < maxTickCount) {
if (slider.state.steps < maxTickCount) {
val sliderDragged by interactionSource.collectIsDraggedAsState()
val hapticFeedback = rememberHapticFeedback()
LaunchedEffect(currentPage) {
LaunchedEffect(slider.value) {
if (sliderDragged) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.MOVE)
}
@@ -55,13 +56,11 @@ fun ChapterNavigator(
}
Slider(
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
value = currentPage,
valueRange = 1..totalPages,
steps = steps,
state = slider,
onValueChange = onSliderValueChange,
maxTickCount = maxTickCount,
interactionSource = interactionSource,
)
Text(text = "$totalPages")
RollingNumberPlaceholder(totalPages)
}
}
@@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.IntSliderState
import com.hippo.ehviewer.ui.main.NavigationIcon
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -35,7 +36,7 @@ fun BoxScope.ReaderAppBars(
title: String,
isRtl: Boolean,
showSeekBar: Boolean,
currentPage: Int,
slider: IntSliderState,
totalPages: Int,
onSliderValueChange: (Int) -> Unit,
onClickSettings: () -> Unit,
@@ -65,7 +66,7 @@ fun BoxScope.ReaderAppBars(
if (showSeekBar && totalPages > 1) {
ChapterNavigator(
isRtl = isRtl,
currentPage = currentPage,
slider = slider,
totalPages = totalPages,
onSliderValueChange = onSliderValueChange,
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() {
override val originalRegions = listOf(
override val originalRegions = [
Region(
rect = Rect(0f, 0f, 0.33f, 1f),
type = NavigationRegion.NEXT,
@@ -27,5 +27,5 @@ object EdgeNavigation : ViewerNavigation() {
rect = Rect(0.66f, 0f, 1f, 1f),
type = NavigationRegion.NEXT,
),
)
]
}
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* +---+---+---+
*/
object KindlishNavigation : ViewerNavigation() {
override val originalRegions = listOf(
override val originalRegions = [
Region(
rect = Rect(0.33f, 0.33f, 1f, 1f),
type = NavigationRegion.NEXT,
@@ -23,5 +23,5 @@ object KindlishNavigation : ViewerNavigation() {
rect = Rect(0f, 0.33f, 0.33f, 1f),
type = NavigationRegion.PREV,
),
)
]
}
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* +---+---+---+
*/
object LNavigation : ViewerNavigation() {
override val originalRegions = listOf(
override val originalRegions = [
Region(
rect = Rect(0f, 0.33f, 0.33f, 0.66f),
type = NavigationRegion.PREV,
@@ -31,5 +31,5 @@ object LNavigation : ViewerNavigation() {
rect = Rect(0f, 0.66f, 1f, 1f),
type = NavigationRegion.NEXT,
),
)
]
}
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* +---+---+---+
*/
object RightAndLeftNavigation : ViewerNavigation() {
override val originalRegions = listOf(
override val originalRegions = [
Region(
rect = Rect(0f, 0f, 0.33f, 1f),
type = NavigationRegion.LEFT,
@@ -23,5 +23,5 @@ object RightAndLeftNavigation : ViewerNavigation() {
rect = Rect(0.66f, 0f, 1f, 1f),
type = NavigationRegion.RIGHT,
),
)
]
}
@@ -21,39 +21,21 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
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.Modifier
import com.ehviewer.core.ui.component.IntSliderState
import com.ehviewer.core.ui.component.Slider
@Composable
fun IntSliderPreference(
state: MutableState<Int>,
state: IntSliderState,
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
valueRange: IntRange = 0..1,
valueSteps: Int = 0,
sliderState: MutableIntState = remember { mutableIntStateOf(state.value) },
enabled: Boolean = true,
icon: @Composable (() -> Unit)? = null,
summary: @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(
title = title,
modifier = modifier,
@@ -64,13 +46,10 @@ fun IntSliderPreference(
summary?.invoke()
Row(verticalAlignment = Alignment.CenterVertically) {
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
state = state,
onValueChange = { state.value = it },
modifier = Modifier.weight(1f),
enabled = enabled,
valueRange = valueRange,
steps = valueSteps,
onValueChangeFinished = { value = sliderValue },
)
if (valueText != null) {
val theme = LocalPreferenceTheme.current
@@ -40,6 +40,7 @@ class AndroidAsyncDns(
private val network: Network? = null,
) : AsyncDns {
@RequiresApi(Build.VERSION_CODES.Q)
@Suppress("DEPRECATION")
internal val resolver = DnsResolver.getInstance()
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.Serializable
/**
* Contains information about the latest release from GitHub.
*/
@Serializable
data class GithubRelease(
data class Release(
@SerialName("tag_name") val version: String,
@SerialName("body") val info: String,
@SerialName("html_url") val releaseLink: String,
@SerialName("assets") val assets: List<GitHubAssets>,
@SerialName("assets") val assets: List<Assets>,
) {
fun getDownloadLink(): String {
val asset = assets.find { AppConfig.matchVariant(it.name) } ?: assets[0]
@@ -20,8 +17,10 @@ data class GithubRelease(
}
}
/**
* Assets class containing download url.
*/
@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_fn = { version = "0.1", optional = true }
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"] }
log = "0.4"
android_logger = { default-features = false, version = "0.15" }
@@ -25,12 +25,12 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0"
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 }
libwebp-sys2 = { git = "https://github.com/FooIbar/libwebp-sys2-rs.git", features = ["1_5", "demux", "extern-types"] }
[dev-dependencies]
reqwest = "0.12"
reqwest = "0.13"
tokio = { version = "1", features = ["macros"] }
[profile.release]
+1
View File
@@ -256,6 +256,7 @@ where
R: Serialize,
{
jni_throwing(env, |env| {
ensure!(limit > 0, "Empty response");
let buffer = deref_mut_direct_bytebuffer(env, str)?;
// 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>(
src: &ImageBuffer<P, &[P::Subpixel]>,
) -> Luma8LuminanceSource {
) -> Result<Luma8LuminanceSource> {
let (w, h) = src.dimensions();
let mut dst = GrayImage::new(w, h);
for (to, from) in dst.pixels_mut().zip(src.pixels()) {
*to = from.to_luma8();
}
Luma8LuminanceSource::new(dst.into_raw(), w, h)
Ok(Luma8LuminanceSource::new(dst.into_raw(), w, h)?)
}
pub struct QrCode;
impl ImageConsumer<bool> for QrCode {
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));
match FinderPatternFinder::new(image.get_black_matrix()).find(&DecodeHints::default()) {
Ok(info) => {
-1
View File
@@ -1,5 +1,4 @@
#![feature(f16)]
#![feature(never_type)]
mod ffi;
pub mod img;
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::parser::utils::unescape;
use crate::regex;
use quick_xml::escape::unescape;
pub fn parse_fav_cat(html: &str) -> Vec<String> {
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::utils::unescape;
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 anyhow::{Context, Result, bail};
use chrono::naive::NaiveDateTime;
use quick_xml::escape::unescape;
use serde::Serialize;
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::utils::unescape;
use anyhow::{Result, bail};
use quick_xml::escape::unescape;
use serde::Serialize;
use tl::Parser;
use tl::VDom;
+6 -6
View File
@@ -1,10 +1,10 @@
use crate::EhError;
use crate::parser::utils::unescape;
use crate::{EHGT_PREFIX, EX_PREFIX};
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_node_attr, get_node_handle_attr, regex};
use anyhow::{Context, Result, bail};
use quick_xml::escape::unescape;
use serde::Serialize;
use std::borrow::Cow;
use std::ops::Index;
@@ -128,13 +128,13 @@ fn parse_gallery_info(node: &Node, parser: &Parser) -> Option<BaseGalleryInfo> {
.collect();
let (thumb, (thumb_height, thumb_width)) =
match tag.query_selector(parser, "[data-src]")?.next() {
None => match tag.query_selector(parser, "[src]")?.next() {
None => return None,
Some(thumb) => (
None => {
let thumb = tag.query_selector(parser, "[src]")?.next()?;
(
get_node_handle_attr(&thumb, parser, "src")?,
parse_thumb_resolution(get_node_handle_attr(&thumb, parser, "style")?),
),
},
)
}
Some(thumb) => (
get_node_handle_attr(&thumb, parser, "data-src")?,
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 anyhow::{Context, Result};
use quick_xml::escape::unescape;
use serde::Serialize;
use tl::{Parser, VDom};
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::parser::utils::unescape;
use crate::regex;
use anyhow::{Context, Result};
use quick_xml::escape::unescape;
use serde::Serialize;
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 {
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 {
alias(libs.plugins.android.test)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.baselineprofile)
}
@@ -30,7 +29,8 @@ android {
}
kotlin {
jvmToolchain(21)
val javaVersion = libs.versions.java.get().toInt()
jvmToolchain(javaVersion)
}
baselineProfile {
+2 -1
View File
@@ -30,7 +30,8 @@ gradlePlugin {
}
kotlin {
jvmToolchain(21)
val javaVersion = libs.versions.java.get().toInt()
jvmToolchain(javaVersion)
}
spotless {
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more