Compare commits

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

* fixup!

---------

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

* Fix clippy

* Fix fmt

---------

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

* Fix deprecation

* Update compileSdk version to 37

* Suppress deprecation warning for DnsResolver usage

---------

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

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-03-27 10:45:47 +00:00
renovate[bot] e0c5763a47 Update dependency androidx.compose:compose-bom-alpha to v2026.03.01 (#2932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 18:58:29 +00:00
renovate[bot] fe3fd59083 Update dependency androidx.browser:browser to v1.10.0 (#2931)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 21:29:06 +00:00
renovate[bot] 09693f91c7 Update dependency androidx.work:work-runtime to v2.11.2 (#2930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 21:28:34 +00:00
renovate[bot] b69500a441 Update benchmark to v1.5.0-alpha05 (#2928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 18:16:34 +00:00
renovate[bot] f344a9a0a5 Update gradle/actions action to v6 (#2926)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 21:23:20 +00:00
renovate[bot] ef62454c00 Update compose.multiplatform to v1.10.3 (#2924)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 21:00:47 +00:00
renovate[bot] fe2cd1b4f4 Update Gradle to v9.4.1 (#2923)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 13:16:10 +00:00
renovate[bot] 19b9fa3581 Update dependency com.diffplug.spotless to v8.4.0 (#2922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 18:17:05 +00:00
renovate[bot] 431a8565ee Update dependency io.arrow-kt:arrow-stack to v2.2.2.1 (#2921)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 13:05:56 +00:00
renovate[bot] 3880f757a5 Update aboutlibraries to v14.0.0-b03 (#2920)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 17:20:15 +00:00
FooIbar acfc81056b Fix error handling when getting original image URL 2026-03-18 01:10:45 +08:00
FooIbar 5f1200a9b3 Silence a warning 2026-03-17 21:24:21 +08:00
renovate[bot] f370ae64ad Update kotlin monorepo to v2.3.20 (#2917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 09:59:23 +00:00
renovate[bot] 509c818ed0 Update moko.resources to v0.26.1 (#2915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-15 08:49:11 +00:00
renovate[bot] 124d1ff638 Lock file maintenance (#2914)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-15 01:12:57 +00:00
renovate[bot] 0348eba0fd Update dependency androidx.activity:activity-compose to v1.13.0 (#2908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 19:08:40 +00:00
renovate[bot] 1d27292bf1 Update dependency androidx.datastore:datastore-preferences to v1.2.1 (#2912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 19:08:02 +00:00
renovate[bot]andFooIbar 1619f6cce6 Update dependency androidx.core:core-ktx to v1.18.0 (#2910)
* Update dependency androidx.core:core-ktx to v1.18.0

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-03-12 15:22:59 +00:00
renovate[bot] 489538baaa Update dependency androidx.compose:compose-bom-alpha to v2026.03.00 (#2909)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 14:02:22 +00:00
renovate[bot] 8794b7cc4d Update dependency com.squareup.okio:okio to v3.17.0 (#2911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 09:49:08 +00:00
renovate[bot] ebd114d10e Update dependency androidx.test.uiautomator:uiautomator to v2.4.0-beta02 (#2907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 00:54:01 +00:00
renovate[bot] f17dd93f27 Update benchmark to v1.5.0-alpha04 (#2905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-11 20:47:42 +00:00
renovate[bot] 899c2e14c8 Update dependency androidx.paging:paging-compose to v3.4.2 (#2906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-11 20:47:17 +00:00
renovate[bot] ae50280fb1 Update dependency me.zhanghai.compose.preference:preference to v2.2.0 (#2904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-11 09:17:34 +00:00
FooIbar a2bd809fbc Remove upstreamed patch 2026-03-11 11:02:58 +08:00
renovate[bot] 963f129bcf Update dependency libarchive to v3.8.6 (#2902)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 12:11:48 +00:00
renovate[bot] 37c0148d0b Update compose.multiplatform to v1.10.2 (#2900)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 09:55:19 +00:00
renovate[bot]andFooIbar 1fa485e31f Lock file maintenance (#2892)
* Lock file maintenance

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-03-10 09:39:45 +00:00
renovate[bot] dceea3b2ff Update Gradle to v9.4.0 (#2898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 14:18:17 +00:00
renovate[bot] bab7770857 Update dependency io.arrow-kt:arrow-stack to v2.2.2 (#2897)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 02:45:16 +00:00
renovate[bot] af9251db0c Update AGP to v9.1.0 (#2896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 16:53:31 +00:00
renovate[bot] 010b990f8d Update ktor monorepo to v3.4.1 (#2895)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:07:23 +00:00
renovate[bot] 0e61e4b00a Update spotless to v8.3.0 (#2894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 00:54:00 +00:00
renovate[bot] 66e43924bd Update dependency androidx.compose:compose-bom-alpha to v2026.02.01 (#2891)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 01:59:59 +00:00
renovate[bot] 3437bb88a2 Update actions/upload-artifact action to v7 (#2890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 18:59:16 +00:00
renovate[bot]andFooIbar f6e0733080 Update dependency io.coil-kt.coil3:coil-bom to v3.4.0 (#2888)
* Update dependency io.coil-kt.coil3:coil-bom to v3.4.0

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-02-25 06:55:27 +00:00
renovate[bot] add2676292 Update dependency org.jetbrains.kotlinx:kotlinx-io-core to v0.9.0 (#2886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 21:48:42 +00:00
renovate[bot] 95839dbb60 Update dependency com.google.devtools.ksp to v2.3.6 (#2885)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 22:42:45 +00:00
renovate[bot]andFooIbar 6dbc2b2e67 Update dependency rust-nightly to v2026-02-15 (#2883)
* Update dependency rust-nightly to v2026-02-15

* Fix build

---------

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

* Fix incompatibility with Nettle 4.x

---------

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

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

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

* Inline progress indicator to reduce recomposition

* Fix transition between progress indicator

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

* Update to snapshot to fix haptic feedback

* Fix build

---------

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

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-01-15 13:42:42 +00:00
renovate[bot]andFooIbar 2aa2c25cdd Update compose.multiplatform to v1.10.0 (#2833)
* Update compose.multiplatform to v1.10.0

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2026-01-15 13:23:59 +00:00
renovate[bot] 123a7cd8e3 Lock file maintenance (#2836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 02:43:33 +00:00
renovate[bot] 72a46acb46 Update dependency com.materialkolor:material-kolor to v4.1.0 (#2834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 18:43:12 +00:00
renovate[bot] 50815c6e91 Update Rust crate quick-xml to 0.39 (#2831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 21:44:14 +00:00
renovate[bot] 2c6f98b582 Update dependency me.zhanghai.compose.preference:preference to v2.1.1 (#2829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 05:17:02 +00:00
renovate[bot] 240bc5d0ae Update dependency libarchive to v3.8.5 (#2826)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 16:14:53 +00:00
FooIbar e4306a7303 Fix delay on the first notification 2026-01-02 22:54:48 +08:00
FooIbar 15179a82e3 Fix sad panda check 2026-01-01 18:33:51 +08:00
renovate[bot] 1e3b2bebb3 Update dependency rust-nightly to v2026 (#2821)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-01 09:33:48 +00:00
renovate[bot] 2c32cafd38 Update Rust crate reqwest to 0.13 (#2819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 18:10:05 +00:00
FooIbar ddd11477f7 Lower image target size to reduce moiré patterns
This partially reverts commit 63164dc70a.
2025-12-27 10:38:04 +08:00
renovate[bot] f3b2d8495a Update dependency io.github.petertrr:kotlin-multiplatform-diff to v1.3.0 (#2816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-26 21:33:10 +00:00
renovate[bot] a56dd32425 Update plugin composeCompilerReportGenerator to v1.5.0 (#2813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 08:42:56 +00:00
FooIbar 873b5e8540 Migrate to biometric-compose 2025-12-20 13:32:06 +08:00
renovate[bot] 34719b599e Update aboutlibraries to v13.2.1 (#2810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 15:40:22 +00:00
FooIbar a3a3a7b46d Simplify release process 2025-12-18 20:10:02 +08:00
renovate[bot] 1a46f75693 Update dependency androidx.webkit:webkit to v1.15.0 (#2805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 03:19:28 +00:00
renovate[bot] 90b4d7777e Update dependency androidx.compose:compose-bom-alpha to v2025.12.01 (#2803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 23:48:45 +00:00
renovate[bot] c363f4df51 Update dependency liblzma to v5.8.2 (#2804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 23:48:26 +00:00
renovate[bot] 4dc0981b34 Update dependency androidx.biometric:biometric to v1.4.0-alpha05 (#2802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 18:35:14 +00:00
renovate[bot] e2772bde04 Update dependency androidx.activity:activity-compose to v1.12.2 (#2801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 18:35:03 +00:00
FooIbar f1666d7a64 Prepare for next iteration 2025-12-17 22:12:34 +08:00
FooIbar 0195f1c9b8 Prepare for release 1.14.6
Releases / build (push) Has been cancelled
2025-12-17 22:12:05 +08:00
renovate[bot] 18d2d992a4 Update dependency io.arrow-kt:arrow-stack to v2.2.1.1 (#2800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 13:44:04 +00:00
FooIbar 5582353c44 Enable group key based Compose stack traces 2025-12-17 15:08:04 +08:00
renovate[bot] 464568f29f Update dependency com.google.devtools.ksp to v2.3.4 (#2798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 20:09:01 +00:00
renovate[bot]andFooIbar 213a9d56d2 Update kotlin monorepo to v2.3.0 (#2797)
* Update kotlin monorepo to v2.3.0

* Fix build

---------

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

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

* Optimize reading progress state collection and display logic

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

* Enhance download progress display and settings

* Optimize state collection for read progress, revert arrangement

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

* Fix spotless

---------

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

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

* Fix build

* Fix build

---------

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

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

* Fix build

* Fix build

---------

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

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

* Fix build

---------

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

* Keep comment

---------

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

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-09-11 05:59:34 +00:00
renovate[bot] c078166e8d Update androidx.room to v2.8.0 (#2601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 00:32:19 +00:00
renovate[bot] 092936eda5 Update dependency androidx.work:work-runtime to v2.10.4 (#2599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 21:51:50 +00:00
renovate[bot] 257a999300 Update dependency com.google.devtools.ksp to v2.2.20-2.0.2 (#2600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 21:51:08 +00:00
renovate[bot] df6a5040a5 Update dependency androidx.activity:activity-compose to v1.12.0-alpha08 (#2598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 17:48:40 +00:00
renovate[bot] 3085ca2eeb Update benchmark to v1.4.1 (#2597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 17:48:17 +00:00
renovate[bot] fa4005cf9a Update kotlin monorepo to v2.2.20 (#2596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 10:18:21 +00:00
FooIbar c4cd375c04 Allow disabling page turning with volume keys 2025-09-09 22:04:41 +08:00
FooIbar 19d9bb198f Work around ANR on DownloadManager initialization 2025-09-05 00:29:28 +08:00
LuK1337 7016190ce5 Use LocalContentColor.current for tag text
Fixes text visibility on Android 16 QPR1.
2025-09-04 22:02:21 +08:00
renovate[bot] 8020ffa666 Update dependency com.google.devtools.ksp to v2.2.20-RC2-2.0.2 (#2590)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 04:25:38 +00:00
FooIbar bc9914ffa6 Add missing changelog entry
[no ci]
2025-09-03 11:54:41 +08:00
renovate[bot] 4ded4ff470 Update dependency chrome to v140 (#2589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 21:30:42 +00:00
renovate[bot] 7742299651 Update AGP to v8.13.0 (#2587)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 17:42:45 +00:00
FooIbar 0b348a59d2 Prepare for next iteration 2025-09-01 20:42:28 +08:00
FooIbar e51d54e346 Prepare changelog for 1.14.3
Releases / build (push) Has been cancelled
2025-09-01 20:42:02 +08:00
renovate[bot] e06b1f0177 Lock file maintenance (#2586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 12:25:48 +00:00
renovate[bot] 178a870608 Update kotlin monorepo to v2.2.20-RC2 (#2585)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 12:23:11 +00:00
FooIbar a7d3d7c651 Prepare changelog for 1.14.3 2025-09-01 20:13:28 +08:00
renovate[bot] c13024f1c7 Update AGP to v8.12.2 (#2583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-30 16:14:02 +00:00
FooIbar c53c0ef727 Fix similar galleries by artist tag 2025-08-31 00:02:06 +08:00
FooIbar cd93fe2646 Revert "Work around crash when dismissing filter settings"
This reverts commit afe638fb90.
2025-08-28 11:12:21 +08:00
renovate[bot] 7b506058a4 Update dependency androidx.compose:compose-bom-alpha to v2025.08.01 (#2580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-28 00:52:26 +00:00
renovate[bot] f658f74f35 Update androidx.lifecycle to v2.10.0-alpha03 (#2578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 20:41:58 +00:00
renovate[bot] eae5efa69d Update dependency androidx.activity:activity-compose to v1.12.0-alpha07 (#2579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 20:41:22 +00:00
FooIbar 4e6051085e Simplify repository content filtering 2025-08-26 20:01:02 +08:00
FooIbar f174ccfc04 Migrate to the new context menu APIs 2025-08-26 10:02:21 +08:00
FooIbar e1252d0554 Support searching download labels with label: 2025-08-25 21:58:18 +08:00
FooIbar afe638fb90 Work around crash when dismissing filter settings 2025-08-25 18:12:39 +08:00
renovate[bot] 17699ce889 Lock file maintenance (#2572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 01:58:40 +00:00
renovate[bot] 4afa06b7b9 Update actions/setup-java action to v5 (#2568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-21 06:39:56 +00:00
FooIbar 2d02815aba Extract common UI components part 1/n 2025-08-20 14:45:18 +08:00
renovate[bot] ae1824c6a2 Update dependency com.google.devtools.ksp to v2.2.20-RC-2.0.2 (#2567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 03:40:50 +00:00
renovate[bot] d63cc39478 Update kotlin monorepo to v2.2.20-RC (#2565)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 08:38:25 +00:00
FooIbar 51d6775429 Remove unnecessary JNI lib and main thread check
The actual disk I/O will perform in the scope of DataStore.
2025-08-19 12:07:34 +08:00
renovate[bot] 35d92f1386 Update dependency sh.calvin.reorderable:reorderable to v3 (#2562)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 22:27:32 +00:00
renovate[bot] ba0ef6dccc Update AGP to v8.12.1 (#2561)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 17:34:26 +00:00
renovate[bot] 2a325e6700 Lock file maintenance (#2559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 01:05:47 +00:00
FooIbar 4e7566f06d Fix aboutlibraries.json being out-of-date 2025-08-17 17:06:35 +08:00
FooIbar d95bf4b71d Prepare for next iteration 2025-08-16 22:24:23 +08:00
FooIbar 43ddf6ccc4 Prepare for release 1.14.2
Releases / build (push) Has been cancelled
2025-08-16 22:23:45 +08:00
FooIbar b55da640e1 Prepare changelog for 1.14.2 2025-08-16 22:19:58 +08:00
FooIbar 90534c3c9f Remove confusing connect timeout configuration 2025-08-16 22:19:58 +08:00
FooIbar 88c9e5e902 Support new tag namespace: "location:"
Treat it as "other:".
2025-08-16 17:21:53 +08:00
FooIbar ac41117a78 [no ci] Tweak Rust toolchain update schedule 2025-08-15 17:51:31 +08:00
renovate[bot] 2418c5f625 Update dependency rust-nightly to v2025-08-15 (#2554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 08:33:52 +00:00
FooIbar 52156aa9ff [no ci] Let renovate update NDK 2025-08-15 16:23:17 +08:00
renovate[bot]andFooIbar 6010fb69a4 Update dependency rust-nightly to v2025-08-14 (#2553)
* Update dependency rust-nightly to v2025-08-14

* Fix build

* Fix build

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2025-08-15 04:59:20 +00:00
FooIbar d5abbb3584 Stop speed tracker immediately if download is done
Subsequent disk I/O will make the reported download speed inaccurate.
2025-08-14 19:25:03 +08:00
FooIbar 8c27504ee2 Update Rust nightly toolchain every ~6 weeks
Follow the stable release cycle.

[no ci]
2025-08-14 15:48:47 +08:00
FooIbar 8a20525da3 Remove obsolete workaround 2025-08-14 11:14:33 +08:00
renovate[bot] c7b8326b52 Update dependency androidx.core:core-ktx to v1.17.0 (#2550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-14 00:53:52 +00:00
renovate[bot] ee8b80651e Update dependency androidx.compose:compose-bom-alpha to v2025.08.00 (#2549)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-14 00:53:37 +00:00
renovate[bot] 589676c52a Update dependency androidx.test.uiautomator:uiautomator to v2.4.0-alpha06 (#2548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 22:54:04 +00:00
FooIbar 83085d5f9a Allow disabling download speed check 2025-08-13 20:37:43 +08:00
FooIbar a1a204d0d5 Refactor settings observation 2025-08-13 00:42:20 +08:00
FooIbar 9a43c36758 KMP DataStorePreferences 2025-08-12 10:40:45 +08:00
renovate[bot] e017efe35f Update actions/checkout action to v5 (#2542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 13:57:50 +00:00
renovate[bot] 1645c17ab7 Lock file maintenance (#2541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 02:33:12 +00:00
FooIbar 075b800932 Exclude CMP resources dependency 2025-08-08 17:19:50 +08:00
FooIbar 56f9138db4 Misc updates 2025-08-08 17:19:50 +08:00
FooIbar 20c1b7acdb Migrate to moko-resources 2025-08-07 10:40:21 +08:00
FooIbar a10022d797 Move plural string resources to their own files 2025-08-07 10:40:21 +08:00
renovate[bot] aafbbbec38 Update dependency chrome to v139 (#2538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 21:43:41 +00:00
FooIbar 028a994070 Hack for type-safe accessors in convention plugins 2025-08-05 22:58:52 +08:00
renovate[bot] 581125db62 Lock file maintenance (#2535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 03:00:33 +00:00
FooIbar 295770002a Extract common build logic to convention plugins 2025-08-03 10:39:03 +08:00
renovate[bot] 1a66d84bd0 Update dependency gradle to v9 (#2532)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-01 01:45:06 +00:00
renovate[bot] cd3f51b66d Update AGP to v8.12.0 (#2531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 18:04:05 +00:00
renovate[bot] 6d8ac4c3da Update dependency com.google.devtools.ksp to v2.2.20-Beta2-2.0.2 (#2530)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 18:01:43 +00:00
FooIbar 156382eaad Prepare for next iteration 2025-07-31 18:35:19 +08:00
FooIbar 2c155fe599 Prepare for release 1.14.1
Releases / build (push) Has been cancelled
2025-07-31 18:34:50 +08:00
renovate[bot] 323de69ba0 Update dependency androidx.test.ext:junit to v1.3.0 (#2527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 01:52:48 +00:00
renovate[bot] 0cdc6be34f Update dependency androidx.browser:browser to v1.9.0 (#2526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 01:52:18 +00:00
FooIbar 36c1703763 Renew GitHub token and prepare for a point release 2025-07-31 09:43:10 +08:00
renovate[bot] ee3d2f681c Update dependency androidx.work:work-runtime to v2.10.3 (#2525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-30 23:05:17 +00:00
renovate[bot] 11ce900868 Update dependency androidx.compose:compose-bom-alpha to v2025.07.01 (#2523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-30 19:59:25 +00:00
renovate[bot] b936dd2533 Update benchmark to v1.4.0 (#2522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-30 19:58:11 +00:00
renovate[bot] fab8cd0c1f Update dependency com.squareup.okio:okio-bom to v3.16.0 (#2520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-30 00:35:57 +00:00
renovate[bot] ec3a962ec8 Update dependency com.squareup.logcat:logcat to v0.4 (#2519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-29 13:56:37 +00:00
FooIbar 348813109b Clean up context parameters 2025-07-29 21:46:57 +08:00
renovate[bot] 59598094df Update kotlin monorepo to v2.2.20-Beta2 (#2517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-29 10:18:22 +00:00
renovate[bot] 8b6ae59eda Update dependency io.ktor:ktor-client-okhttp to v3.2.3 (#2516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-29 10:17:55 +00:00
FooIbar b3d8653882 Speed up dependency downloading 2025-07-29 18:17:42 +08:00
renovate[bot] 719d1ebbb8 Update xmlutil to v0.91.2 (#2514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-29 00:21:18 +00:00
FooIbar a23f902e9a Revert "[no ci] Allow blank issue"
This reverts commit 24e36b6283.
2025-07-28 20:25:31 +08:00
renovate[bot] 79559635e1 Lock file maintenance (#2512)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 03:06:58 +00:00
FooIbar c57a4e4132 Kotlin 2.2.20-Beta1 and remove custom impl 2025-07-26 23:55:25 +08:00
renovate[bot] c6f7d9d869 Update dependency com.squareup.logcat:logcat to v0.3 (#2509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 08:47:54 +00:00
renovate[bot] 9fcddd06e4 Update dependency com.materialkolor:material-kolor to v3.0.1 (#2508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-24 20:54:47 +00:00
FooIbar 3526a2b0e9 [no ci] Add comment about KSP version check 2025-07-24 22:58:06 +08:00
FooIbar 56187aa4dc Prepare for next iteration
(cherry picked from commit 2d02eda9e3)
2025-07-23 23:25:15 +08:00
409 changed files with 5613 additions and 4859 deletions

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
[*.{kt,kts}]
ij_kotlin_imports_layout = *
ij_kotlin_line_break_after_multiline_when_entry = false
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled
ktlint_standard_mixed-condition-operators = disabled
File renamed without changes.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
@@ -13,42 +13,39 @@ on:
jobs:
baseline-profile:
runs-on: ubuntu-latest
runs-on: nekomoee
steps:
- name: Free disk space
run: rm -rf $AGENT_TOOLSDIRECTORY
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v4
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 aarch64-linux-android x86_64-linux-android
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: app/.cxx
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
@@ -59,7 +56,7 @@ jobs:
- name: Upload reports
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: reports
path: benchmark/build/reports
+128
View File
@@ -0,0 +1,128 @@
name: CI
on:
push:
branches:
- '*'
pull_request:
workflow_dispatch:
jobs:
check:
runs-on: nekomoee
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
- name: Clippy Check
working-directory: ./app/src/main/rust
run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: true
- name: Check build-logic
run: ./gradlew :build-logic:convention:check
- name: Spotless Check
run: ./gradlew spotlessCheck
default:
runs-on: nekomoee
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v5
with:
path: app/.cxx
key: cmake-default-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
restore-keys: cmake-default-
- name: Build
run: ./gradlew assembleDefaultRelease
- name: Upload ARM64
uses: actions/upload-artifact@v3
with:
name: default-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v3
with:
name: default-x86_64-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v3
with:
name: default-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
- name: Upload mapping
uses: actions/upload-artifact@v3
with:
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease/*.txt
- name: Upload native debug symbols
uses: actions/upload-artifact@v3
with:
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
+79
View File
@@ -0,0 +1,79 @@
name: Releases
on:
push:
tags:
- "*"
jobs:
build:
runs-on: nekomoee
steps:
- name: Free disk space
run: rm -rf $AGENT_TOOLSDIRECTORY
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache/restore@v5
with:
path: app/.cxx
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
- name: Generate Baseline Profile
run: ./gradlew generateBaselineProfile
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
-Prelease
- name: Build
run: ./gradlew assembleRelease
-Prelease
- name: Rename Apks
run: |
mkdir release
mv app/build/outputs/apk/default/release/app-default-universal-release.apk release/EhViewer-${{ github.ref_name }}-default-universal.apk
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk release/EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk release/EhViewer-${{ github.ref_name }}-default-x86_64.apk
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk release/EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultRelease/mapping.txt release/EhViewer-${{ github.ref_name }}-default-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip release/EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
- name: Releases
uses: actions/forgejo-release@v2
with:
direction: upload
url: https://codeberg.org
repo: EhViewer/EhViewer
release-dir: release
body: Bump Version
-1
View File
@@ -1 +0,0 @@
*.sh eol=lf
-185
View File
@@ -1,185 +0,0 @@
name: CI
on:
push:
branches:
- '*'
pull_request:
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android
- 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@v4
with:
cache-read-only: true
- name: Spotless Check
run: ./gradlew spotlessCheck
- name: Lint Check
run: ./gradlew lintMarshmallowRelease
default:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android
- 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@v4
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v4
with:
path: app/.cxx
key: cmake-default-${{ hashFiles('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@v4
with:
name: default-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v4
with:
name: default-x86_64-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v4
with:
name: default-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
- name: Upload mapping
if: always()
uses: actions/upload-artifact@v4
with:
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
with:
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
marshmallow:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android
- 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@v4
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v4
with:
path: app/.cxx
key: cmake-marshmallow-${{ hashFiles('settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
restore-keys: cmake-marshmallow-
- name: Build
run: ./gradlew assembleMarshmallowRelease
- name: Upload ARM64
uses: actions/upload-artifact@v4
with:
name: marshmallow-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v4
with:
name: marshmallow-x86_64-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v4
with:
name: marshmallow-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
- name: Upload mapping
if: always()
uses: actions/upload-artifact@v4
with:
name: marshmallow-mapping-${{ github.sha }}
path: app/build/outputs/mapping/marshmallowRelease
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
with:
name: marshmallow-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
-21
View File
@@ -1,21 +0,0 @@
name: 'Lock Threads'
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
permissions:
issues: write
concurrency:
group: lock-threads
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: '1'
process-only: 'issues'
-90
View File
@@ -1,90 +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@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache/restore@v4
with:
path: app/.cxx
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
- name: Generate Baseline Profile
run: ./gradlew generateBaselineProfile
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
- 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
+3 -2
View File
@@ -1,8 +1,9 @@
.gradle
/local.properties
.DS_Store
/build
/.kotlin
build
.cxx
.kotlin
*.iml
.idea
.vscode
+1
View File
@@ -0,0 +1 @@
25
+91
View File
@@ -1,5 +1,96 @@
# 变更日志
## [1.14.6] - 2025-12-17
### 新功能
* 支持显示画廊阅读进度(于 EH 设置中开启) [#2277](https://github.com/FooIbar/EhViewer/issues/2277)
### 改进
* 画廊列表变化时重新显示悬浮操作按钮 [#2681](https://github.com/FooIbar/EhViewer/issues/2681)
* 移除文件名中的尾随空格和点以兼容 Windows [#2727](https://github.com/FooIbar/EhViewer/issues/2727)
* 长按画廊详情中的标签时显示标签名称 [#2769](https://github.com/FooIbar/EhViewer/issues/2769)
* 杂项优化
### Bug 修复
* 对话框中的下拉菜单显示错位
* HTTP 状态错误显示为“什么都没有找到”
* 某些老设备无法通过 Cloudflare 验证 [#2794](https://github.com/FooIbar/EhViewer/issues/2794)
* 杂项修复
## [1.14.5] - 2025-10-19
### 改进
* [阅读器] 显示画廊标题
* [阅读器] 显示菜单时改变标题栏颜色
### Bug 修复
* 横屏模式下打开阅读器设置会闪退 [#2673](https://github.com/FooIbar/EhViewer/issues/2673)
* 强制横屏时竖直拿取设备加载图片会横竖屏乱跳 [#2674](https://github.com/FooIbar/EhViewer/issues/2674)
## [1.14.4] - 2025-10-12
### 改进
* [阅读器] 支持禁用使用音量键翻页 [#2593](https://github.com/FooIbar/EhViewer/issues/2593)
* 视搜索警告为错误 [#2604](https://github.com/FooIbar/EhViewer/issues/2604)
* 余额不足时停止下载并显示通知 [#2479](https://github.com/FooIbar/EhViewer/issues/2479)
* [阅读器] 使用默认阅读模式时对带有 webtoon 标签的画廊自动使用条漫模式 [#2632](https://github.com/FooIbar/EhViewer/issues/2632)
* 优化按页数排序下载项目的速度
* 开启显示标签投票状态时为 power >= 100 的标签添加下划线
* 杂项优化
### Bug 修复
* Android 16 QPR1 上画廊详情中的标签文本对比度不足
* 初始化 DownloadManager 用时过长导致 ANR [#2588](https://github.com/FooIbar/EhViewer/issues/2588)
* 某些设备上无法下载名称较长的画廊 [#2607](https://github.com/FooIbar/EhViewer/issues/2607)
* 评论区链接实际可点击区域比画面显示上的略长 [#2615](https://github.com/FooIbar/EhViewer/issues/2615)
* 主页快速搜索再次打开出现以前的画廊重复出现在最上层 [#2628](https://github.com/FooIbar/EhViewer/issues/2628)
* 非条漫模式下原始大小较小的动图不播放 [#2652](https://github.com/FooIbar/EhViewer/issues/2652)
* Android 9 以下打开评论区切屏后回来会闪退 [#2655](https://github.com/FooIbar/EhViewer/issues/2655)
* 杂项修复
## [1.14.3] - 2025-09-01
### 新功能
* 支持以 label: 语法搜索下载标签
### Bug 修复
* [阅读器] 关闭滤镜设置时崩溃 [#2573](https://github.com/FooIbar/EhViewer/issues/2573)
* 编辑评论时上下文菜单缺少格式化选项
* 某些情况下搜索相似画廊时出错
## [1.14.2] - 2025-08-16
### 改进
* [下载] 支持将最低下载速度设为 0 [#2545](https://github.com/FooIbar/EhViewer/issues/2545)
* [下载] 移除令人困惑的连接超时设置 [#2556](https://github.com/FooIbar/EhViewer/issues/2556)
### Bug 修复
* 下载速度计算不准确导致下载失败误报 [#2545](https://github.com/FooIbar/EhViewer/issues/2545)
* 无法解析带有 location: 标签的画廊
## [1.14.1] - 2025-07-31
### 改进
* 更新 GitHub 访问令牌
## [1.14.0] - 2025-07-23
### 重大变更
+12 -90
View File
@@ -1,103 +1,25 @@
<p align="right">
<strong>English</strong>
<span> | </span>
<a href="/docs/README/zh-cn.md">
简体中文
</a>
<span> | </span>
<a href="/docs/README/zh-tw.md">
正體中文
</a>
<span> | </span>
<a href="/docs/README/ja.md">
日本語
</a>
</p>
<h1 align="center">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<img src="https://github.com/FooIbar/EhViewer-art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<br>EhViewer<br>
</h1>
<p align="center">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="/LICENSE">
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/FooIbar/EhViewer/releases">
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
</a>
<a href="https://github.com/FooIbar/EhViewer/issues">
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
</a>
Standalone fork of
<a href="https://codeberg.org/EhViewer/EhViewer">EhViewer (codeberg)</a>
maintained at <a href="https://gitea.yrkaierx.com/yenru0/EhViewer">gitea.yrkaierx.com/yenru0/EhViewer</a>
</p>
<div align="center">
<h3>
<a href="#description">
Description
</a>
<span> | </span>
<a href="#download">
Download
</a>
<span> | </span>
<a href="#screenshot">
Screenshot
</a>
<span> | </span>
<a href="#thanks">
Thanks
</a>
<span> | </span>
<a href="#license">
License
</a>
</h3>
</div>
# Changes over upstream
# Description
- Built-in hosts, Cloudflare CDN fallback and domain fronting with SNI spoofing for censored networks (`EhHostsConfig.kt`, `EhDns.kt`, `EhSSLSocketFactory.kt`)
- Cronet engine disabled by default (bypasses the anti-DPI OkHttp stack; `enable_quic` re-enables it)
- Update check disabled by default (manual check still available)
- API request timeout raised to 30 s
- Own application id (`yenru0.yrkaierx.ehviewer`), installs side by side with upstream builds
A modern EhViewer fork dedicated to high-performance
# Upstream
with [Material Design 3](https://m3.material.io/)
and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview) Support
# Download
| Flavor | Minimum Android Version | Notes |
|-------------|-------------------------|--------------------------------|
| Default | 8.0 | Full support |
| Marshmallow | 6.0* | Limited support, no guarantees |
*Devices running Android 6 need to have the [ISRG Root X1](https://letsencrypt.org/certs/isrgrootx1.pem) certificate installed
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
</a>
# Screenshot
![screenshot-01](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-01.png)
![screenshot-02](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-02.png)
# Thanks
Here is the libraries
- [Arrow](https://arrow-kt.io/)
- [AOSP & AndroidX](https://source.android.com/)
- [Kotlin & KotlinX](https://kotlinlang.org/)
- [Material Icons](https://github.com/google/material-design-icons)
- [Ktor](https://ktor.io/)
- [Coil](https://coil-kt.github.io/coil/)
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
- [libarchive](https://www.libarchive.org/)
Synced from [EhViewer/EhViewer on codeberg](https://codeberg.org/EhViewer/EhViewer), the continuation of FooIbar's EhViewer. Feature work happens there; this fork tracks it and carries the network/resilience patches above.
# License
-2
View File
@@ -1,2 +0,0 @@
/build
/.cxx
+30 -73
View File
@@ -1,20 +1,16 @@
import com.mikepenz.aboutlibraries.plugin.DuplicateMode
import com.mikepenz.aboutlibraries.plugin.DuplicateRule
import java.util.regex.Pattern
import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode
val isRelease: Boolean
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ehviewer.android.application)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.spotless)
alias(libs.plugins.aboutlibrariesPlugin)
alias(libs.plugins.composeCompilerReportGenerator)
alias(libs.plugins.aboutlibraries)
alias(libs.plugins.aboutlibrariesAndroid)
alias(libs.plugins.baselineprofile)
}
@@ -53,21 +49,23 @@ android {
val repoName = providers.exec {
commandLine = "git remote get-url origin".split(' ')
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
.removeSuffix(".git")
}.standardOutput.asText.get().trim().removeSuffix(".git").removePrefix("https://codeberg.org/")
val chromeVersion = rootProject.layout.projectDirectory.file("chrome.version").asFile.readText().trim()
val snapshot = !hasProperty("release")
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
versionCode = 180063
versionName = "1.14.0"
// versionNameSuffix = "-SNAPSHOT"
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
applicationId = "yenru0.yrkaierx.ehviewer"
versionCode = 180064
versionName = if (snapshot) {
"1.15.0-SNAPSHOT"
} else {
"1.15.0"
}
buildConfigField("boolean", "SNAPSHOT", "$snapshot")
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName\"")
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("long", "COMMIT_TIME", commitTime)
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
buildConfigField("String", "CHROME_VERSION", "\"$chromeVersion\"")
ndk {
if (isRelease) {
abiFilters.addAll(supportedAbis)
@@ -79,9 +77,10 @@ android {
flavorDimensions += "api"
productFlavors {
create("default")
create("default") {
minSdk = 26
}
create("marshmallow") {
minSdk = 23
applicationIdSuffix = ".m"
versionNameSuffix = "-M"
}
@@ -97,16 +96,13 @@ android {
isCoreLibraryDesugaringEnabled = true
}
lint {
checkReleaseBuilds = false
disable += setOf("MissingTranslation", "MissingQuantity")
error += setOf("InlinedApi")
}
packaging {
dex {
useLegacyPackaging = false
}
jniLibs {
excludes += "**/libdatastore_shared_counter.so" // DataStore multi-process
}
resources {
// Required by Layout Inspector
pickFirsts += "/META-INF/androidx.compose.ui_ui.version"
@@ -176,20 +172,24 @@ baselineProfile {
}
dependencies {
implementation(projects.core.data)
implementation(projects.core.i18n)
implementation(projects.core.ui)
// https://developer.android.com/jetpack/androidx/releases/activity
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.biometric)
implementation(libs.androidx.browser)
// https://developer.android.com/jetpack/androidx/releases/compose-material3
api(platform(libs.compose.bom))
implementation(libs.bundles.compose)
implementation(libs.androidx.webkit)
implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.compiler)
implementation(libs.compose.preference)
implementation(libs.compose.preference) {
// R8 won't remove it because it adds a content provider
exclude(group = "org.jetbrains.compose.components", module = "components-resources")
}
implementation(libs.androidx.core)
implementation(libs.androidx.core.splashscreen)
@@ -204,7 +204,6 @@ dependencies {
implementation(libs.androidx.paging.compose)
// https://developer.android.com/jetpack/androidx/releases/room
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.paging)
implementation(libs.androidx.work.runtime)
@@ -216,8 +215,6 @@ dependencies {
// https://square.github.io/okhttp/changelogs/changelog/
implementation(platform(libs.okhttp.bom))
implementation(platform(libs.okio.bom))
implementation(libs.logcat)
implementation(libs.diff)
@@ -254,43 +251,16 @@ dependencies {
}
kotlin {
jvmToolchain(21)
// https://kotlinlang.org/docs/gradle-compiler-options.html#all-compiler-options
compilerOptions {
jvmDefault = JvmDefaultMode.NO_COMPATIBILITY
progressiveMode = true
optIn.addAll(
"coil3.annotation.ExperimentalCoilApi",
"androidx.compose.foundation.layout.ExperimentalLayoutApi",
"androidx.compose.material3.ExperimentalMaterial3Api",
"androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
"androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi",
"androidx.compose.ui.ExperimentalComposeUiApi",
"androidx.compose.foundation.ExperimentalFoundationApi",
"androidx.compose.animation.ExperimentalAnimationApi",
"androidx.compose.animation.ExperimentalSharedTransitionApi",
"androidx.compose.runtime.ExperimentalComposeRuntimeApi",
"androidx.paging.ExperimentalPagingApi",
"kotlin.ExperimentalStdlibApi",
"kotlin.concurrent.atomics.ExperimentalAtomicApi",
"kotlin.contracts.ExperimentalContracts",
"kotlin.time.ExperimentalTime",
"kotlinx.coroutines.ExperimentalCoroutinesApi",
"kotlinx.coroutines.FlowPreview",
"kotlinx.serialization.ExperimentalSerializationApi",
"splitties.experimental.ExperimentalSplittiesApi",
"splitties.preferences.DataStorePreferencesPreview",
)
freeCompilerArgs.addAll(
"-Xcontext-parameters",
"-Xannotation-default-target=param-property",
"me.saket.telephoto.ExperimentalTelephotoApi",
)
}
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
}
@@ -304,16 +274,3 @@ aboutLibraries {
duplicationRule = DuplicateRule.GROUP
}
}
val ktlintVersion = libs.ktlint.get().version
spotless {
kotlin {
// https://github.com/diffplug/spotless/issues/111
target("src/**/*.kt")
ktlint(ktlintVersion)
}
kotlinGradle {
ktlint(ktlintVersion)
}
}
+4
View File
@@ -15,6 +15,10 @@
java.nio.ByteBuffer source;
}
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
static boolean isLinkBufferComposerEnabled() return true;
}
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
+1 -5
View File
@@ -39,7 +39,7 @@
<activity
android:name=".ui.MainActivity"
android:banner="@mipmap/ic_leanback_banner"
android:configChanges="screenSize"
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/AppTheme.Splash"
@@ -118,10 +118,6 @@
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name=".ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings" />
<activity
android:name=".shortcuts.ShortcutsActivity"
android:excludeFromRecents="true"
+37 -27
View File
@@ -4,17 +4,20 @@ include(ExternalProject)
include(FetchContent)
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
-mllvm -polly-isl-arg=--no-schedule-serialize-sccs \
-mllvm -polly-ast-use-context \
-mllvm -polly-detect-keep-going \
-mllvm -polly-position=before-vectorizer \
-mllvm -polly-vectorizer=stripmine \
-mllvm -polly-detect-profitability-min-per-loop-insts=40 \
-mllvm -polly-invariant-load-hoisting")
string(CONCAT CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -ffast-math -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto "
"-ffile-prefix-map=${ANDROID_TOOLCHAIN_ROOT}= "
"-ffile-prefix-map=${CMAKE_SOURCE_DIR}= "
"-ffile-prefix-map=${CMAKE_BINARY_DIR}= "
"-mllvm -polly "
"-mllvm -polly-run-dce "
"-mllvm -polly-run-inliner "
"-mllvm -polly-isl-arg=--no-schedule-serialize-sccs "
"-mllvm -polly-ast-use-context "
"-mllvm -polly-detect-keep-going "
"-mllvm -polly-position=before-vectorizer "
"-mllvm -polly-vectorizer=stripmine "
"-mllvm -polly-detect-profitability-min-per-loop-insts=40 "
"-mllvm -polly-invariant-load-hoisting")
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
option(BUILD_TESTING OFF)
@@ -29,7 +32,7 @@ option(XZ_TOOL_XZDEC OFF)
FetchContent_Declare(
liblzma
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
GIT_TAG v5.8.1
GIT_TAG v5.8.3
GIT_SHALLOW 1
)
@@ -38,23 +41,27 @@ include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
# Build GNUTLS libnettle
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
execute_process(
COMMAND wsl wslpath ${CMAKE_CURRENT_LIST_DIR}
OUTPUT_VARIABLE CURRENT_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
else ()
set(CURRENT_DIR ${CMAKE_CURRENT_LIST_DIR})
endif ()
set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}")
set(TOOLCHAIN "$ANDROID_HOME/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin")
set(WSL wsl)
else ()
set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin")
endif ()
set(CONFIGURE_COMMAND ${WSL} ./.bootstrap && ${WSL} ./configure --disable-public-key --disable-dependency-tracking
"--host=${CMAKE_C_COMPILER_TARGET}"
"CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments"
"AR=${TOOLCHAIN}/llvm-ar"
"CC=${TOOLCHAIN}/clang --target=${CMAKE_C_COMPILER_TARGET}"
"RANLIB=${TOOLCHAIN}/llvm-ranlib"
)
ExternalProject_Add(
nettle
GIT_REPOSITORY https://gitlab.com/gnutls/nettle.git
GIT_TAG nettle_3.10.2_release_20250626
GIT_TAG nettle_4.0_release_20260205
GIT_SHALLOW 1
GIT_CONFIG core.autocrlf=input
CONFIGURE_COMMAND bash ${CURRENT_DIR}/configure_nettle.sh ${NDK_VERSION} ${CMAKE_C_COMPILER_TARGET}
BUILD_COMMAND bash -c "make -j4 libnettle.a CFLAGS='${CMAKE_C_FLAGS} -Qunused-arguments'"
CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
BUILD_COMMAND ${WSL} make -j4 libnettle.a
INSTALL_COMMAND ""
BUILD_IN_SOURCE 1
BUILD_BYPRODUCTS <SOURCE_DIR>/libnettle.a
@@ -96,7 +103,7 @@ set(LIBARCHIVE_PATCH
FetchContent_Declare(
libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
GIT_TAG v3.8.1
GIT_TAG v3.8.7
GIT_SHALLOW 1
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
)
@@ -129,10 +136,12 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Android")
set(ANDROID 1)
endif ()
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ../../../../rust-toolchain.toml)
unset(Rust_TOOLCHAIN CACHE)
FetchContent_Declare(
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.5.2
GIT_REPOSITORY https://github.com/FooIbar/corrosion.git
GIT_TAG v0.6.1
GIT_SHALLOW 1
)
@@ -141,8 +150,9 @@ corrosion_import_crate(
MANIFEST_PATH ../rust/Cargo.toml
LOCKED
FEATURES $<IF:$<VERSION_GREATER_EQUAL:${ANDROID_NATIVE_API_LEVEL},26>,android-26,android>
FLAGS $<$<NOT:$<CONFIG:Debug>>:-Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort>
FLAGS $<$<NOT:$<CONFIG:Debug>>:-Z build-std -Z build-std-features=>
)
corrosion_add_target_rustflags(ehviewer_rust $<$<NOT:$<CONFIG:Debug>>:-Z unstable-options -C panic=immediate-abort -Z location-detail=none -Z fmt-debug=none>)
corrosion_link_libraries(ehviewer_rust android jnigraphics log webp webpdemux)
# Build and link our app's native lib
-13
View File
@@ -1,13 +0,0 @@
SUFFIX=toolchains/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64/bin
CI_NDK_LOCATION=$ANDROID_HOME/ndk/$1/$SUFFIX
ARCH_NDK_LOCATION=$ANDROID_HOME/ndk-bundle/$SUFFIX
if test -e $CI_NDK_LOCATION ;then
TOOLCHAIN=$CI_NDK_LOCATION
else
TOOLCHAIN=$ARCH_NDK_LOCATION
fi
TARGET=$2
./.bootstrap && ./configure --host=$TARGET --disable-public-key --disable-dependency-tracking \
AR=$TOOLCHAIN/llvm-ar \
CC="$TOOLCHAIN/clang -target $TARGET" \
RANLIB=$TOOLCHAIN/llvm-ranlib
+1 -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];
@@ -1,161 +0,0 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.systemuicontroller
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import android.view.Window
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
/**
* A class which provides easy-to-use utilities for updating the System UI bar
* colors within Jetpack Compose.
*/
@Stable
interface SystemUiController {
/**
* Control for the behavior of the system bars. This value should be one of the
* [WindowInsetsControllerCompat] behavior constants:
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH] (Deprecated),
* [WindowInsetsControllerCompat.BEHAVIOR_DEFAULT] and
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE].
*/
var systemBarsBehavior: Int
/**
* Property which holds the status bar visibility. If set to true, show the status bar,
* otherwise hide the status bar.
*/
var isStatusBarVisible: Boolean
/**
* Property which holds the navigation bar visibility. If set to true, show the navigation bar,
* otherwise hide the navigation bar.
*/
var isNavigationBarVisible: Boolean
/**
* Property which holds the status & navigation bar visibility. If set to true, show both bars,
* otherwise hide both bars.
*/
var isSystemBarsVisible: Boolean
get() = isNavigationBarVisible && isStatusBarVisible
set(value) {
isStatusBarVisible = value
isNavigationBarVisible = value
}
/**
* Property which holds whether the status bar icons + content are 'dark' or not.
*/
var statusBarDarkContentEnabled: Boolean
}
/**
* Remembers a [SystemUiController] for the given [window].
*
* If no [window] is provided, an attempt to find the correct [Window] is made.
*
* First, if the [LocalView]'s parent is a [DialogWindowProvider], then that dialog's [Window] will
* be used.
*
* Second, we attempt to find [Window] for the [Activity] containing the [LocalView].
*
* If none of these are found (such as may happen in a preview), then the functionality of the
* returned [SystemUiController] will be degraded, but won't throw an exception.
*/
@Composable
fun rememberSystemUiController(
window: Window? = findWindow(),
): SystemUiController {
val view = LocalView.current
return remember(view, window) { AndroidSystemUiController(view, window) }
}
@Composable
private fun findWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window
?: LocalView.current.context.findWindow()
private tailrec fun Context.findWindow(): Window? = when (this) {
is Activity -> window
is ContextWrapper -> baseContext.findWindow()
else -> null
}
/**
* A helper class for setting the navigation and status bar colors for a [View], gracefully
* degrading behavior based upon API level.
*
* Typically you would use [rememberSystemUiController] to remember an instance of this.
*/
internal class AndroidSystemUiController(
private val view: View,
window: Window?,
) : SystemUiController {
private val windowInsetsController = window?.let {
WindowCompat.getInsetsController(it, view)
}
override var systemBarsBehavior: Int
get() = windowInsetsController?.systemBarsBehavior ?: 0
set(value) {
windowInsetsController?.systemBarsBehavior = value
}
override var isStatusBarVisible: Boolean
get() {
return ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.statusBars()) == true
}
set(value) {
if (value) {
windowInsetsController?.show(WindowInsetsCompat.Type.statusBars())
} else {
windowInsetsController?.hide(WindowInsetsCompat.Type.statusBars())
}
}
override var isNavigationBarVisible: Boolean
get() {
return ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true
}
set(value) {
if (value) {
windowInsetsController?.show(WindowInsetsCompat.Type.navigationBars())
} else {
windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars())
}
}
override var statusBarDarkContentEnabled: Boolean
get() = windowInsetsController?.isAppearanceLightStatusBars == true
set(value) {
windowInsetsController?.isAppearanceLightStatusBars = value
}
}
@@ -180,7 +180,6 @@ fun WebView(
is WebContent.Url -> {
wv.loadUrl(content.url, content.additionalHttpHeaders)
}
is WebContent.Data -> {
wv.loadDataWithBaseURL(
content.baseUrl,
@@ -190,14 +189,12 @@ fun WebView(
content.historyUrl,
)
}
is WebContent.Post -> {
wv.postUrl(
content.url,
content.postData,
)
}
is WebContent.NavigatorOnly -> {
// NO-OP
}
@@ -502,11 +499,9 @@ class WebViewNavigator(private val coroutineScope: CoroutineScope) {
event.encoding,
event.historyUrl,
)
is NavigationEvent.LoadUrl -> {
loadUrl(event.url, event.additionalHttpHeaders)
}
is NavigationEvent.PostUrl -> {
postUrl(event.url, event.postData)
}
@@ -22,6 +22,7 @@ import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composer
import androidx.compose.runtime.tooling.ComposeStackTraceMode
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.coroutineScope
import coil3.EventListener
@@ -37,6 +38,16 @@ import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.serviceLoaderEnabled
import coil3.util.DebugLogger
import com.ehviewer.core.database.SearchDatabase
import com.ehviewer.core.database.roomDb
import com.ehviewer.core.files.deleteContent
import com.ehviewer.core.ui.util.initSETConnection
import com.ehviewer.core.util.isAtLeastO
import com.ehviewer.core.util.isAtLeastP
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withUIContext
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.coil.AnimatedWebPDecoder
import com.hippo.ehviewer.coil.CropBorderInterceptor
@@ -47,7 +58,6 @@ import com.hippo.ehviewer.coil.MapExtraInfoInterceptor
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.coil.QrCodeInterceptor
import com.hippo.ehviewer.dailycheck.checkDawn
import com.hippo.ehviewer.dao.SearchDatabase
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.ktbuilder.diskCache
@@ -55,25 +65,15 @@ import com.hippo.ehviewer.ktbuilder.imageLoader
import com.hippo.ehviewer.ktor.Cronet
import com.hippo.ehviewer.ktor.configureClient
import com.hippo.ehviewer.ktor.configureCommon
import com.hippo.ehviewer.ktor.isCronetAvailable
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.lockObserver
import com.hippo.ehviewer.ui.screen.detailCache
import com.hippo.ehviewer.ui.tools.dataStateFlow
import com.hippo.ehviewer.ui.tools.initSETConnection
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.CrashHandler
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.OSUtils
import com.hippo.ehviewer.util.isAtLeastO
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastS
import com.hippo.ehviewer.util.isAtLeastSExtension7
import com.hippo.files.deleteContent
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import kotlinx.coroutines.Dispatchers
@@ -83,19 +83,16 @@ import logcat.LogPriority
import logcat.LogcatLogger
import logcat.asLog
import okio.Path.Companion.toOkioPath
import splitties.arch.room.roomDb
import splitties.init.appCtx
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
private val lifecycleScope = lifecycle.coroutineScope
class EhApplication :
Application(),
SingletonImageLoader.Factory {
override fun onCreate() {
class EhApplication : Application(), SingletonImageLoader.Factory {
override fun onCreate() = with(lifecycleScope) {
initSETConnection()
// Initialize Settings on first access
lifecycleScope.launchIO {
launchIO {
val mode = Settings.theme.value
if (!isAtLeastS) {
withUIContext {
@@ -111,12 +108,10 @@ class EhApplication :
}
}
}
lifecycle.addObserver(lockObserver)
CrashHandler.install()
super.onCreate()
System.loadLibrary("ehviewer")
lifecycleScope.launchIO {
launchUI {
launch {
FavouriteStatusRouter.collect { info ->
detailCache[info.gid]?.apply {
favoriteSlot = info.favoriteSlot
@@ -125,14 +120,17 @@ class EhApplication :
}
}
}
launchIO {
EhTagDatabase.launchUpdate()
@Suppress("UNUSED_EXPRESSION")
launch { EhDB }
launchIO { dataStateFlow.value }
launchIO { OSUtils.totalMemory }
launch { dataStateFlow.value }
launch { OSUtils.totalMemory }
launch {
if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode.key !in Settings.prefs) {
if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode !in Settings.snapshot()) {
Settings.downloadFilterMode.value = DownloadsFilterMode.CUSTOM.flag
}
initialized = true
DownloadManager.readMetadataFromLocal()
}
launch {
@@ -146,7 +144,9 @@ class EhApplication :
}
if (BuildConfig.DEBUG) {
StrictMode.enableDefaults()
Composer.setDiagnosticStackTraceEnabled(true)
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.SourceInformation)
} else {
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.Auto)
}
}
@@ -196,7 +196,7 @@ class EhApplication :
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))
@@ -214,8 +214,12 @@ class EhApplication :
}
companion object {
@Volatile
var initialized = false
private set
val ktorClient by lazy {
if (isAtLeastSExtension7 && Settings.enableCronet.value) {
if (isCronetAvailable && Settings.enableQuic.value) {
HttpClient(Cronet) {
engine { configureClient(Settings.enableQuic.value) }
configureCommon()
@@ -234,10 +238,10 @@ class EhApplication :
}
}
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)
}
}
+45 -37
View File
@@ -15,28 +15,31 @@
*/
package com.hippo.ehviewer
import android.content.Context
import android.net.Uri
import androidx.room.execSQL
import androidx.room.useWriterConnection
import arrow.fx.coroutines.resource
import arrow.fx.coroutines.resourceScope
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.dao.DownloadDirname
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.dao.EhDatabase
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.HistoryInfo
import com.hippo.ehviewer.dao.LocalFavoriteInfo
import com.hippo.ehviewer.dao.ProgressInfo
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.dao.Schema17to18
import com.ehviewer.core.data.model.asEntity
import com.ehviewer.core.database.EhDatabase
import com.ehviewer.core.database.Schema17to18
import com.ehviewer.core.database.getDatabasePath
import com.ehviewer.core.database.model.DownloadArtist
import com.ehviewer.core.database.model.DownloadDirname
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.database.model.DownloadLabel
import com.ehviewer.core.database.model.Filter
import com.ehviewer.core.database.model.GalleryEntity
import com.ehviewer.core.database.model.HistoryInfo
import com.ehviewer.core.database.model.LocalFavoriteInfo
import com.ehviewer.core.database.model.ProgressInfo
import com.ehviewer.core.database.model.QuickSearch
import com.ehviewer.core.database.roomDb
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.sendTo
import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.util.sendTo
import kotlinx.coroutines.flow.Flow
import okio.Path
import okio.Path.Companion.toOkioPath
import splitties.arch.room.roomDb
object EhDB {
private const val DB_NAME = "eh.db"
@@ -44,15 +47,15 @@ object EhDB {
addMigrations(Schema17to18())
}
suspend fun putGalleryInfo(galleryInfo: BaseGalleryInfo) {
suspend fun putGalleryInfo(galleryInfo: GalleryEntity) {
db.galleryDao().upsert(galleryInfo)
}
private suspend fun deleteGalleryInfo(galleryInfo: BaseGalleryInfo) {
private suspend fun deleteGalleryInfo(galleryInfo: GalleryEntity) {
runCatching { db.galleryDao().delete(galleryInfo) }
}
suspend fun updateGalleryInfo(galleryInfoList: List<BaseGalleryInfo>) {
suspend fun updateGalleryInfo(galleryInfoList: List<GalleryEntity>) {
db.galleryDao().update(galleryInfoList)
}
@@ -146,6 +149,8 @@ object EhDB {
dao.fill(raw.position)
}
suspend fun searchDownloadLabel(keyword: String, limit: Int) = db.downloadLabelDao().search("%$keyword%", limit)
suspend fun putDownloadArtist(gid: Long, artists: List<DownloadArtist>) {
if (artists.isNotEmpty()) {
val dao = db.downloadArtistDao()
@@ -154,12 +159,12 @@ object EhDB {
}
}
suspend fun removeLocalFavorites(galleryInfo: BaseGalleryInfo) {
suspend fun removeLocalFavorites(galleryInfo: GalleryInfo) {
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
deleteGalleryInfo(galleryInfo)
deleteGalleryInfo(galleryInfo.asEntity())
}
suspend fun removeLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
suspend fun removeLocalFavorites(galleryInfoList: Collection<GalleryInfo>) {
galleryInfoList.forEach {
removeLocalFavorites(it)
}
@@ -170,12 +175,12 @@ object EhDB {
return dao.contains(gid)
}
suspend fun putLocalFavorites(galleryInfo: BaseGalleryInfo) {
putGalleryInfo(galleryInfo)
suspend fun putLocalFavorites(galleryInfo: GalleryInfo) {
putGalleryInfo(galleryInfo.asEntity())
db.localFavoritesDao().upsert(LocalFavoriteInfo(galleryInfo.gid))
}
suspend fun putLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
suspend fun putLocalFavorites(galleryInfoList: Collection<GalleryInfo>) {
galleryInfoList.forEach {
putLocalFavorites(it)
}
@@ -221,8 +226,8 @@ object EhDB {
fun searchLocalFav(keyword: String) = db.localFavoritesDao().joinListLazy("*$keyword*")
suspend fun putHistoryInfo(galleryInfo: BaseGalleryInfo) {
putGalleryInfo(galleryInfo)
suspend fun putHistoryInfo(galleryInfo: GalleryInfo) {
putGalleryInfo(galleryInfo.asEntity())
db.historyDao().upsert(HistoryInfo(galleryInfo.gid))
}
@@ -239,7 +244,7 @@ object EhDB {
dao.insertOrIgnore(historyInfoList)
}
suspend fun deleteHistoryInfo(galleryInfo: BaseGalleryInfo) {
suspend fun deleteHistoryInfo(galleryInfo: GalleryEntity) {
val dao = db.historyDao()
dao.deleteByKey(galleryInfo.gid)
deleteGalleryInfo(galleryInfo)
@@ -273,23 +278,26 @@ object EhDB {
db.filterDao().update(filter)
}
fun exportDB(context: Context, file: Path) {
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
val dbFile = context.getDatabasePath(DB_NAME)
dbFile.toOkioPath() sendTo file
suspend fun exportDB(file: Path) {
db.useWriterConnection { conn ->
conn.execSQL("PRAGMA wal_checkpoint(FULL)")
conn.execSQL("VACUUM")
}
val dbFile = getDatabasePath(DB_NAME)
dbFile sendTo file
}
suspend fun importDB(context: Context, uri: Uri) = resourceScope {
suspend fun importDB(file: Path) = resourceScope {
val tempDBName = "tmp.db"
val dbFile = getDatabasePath(tempDBName)
file sendTo dbFile
val oldDB = resource {
context.deleteDatabase(tempDBName)
roomDb<EhDatabase>(tempDBName) {
createFromInputStream { context.contentResolver.openInputStream(uri) }
addMigrations(Schema17to18())
}
} release { db ->
db.close()
context.deleteDatabase(tempDBName)
dbFile.delete()
}
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
@@ -0,0 +1,56 @@
package com.hippo.ehviewer
import com.hippo.ehviewer.client.hosts
import com.hippo.ehviewer.client.hostsDsl
val builtInHosts = hostsDsl {
hosts("exhentai.org") {
"178.175.128.251" blockedInCN false
"178.175.128.252" blockedInCN false
"178.175.128.253" blockedInCN false
"178.175.128.254" blockedInCN false
"178.175.129.251" blockedInCN false
"178.175.129.252" blockedInCN false
"178.175.129.253" blockedInCN false
"178.175.129.254" blockedInCN false
"178.175.132.19" blockedInCN false
"178.175.132.20" blockedInCN false
"178.175.132.21" blockedInCN false
"178.175.132.22" blockedInCN false
}
hosts("api.github.com") {
"140.82.114.5" blockedInCN false
"140.82.114.6" blockedInCN false
"140.82.116.5" blockedInCN false
"140.82.116.6" blockedInCN false
"140.82.121.5" blockedInCN true
"140.82.121.6" blockedInCN true
"20.200.245.245" blockedInCN false
"20.201.28.148" blockedInCN false
"20.205.243.168" blockedInCN false
"20.26.156.210" blockedInCN false
"20.27.177.116" blockedInCN false
"20.87.245.6" blockedInCN false
"4.237.22.34" blockedInCN false
}
hosts("github.com") {
"140.82.113.4" blockedInCN false
"140.82.114.3" blockedInCN false
"140.82.116.4" blockedInCN false
"140.82.121.3" blockedInCN true
"140.82.121.4" blockedInCN true
"20.200.245.247" blockedInCN false
"20.201.28.151" blockedInCN false
"20.205.243.166" blockedInCN false
"20.26.156.215" blockedInCN false
"20.27.177.113" blockedInCN false
"20.87.245.0" blockedInCN false
"4.237.22.38" blockedInCN false
}
hosts("raw.githubusercontent.com") {
"185.199.108.133" blockedInCN false
"185.199.109.133" blockedInCN false
"185.199.110.133" blockedInCN false
"185.199.111.133" blockedInCN false
}
}
@@ -9,11 +9,13 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import com.hippo.ehviewer.client.EhCookieStore
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.preferences.DataStorePreferences
import com.ehviewer.core.preferences.PrefDelegate
import com.ehviewer.core.preferences.edit
import com.hippo.ehviewer.client.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
@@ -25,47 +27,12 @@ import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.map
import splitties.preferences.BoolPref
import splitties.preferences.DataStorePreferences
import splitties.preferences.FloatPref
import splitties.preferences.IntPref
import splitties.preferences.LongPref
import splitties.preferences.PrefDelegate
import splitties.preferences.StringOrNullPref
import splitties.preferences.StringPref
import splitties.preferences.StringSetOrNullPref
import splitties.preferences.StringSetPref
import splitties.preferences.edit
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
fun <T> PrefDelegate<T>.getValue(): T = when (this) {
is BoolPref -> value
is IntPref -> value
is FloatPref -> value
is LongPref -> value
is StringPref -> value
is StringOrNullPref -> value
is StringSetPref -> value
is StringSetOrNullPref -> value
} as T
@Suppress("UNCHECKED_CAST")
fun <T> PrefDelegate<T>.setValue(newValue: T) = when (this) {
is BoolPref -> value = newValue as Boolean
is IntPref -> value = newValue as Int
is FloatPref -> value = newValue as Float
is LongPref -> value = newValue as Long
is StringPref -> value = newValue as String
is StringOrNullPref -> value = newValue as? String
is StringSetPref -> value = newValue as Set<String?>
is StringSetOrNullPref -> value = newValue as? Set<String?>
}
@Stable
@Composable
inline fun <R, T> PrefDelegate<T>.collectAsState(crossinline transform: @DisallowComposableCalls (T) -> R): State<R> {
val flow = remember { valueFlow().map { transform(it) } }
val init = getValue()
val init = value
return flow.collectAsState(transform(init))
}
@@ -73,32 +40,28 @@ inline fun <R, T> PrefDelegate<T>.collectAsState(crossinline transform: @Disallo
@Composable
fun <T> PrefDelegate<T>.collectAsState(): State<T> {
val flow = remember { valueFlow() }
val init = getValue()
val init = value
return flow.collectAsState(init)
}
@Stable
@Composable
fun <T> PrefDelegate<T>.asMutableState(): MutableState<T> {
val flow = remember { valueFlow() }
val init = getValue()
val readOnly = flow.collectAsState(init)
val readOnly = collectAsState()
return remember {
object : MutableState<T> {
override var value: T
get() = readOnly.value
set(value) {
setValue(value)
this@asMutableState.value = value
}
override fun component1() = readOnly.value
override fun component2() = ::setValue
override fun component2() = this@asMutableState::value::set
}
}
}
object Settings : DataStorePreferences(null) {
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
@Suppress("ktlint:standard:backing-property-naming")
private val _favFlow = MutableSharedFlow<Unit>()
val favChangesFlow = _favFlow.debounce(1000)
@@ -107,9 +70,9 @@ object Settings : DataStorePreferences(null) {
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
// Eh
val gallerySite = intPref("gallery_site_2", 0).observed { updateWhenGallerySiteChanges() }
val gallerySite = intPref("gallery_site_2", 0).observed(::updateWhenGallerySiteChanges)
val defaultFavSlot = intPref("default_favorite_slot", -2)
val theme = intPref("theme_2", -1).observed { updateWhenThemeChanges() }
val theme = intPref("theme_2", -1).observed(::updateWhenThemeChanges)
val blackDarkTheme = boolPref("black_dark_theme", false)
val harmonizeCategoryColor = boolPref("harmonize_category_color", true)
val launchPage = intPref("launch_page_2", 0)
@@ -118,20 +81,20 @@ object Settings : DataStorePreferences(null) {
val detailSize = intPref("detail_size_2", 0)
val thumbColumns = intPref("thumb_columns", 3)
val showGalleryPages = boolPref("show_gallery_pages", true)
val showReadingProgress = boolPref("show_reading_progress", false)
val showVoteStatus = boolPref("show_vote_status", false)
val showComments = boolPref("show_gallery_comments", true)
val commentThreshold = intPref("comment_threshold", -100)
val showTagTranslations = boolPref(KEY_SHOW_TAG_TRANSLATIONS, false).observed { updateWhenTagTranslationChanges() }
val showTagTranslations = boolPref("show_tag_translations", false).observed(::updateWhenTagTranslationChanges)
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
val showJpnTitle = boolPref("show_jpn_title", false)
val requestNews = boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
val hideHvEvents = boolPref("hide_hv_events", false)
// Download
val mediaScan = boolPref("media_scan", false).observed { updateWhenKeepMediaStatusChanges() }
val mediaScan = boolPref("media_scan", false).observed(::updateWhenKeepMediaStatusChanges)
val multiThreadDownload = intPref("download_thread_2", 3)
val downloadDelay = intPref("download_delay_3", 1000)
val connTimeout = intPref("conn_timeout", 10)
val timeoutSpeed = intPref("timeout_speed_level", 6)
val preloadImage = intPref("preload_image_2", 5)
val downloadOriginImage = boolPref("download_origin_image", false)
@@ -148,16 +111,16 @@ 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)
val desktopSite = boolPref("desktop_site", true)
// About
val backupBeforeUpdate = boolPref("backup_before_update", true)
val useCIUpdateChannel = boolPref("ci_update_channel", AppConfig.isSnapshot)
val updateIntervalDays = intPref("update_interval_days", 7)
val backupBeforeUpdate = boolPref("backup_before_update", false)
val useCIUpdateChannel = boolPref("ci_update_channel", BuildConfig.SNAPSHOT)
val updateIntervalDays = intPref("update_interval_days", 0)
// Misc
val languageFilter = intPref("language_filter", -1)
@@ -167,15 +130,15 @@ object Settings : DataStorePreferences(null) {
val needSignIn = boolPref("need_sign_in", true)
val gridView = boolPref("grid_view", false)
val qSSaveProgress = boolPref("qs_save_progress", true)
val displayName = stringOrNullPref("display_name", null)
val avatar = stringOrNullPref("avatar", null)
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
val displayName = stringOrNullPref("display_name")
val avatar = stringOrNullPref("avatar")
val recentDownloadLabel = stringOrNullPref("recent_download_label")
var downloadScheme by stringOrNullPref("image_scheme", null)
var downloadAuthority by stringOrNullPref("image_authority", null)
var downloadPath by stringOrNullPref("image_path", null)
var downloadQuery by stringOrNullPref("image_query", null)
var downloadFragment by stringOrNullPref("image_fragment", null)
var downloadScheme by stringOrNullPref("image_scheme")
var downloadAuthority by stringOrNullPref("image_authority")
var downloadPath by stringOrNullPref("image_path")
var downloadQuery by stringOrNullPref("image_query")
var downloadFragment by stringOrNullPref("image_fragment")
var archivePasswds by stringSetPref("archive_passwds")
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
@@ -185,7 +148,7 @@ object Settings : DataStorePreferences(null) {
var requestNewsTime by intPref("request_news_time", 0).observed { updateWhenRequestNewsChanges() }
var lastDawnDays by intPref("last_dawn_days", 0)
var recentToplist by stringPref("recent_toplist", "11")
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
var defaultDownloadLabel by stringOrNullPref("default_download_label")
var lastUpdateTime by longPref("last_update_time", BuildConfig.COMMIT_TIME)
// Reader
@@ -195,8 +158,8 @@ object Settings : DataStorePreferences(null) {
val colorFilterMode = intPref("color_filter_mode", 0)
val customBrightness = boolPref("pref_custom_brightness_key", false)
val customBrightnessValue = intPref("custom_brightness_value", 0)
val readingMode = intPref("pref_default_reading_mode_key", ReadingModeType.LEFT_TO_RIGHT.flagValue)
val orientationMode = intPref("pref_default_orientation_type_key", OrientationType.FREE.flagValue)
val readingMode = intPref("pref_default_reading_mode_key", ReadingModeType.DEFAULT.prefValue)
val orientationMode = intPref("pref_default_orientation_type_key", OrientationType.DEFAULT.prefValue)
val showReaderSeekbar = boolPref("pref_show_reader_seekbar", true)
val showPageNumber = boolPref("pref_show_page_number_key", true)
val readerTheme = intPref("pref_reader_theme_key", 1)
@@ -206,7 +169,8 @@ object Settings : DataStorePreferences(null) {
val keepScreenOn = boolPref("pref_keep_screen_on_key", true)
val readerLongTapAction = boolPref("reader_long_tap", true)
val pageTransitions = boolPref("pref_enable_transitions_key", true)
val readerReverseControls = boolPref("reader_volume_keys_inverted", false)
val readWithVolumeKeys = boolPref("reader_volume_keys", false)
val readWithVolumeKeysInverted = boolPref("reader_volume_keys_inverted", false)
val grayScale = boolPref("pref_grayscale", false)
val invertedColors = boolPref("pref_inverted_colors", false)
val readerWebtoonNav = intPref("reader_navigation_mode_webtoon", 0)
@@ -223,43 +187,43 @@ object Settings : DataStorePreferences(null) {
val stripExtraneousAds = boolPref("strip_extraneous_ads", false)
init {
edit { pref ->
if ("CN" == Locale.getDefault().country) {
edit {
if (KEY_SHOW_TAG_TRANSLATIONS !in prefs) showTagTranslations.value = true
if (showTagTranslations !in pref) pref[showTagTranslations] = true
}
if (pref[downloadFilterMode] == DownloadsFilterMode.ARTIST.flag && pref[recentDownloadLabel] == null) {
pref[recentDownloadLabel] = ""
}
val orientation = pref[orientationMode]
if (OrientationType.entries.none { it.prefValue == orientation }) {
pref.remove(orientationMode)
}
if (downloadFilterMode.value == DownloadsFilterMode.ARTIST.flag && recentDownloadLabel.value == null) {
recentDownloadLabel.value = ""
}
}
interface Delegate<R> {
val flowGetter: () -> Flow<Unit>
fun changesFlow(): Flow<Unit>
operator fun getValue(thisRef: Any?, prop: KProperty<*>?): R
operator fun setValue(thisRef: Any?, prop: KProperty<*>?, value: R)
}
private fun intArrayPref(key: String, count: Int) = object : Delegate<IntArray> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
@Suppress("ktlint:standard:backing-property-naming")
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): IntArray = _value.map { it.value }.toIntArray()
private val delegates = Array(count) { intPref("${key}_$it", 0) }
override fun changesFlow(): Flow<Unit> = delegates.asFlow().flatMapMerge { it.changesFlow() }.conflate()
override fun getValue(thisRef: Any?, prop: KProperty<*>?) = IntArray(delegates.size) { delegates[it].value }
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: IntArray) {
check(value.size == count)
edit { value.zip(_value) { v, d -> d.value = v } }
edit { pref -> value.zip(delegates) { v, d -> pref[d] = v } }
}
}
private fun stringArrayPref(key: String, count: Int, defMetaValue: String) = object : Delegate<Array<String>> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
@Suppress("ktlint:standard:backing-property-naming")
private var _value = (0 until count).map { stringPref("${key}_$it", "$defMetaValue $it") }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): Array<String> = _value.map { it.value }.toTypedArray()
private val delegates = Array(count) { stringPref("${key}_$it", "$defMetaValue $it") }
override fun changesFlow(): Flow<Unit> = delegates.asFlow().flatMapMerge { it.changesFlow() }.conflate()
override fun getValue(thisRef: Any?, prop: KProperty<*>?) = Array(delegates.size) { delegates[it].value }
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Array<String>) {
check(value.size == count)
edit { value.zip(_value) { v, d -> d.value = v } }
edit { pref -> value.zip(delegates) { v, d -> pref[d] = v } }
}
}
}
@@ -2,68 +2,73 @@ package com.hippo.ehviewer
import android.app.UiModeManager
import androidx.appcompat.app.AppCompatDelegate
import arrow.core.Either.Companion.catch
import com.ehviewer.core.preferences.PrefDelegate
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.util.isAtLeastS
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.launch
import splitties.init.appCtx
import splitties.preferences.PrefDelegate
import splitties.systemservices.uiModeManager
private const val TAG = "SettingsCollector"
private val collectScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun <T, R : PrefDelegate<T>> R.observed(func: (Unit) -> Unit) = apply { collectScope.launch { changesFlow().collect(func) } }
fun <T, R : Settings.Delegate<T>> R.observed(func: (Unit) -> Unit) = apply { collectScope.launch { flowGetter().collect(func) } }
fun <T, R : Settings.Delegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { flowGetter().collect { flow.emit(Unit) } } }
fun <T, R : PrefDelegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { changesFlow().collect { flow.emit(Unit) } } }
fun <T, R : PrefDelegate<T>> R.observed(func: suspend (T) -> Unit) = apply { collectScope.launch { valueFlow().drop(1).collectLatest(func) } }
fun <T, R : Settings.Delegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { flow.emitAll(changesFlow()) } }
fun <T, R : PrefDelegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { flow.emitAll(changesFlow()) } }
fun updateWhenKeepMediaStatusChanges() {
collectScope.launchIO {
runCatching {
keepNoMediaFileStatus()
suspend fun updateWhenKeepMediaStatusChanges(mediaScan: Boolean) {
withIOContext {
catch {
keepNoMediaFileStatus(mediaScan = mediaScan)
}.onLeft {
logcat(TAG, it)
}
}
}
fun updateWhenThemeChanges() {
collectScope.launch {
suspend fun updateWhenThemeChanges(theme: Int) {
delay(100) // Avoid recompose being cancelled
if (isAtLeastS) {
val mode = when (Settings.theme.value) {
val mode = when (theme) {
AppCompatDelegate.MODE_NIGHT_NO -> UiModeManager.MODE_NIGHT_NO
AppCompatDelegate.MODE_NIGHT_YES -> UiModeManager.MODE_NIGHT_YES
else -> UiModeManager.MODE_NIGHT_AUTO
}
uiModeManager.setApplicationNightMode(mode)
}
AppCompatDelegate.setDefaultNightMode(Settings.theme.value)
}
AppCompatDelegate.setDefaultNightMode(theme)
}
fun updateWhenRequestNewsChanges() {
updateDailyCheckWork(appCtx)
}
fun updateWhenGallerySiteChanges() {
suspend fun updateWhenGallerySiteChanges(gallerySite: Int) {
if (Settings.hasSignedIn.value) {
collectScope.launchIO {
runCatching {
EhEngine.getUConfig()
}.onFailure {
logcat(it)
}
catch {
EhEngine.getUConfig(gallerySite)
}.onLeft {
logcat(TAG, it)
}
}
}
fun updateWhenTagTranslationChanges() {
fun updateWhenTagTranslationChanges(enabled: Boolean) {
if (enabled) {
EhTagDatabase.launchUpdate()
}
}
@@ -15,7 +15,7 @@
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.client.data.GalleryInfo
import com.ehviewer.core.model.GalleryInfo
// Normal Preview (v2): https://*.hath.network/c(m|1|2)/[timed token]/[gid]-[index].(jpg|webp)
// ExHentai Large Preview (v1 Cover): https://s.exhentai.org/t/***
@@ -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()
}
@@ -21,15 +21,22 @@ import arrow.core.partially2
import arrow.core.right
import arrow.fx.coroutines.parMap
import arrow.fx.coroutines.parZip
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.fillInfo
import com.hippo.ehviewer.client.data.filterComments
import com.hippo.ehviewer.client.exception.CloudflareBypassException
import com.hippo.ehviewer.client.exception.EhError
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.InsufficientGpException
import com.hippo.ehviewer.client.exception.IpBannedException
import com.hippo.ehviewer.client.exception.NoHathClientException
import com.hippo.ehviewer.client.exception.NoHitsFoundException
import com.hippo.ehviewer.client.exception.NotLoggedInException
@@ -41,6 +48,7 @@ import com.hippo.ehviewer.client.parser.FavoritesParser
import com.hippo.ehviewer.client.parser.GalleryApiParser
import com.hippo.ehviewer.client.parser.GalleryDetailParser
import com.hippo.ehviewer.client.parser.GalleryListParser
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
import com.hippo.ehviewer.client.parser.GalleryPageParser
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
import com.hippo.ehviewer.client.parser.HomeParser
@@ -58,10 +66,12 @@ import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.HttpStatement
import io.ktor.client.statement.bodyAsChannel
import io.ktor.client.statement.request
import io.ktor.http.Url
import io.ktor.util.moveToByteArray
import io.ktor.utils.io.pool.DirectByteBufferPool
import io.ktor.utils.io.pool.useInstance
import io.ktor.utils.io.readAvailable
@@ -72,6 +82,8 @@ import kotlin.system.measureTimeMillis
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.serialization.SerializationException
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonArray
import kotlinx.serialization.json.put
@@ -86,7 +98,9 @@ private const val MAX_REQUEST_SIZE = 25
private const val MAX_SEQUENTIAL_REQUESTS = 5
private const val REQUEST_INTERVAL = 5000L
fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
private val Url.isLogin get() = segments.firstOrNull() == "bounce_login.php"
private fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
val dir = AppConfig.externalParseErrorDir ?: return
val file = File(dir, ReadableTime.getFilenamableTime() + ".txt")
file.sink().buffer().use { sink ->
@@ -98,7 +112,7 @@ fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
}
}
fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuffer>, e: Throwable): Nothing {
private fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuffer>, e: Throwable): Nothing {
// Don't translate coroutine cancellation
if (e is CancellationException) throw e
@@ -110,36 +124,40 @@ fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuffer>, e:
{ !it.hasRemaining() },
)
if (empty) {
if (EhUtils.isExHentai) {
throw EhException("Sad Panda\n(without panda)")
val message = if (response.request.url.host == EhUrl.DOMAIN_EX) {
"Sad Panda\n(without panda)"
} else {
throw EhException("IP banned")
appCtx.getString(R.string.error_ip_banned)
}
throw IpBannedException(message)
}
// Check Gallery Not Available
body.onLeft {
if ("Gallery Not Available - " in it) throw e
}
// Parse error from rust
val error = body.getOrNull()?.runCatching {
val array = moveToByteArray()
rewind()
Cbor.decodeFromByteArray<EhError>(array)
}?.getOrNull()
// Check parse error thrown by rust
val cause = e.cause
if (e is ParseException && cause is RuntimeException) {
when (val message = cause.message) {
"0" -> throw NoHitsFoundException()
"1" -> throw EhException(R.string.gallery_list_empty_hit_subscription)
"2" -> throw NotLoggedInException()
"3" -> throw NoHathClientException()
"4" -> throw InsufficientFundsException()
is String if message.startsWith('5') -> throw EhException(message.drop(1))
}
}
// Check Gallery Not Available - 404
if (error is EhError.GalleryUnavailable) throw EhException(error.message)
// Check bad response code
response.status.ensureSuccess()
when (error) {
EhError.NoHits -> throw NoHitsFoundException()
EhError.NoWatched -> throw EhException(R.string.gallery_list_empty_hit_subscription)
EhError.NeedLogin -> throw NotLoggedInException()
EhError.NoHathClient -> throw NoHathClientException()
EhError.InsufficientFunds -> throw InsufficientFundsException()
is EhError.IpBanned -> throw IpBannedException(error.message)
is EhError.Error -> throw EhException(error.message)
else -> Unit
}
if (e is ParseException || e is SerializationException) {
body.onLeft { if ("<" !in it) throw EhException(it) }
body.onLeft { if ("<" !in it) throw IpBannedException(it) }
if (Settings.saveParseErrorBody.value) body.saveParseError(e)
throw EhException(appCtx.getString(R.string.error_parse_error), e)
}
@@ -148,9 +166,10 @@ fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuffer>, e:
throw e
}
val httpContentPool = DirectByteBufferPool(8, 0x80000)
private val httpContentPool = DirectByteBufferPool(8, 0x80000)
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = executeSafely { response ->
private suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = executeSafely { response ->
if (response.request.url.isLogin) throw NotLoggedInException()
val body = response.bodyAsUtf8Text()
runSuspendCatching {
block(body)
@@ -159,7 +178,8 @@ suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend
}.getOrThrow()
}
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = executeSafely { response ->
private suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = executeSafely { response ->
if (response.request.url.isLogin) throw NotLoggedInException()
httpContentPool.useInstance { buffer ->
with(response.bodyAsChannel()) { while (!isClosedForRead) readAvailable(buffer) }
buffer.flip()
@@ -174,9 +194,16 @@ suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: s
object EhEngine {
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
val location = response.headers["Location"] ?: throw InsufficientFundsException()
location.takeIf { "bounce_login" !in it } ?: throw NotLoggedInException()
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)
.fetchUsingAsText(GalleryMultiPageViewerPTokenParser::parse)
suspend fun getTorrentList(gid: Long, token: String): TorrentResult {
val url = EhUrl.getTorrentUrl(gid, token)
@@ -212,7 +239,8 @@ object EhEngine {
return ehRequest(url, EhUrl.URL_FORUMS).fetchUsingAsByteBuffer(ProfileParser::parse)
}
suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {
suspend fun getUConfig(gallerySite: Int) {
val url = EhUrl.getUConfigUrl(gallerySite)
runSuspendCatching {
ehRequest(url).fetchUsingAsByteBuffer(UserConfigParser::parse)
}.onFailure { throwable ->
@@ -1,10 +1,10 @@
package com.hippo.ehviewer.client
import arrow.core.memoize
import com.ehviewer.core.database.model.Filter
import com.ehviewer.core.database.model.FilterMode
import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.FilterMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -15,11 +15,11 @@
*/
package com.hippo.ehviewer.client
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.header
@@ -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)
}
@@ -20,17 +20,17 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import arrow.core.Either
import com.ehviewer.core.files.metadataOrNull
import com.ehviewer.core.files.read
import com.ehviewer.core.files.write
import com.ehviewer.core.model.TagNamespace
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess
import com.hippo.ehviewer.util.utf8
import com.hippo.files.metadataOrNull
import com.hippo.files.read
import com.hippo.files.write
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.request.prepareGet
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
@@ -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)
@@ -68,6 +63,7 @@ object EhTagDatabase : CoroutineScope {
// Copied from https://github.com/EhTagTranslation/EhSyringe/blob/c0cf88da04081072492829673aca34380b14dae6/src/plugin/suggest.ts#L37
// TODO: Optimize
private val nsScore = mapOf(
TagNamespace.Location.prefix to 10f,
TagNamespace.Other.prefix to 10f,
TagNamespace.Female.prefix to 9f,
TagNamespace.Male.prefix to 8.5f,
@@ -82,7 +78,7 @@ object EhTagDatabase : CoroutineScope {
data class Tag(val tag: String, val hint: String?, val score: Float)
context(ctx: Context)
context(_: Context)
fun suggestion(rawKeyword: String, expectTranslate: Boolean): List<Tag> {
val keyword = PREFIXES.fold(rawKeyword) { kwd, pfx -> kwd.removePrefix(pfx) }
if (!initialized || keyword.isEmpty()) return emptyList()
@@ -80,8 +80,7 @@ object EhUrl {
else -> ORIGIN_E
}
val uConfigUrl: String
get() = when (Settings.gallerySite.value) {
fun getUConfigUrl(gallerySite: Int = Settings.gallerySite.value) = when (gallerySite) {
SITE_E -> URL_UCONFIG_E
SITE_EX -> URL_UCONFIG_EX
else -> URL_UCONFIG_E
@@ -108,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()
@@ -144,7 +143,7 @@ object EhUrl {
}
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
path = if (path.isNullOrBlank()) emptyList() else listOf(path),
path = if (path.isNullOrBlank()) [] else [path],
host = host,
parameters = Parameters.build(builder),
)
@@ -20,16 +20,21 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Environment
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.net.toUri
import arrow.core.memoize
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.util.withUIContext
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.parser.Archive
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.util.AppConfig
@@ -38,7 +43,7 @@ import com.hippo.ehviewer.util.addTextToClipboard
import com.materialkolor.hct.Hct
import com.materialkolor.ktx.from
import com.materialkolor.ktx.toColor
import eu.kanade.tachiyomi.util.lang.withUIContext
import com.materialkolor.utils.ColorUtils.lstarFromArgb
import splitties.systemservices.downloadManager
object EhUtils {
@@ -151,7 +156,13 @@ object EhUtils {
}
}
val categoryTextColor = Color(0xffe6e0e9)
@ReadOnlyComposable
@Composable
fun getCategoryTextColor(color: Color) = if (isSystemInDarkTheme() == lstarFromArgb(color.toArgb()) > 70) {
MaterialTheme.colorScheme.surface
} else {
LocalContentColor.current
}
val favoriteIconColor = Color(0xffff3040)
@@ -1,13 +0,0 @@
package com.hippo.ehviewer.client.data
interface AbstractDownloadInfo {
var state: Int
var legacy: Int
var time: Long
var label: String?
var speed: Long
var remaining: Long
var finished: Int
var downloaded: Int
var total: Int
}
@@ -1,96 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "GALLERIES")
@TypeConverters(SimpleTagsConverter::class)
data class BaseGalleryInfo(
@PrimaryKey
@ColumnInfo(name = "GID")
override var gid: Long = 0,
@ColumnInfo(name = "TOKEN")
override var token: String = "",
@ColumnInfo(name = "TITLE")
override var title: String? = null,
@ColumnInfo(name = "TITLE_JPN")
override var titleJpn: String? = null,
@ColumnInfo(name = "THUMB")
override var thumbKey: String? = null,
@ColumnInfo(name = "CATEGORY")
override var category: Int = 0,
@ColumnInfo(name = "POSTED")
override var posted: String? = null,
@ColumnInfo(name = "UPLOADER")
override var uploader: String? = null,
@Ignore
override var disowned: Boolean = false,
@ColumnInfo(name = "RATING")
override var rating: Float = 0f,
@Ignore
override var rated: Boolean = false,
@ColumnInfo(name = "SIMPLE_TAGS")
override var simpleTags: List<String>? = null,
@ColumnInfo(name = "PAGES", defaultValue = "0")
override var pages: Int = 0,
@Ignore
override var thumbWidth: Int = 0,
@Ignore
override var thumbHeight: Int = 0,
@ColumnInfo(name = "SIMPLE_LANGUAGE")
override var simpleLanguage: String? = null,
@ColumnInfo(name = "FAVORITE_SLOT")
override var favoriteSlot: Int = NOT_FAVORITED,
@Ignore
override var favoriteName: String? = null,
@Ignore
override var favoriteNote: String? = null,
) : GalleryInfo
class SimpleTagsConverter {
@TypeConverter
fun fromString(value: String) = value.split(", ")
@TypeConverter
fun toString(value: List<String>) = value.joinToString()
}
@@ -25,9 +25,9 @@ import kotlinx.serialization.Serializable
data class FavListUrlBuilder(
val favCat: Int = FAV_CAT_ALL,
val keyword: String? = null,
var jumpTo: String? = null,
var prev: String? = null,
var next: String? = null,
private var jumpTo: String? = null,
private var prev: String? = null,
private var next: String? = null,
) {
val isLocal
get() = favCat == FAV_CAT_LOCAL
@@ -35,6 +35,7 @@ data class FavListUrlBuilder(
fun setIndex(index: String?, isNext: Boolean) {
next = index.takeIf { isNext }
prev = index.takeUnless { isNext }
jumpTo = null
}
fun build() = ehUrl(EhUrl.FAV_PATH) {
@@ -16,10 +16,13 @@
package com.hippo.ehviewer.client.data
import androidx.compose.ui.util.fastFlatMap
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryTagGroup
import com.ehviewer.core.model.PowerStatus
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhFilter
import kotlinx.serialization.Serializable
private val LANGUAGES = arrayOf(
"English",
@@ -38,24 +41,7 @@ private val LANGUAGES = arrayOf(
"Dutch",
)
@Serializable
data class GalleryDetail(
val galleryInfo: BaseGalleryInfo = BaseGalleryInfo(),
var apiUid: Long = -1L,
var apiKey: String? = null,
var torrentCount: Int = 0,
var parent: String? = null,
var newerVersions: List<BaseGalleryInfo> = emptyList(),
var visible: String? = null,
var language: String? = null,
var size: String? = null,
var favoriteCount: Int = 0,
var ratingCount: Int = 0,
val tagGroups: List<GalleryTagGroup>,
var comments: GalleryCommentList,
val previewList: List<GalleryPreview>,
) : GalleryInfo by galleryInfo {
suspend fun fillInfo() {
suspend fun GalleryDetail.fillInfo() {
val index = LANGUAGES.indexOf(language)
if (index != -1) simpleLanguage = GalleryInfo.S_LANGS[index]
simpleTags = tagGroups.fastFlatMap(GalleryTagGroup::tags).map { (text, power, _) ->
@@ -66,7 +52,7 @@ data class GalleryDetail(
}
}
suspend fun filterComments() {
suspend fun GalleryDetail.filterComments() {
comments = with(comments) {
val scoreThreshold = Settings.commentThreshold.value
copy(
@@ -79,4 +65,3 @@ data class GalleryDetail(
)
}
}
}
@@ -15,28 +15,27 @@
*/
package com.hippo.ehviewer.client.data
import com.ehviewer.core.database.model.QuickSearch
import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.addQueryParameter
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
import com.hippo.ehviewer.client.ehUrl
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.ui.main.AdvanceTable
import io.ktor.http.Parameters
import kotlin.text.toIntOrNull
import kotlinx.serialization.Serializable
@Serializable
data class ListUrlBuilder(
var mode: Int = MODE_NORMAL,
private var prev: String? = null,
var next: String? = null,
// Reset to null after initial loading
var jumpTo: String? = null,
private var next: String? = null,
private var jumpTo: String? = null,
private var range: Int = 0,
var category: Int = EhUtils.NONE,
private var mKeyword: String? = null,
var keyword: String? = null,
var hash: String? = null,
var language: Int = -1,
var advanceSearch: Int = -1,
@@ -49,10 +48,13 @@ data class ListUrlBuilder(
next = index.takeIf { isNext }
prev = index.takeUnless { isNext }
range = 0
jumpTo = null
}
fun setJumpTo(to: Int) {
jumpTo = to.takeUnless { it == 0 }?.toString()
var page: Int
get() = (jumpTo?.toInt() ?: 0) + 1
set(value) {
jumpTo = (value - 1).takeUnless { it == 0 }?.toString()
}
fun setRange(to: Int) {
@@ -62,16 +64,15 @@ data class ListUrlBuilder(
jumpTo = null
}
var keyword: String?
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
set(keyword) {
mKeyword = keyword
fun setSeek(date: String) {
setIndex("2", true)
jumpTo = date
}
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,
@@ -83,7 +84,7 @@ data class ListUrlBuilder(
name = name,
mode = mode,
category = category,
keyword = mKeyword,
keyword = keyword,
advanceSearch = advanceSearch,
minRating = minRating,
pageFrom = pageFrom,
@@ -100,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) {
@@ -122,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
@@ -153,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 {
@@ -205,26 +206,23 @@ data class ListUrlBuilder(
}
}
}.buildString()
MODE_UPLOADER, MODE_TAG -> {
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
ehUrl(listOf(path, requireNotNull(mKeyword))) {
ehUrl([path, requireNotNull(keyword)]) {
addQueryParameterIfNotBlank("prev", prev)
addQueryParameterIfNotBlank("next", next)
addQueryParameterIfNotBlank("seek", jumpTo)
addQueryParameterIfNotBlank("range", range.takeIf { it > 0 }?.toString())
}.buildString()
}
MODE_WHATS_HOT -> EhUrl.popularUrl
MODE_IMAGE_SEARCH -> ehUrl {
addQueryParameter("f_shash", requireNotNull(hash))
}.buildString()
MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) {
addQueryParameter("tl", requireNotNull(mKeyword))
addQueryParameter("tl", requireNotNull(keyword))
addQueryParameterIfNotBlank("p", jumpTo)
}.buildString()
else -> throw IllegalStateException("Unexpected value: $mode")
}
@@ -1,5 +1,5 @@
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
import com.ehviewer.core.i18n.R
class CloudflareBypassException : EhException(R.string.cloudflare_bypass_failed)
@@ -0,0 +1,48 @@
package com.hippo.ehviewer.client.exception
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.CborArray
@Serializable
sealed interface EhError {
@CborArray
@Serializable
@SerialName("NoHits")
data object NoHits : EhError
@CborArray
@Serializable
@SerialName("NoWatched")
data object NoWatched : EhError
@CborArray
@Serializable
@SerialName("NeedLogin")
data object NeedLogin : EhError
@CborArray
@Serializable
@SerialName("NoHathClient")
data object NoHathClient : EhError
@CborArray
@Serializable
@SerialName("InsufficientFunds")
data object InsufficientFunds : EhError
@CborArray
@Serializable
@SerialName("GalleryUnavailable")
data class GalleryUnavailable(val message: String) : EhError
@CborArray
@Serializable
@SerialName("IpBanned")
data class IpBanned(val message: String) : EhError
@CborArray
@Serializable
@SerialName("Error")
data class Error(val message: String) : EhError
}
@@ -0,0 +1,13 @@
package com.hippo.ehviewer.client.exception
import androidx.annotation.StringRes
import com.ehviewer.core.i18n.R
sealed class FatalException : EhException {
constructor(message: String) : super(message)
constructor(@StringRes messageId: Int) : super(messageId)
}
class IpBannedException(message: String) : FatalException(message)
class InsufficientGpException : FatalException(R.string.insufficient_gp)
class QuotaExceededException : FatalException(R.string.error_509)
@@ -1,5 +1,5 @@
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
import com.ehviewer.core.i18n.R
class InsufficientFundsException : EhException(R.string.insufficient_funds)
@@ -15,6 +15,6 @@
*/
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
import com.ehviewer.core.i18n.R
class NoHathClientException() : EhException(R.string.download_archive_failure_no_hath)
@@ -1,5 +1,5 @@
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
import com.ehviewer.core.i18n.R
class NoHitsFoundException : EhException(R.string.gallery_list_empty_hit)
@@ -1,5 +1,5 @@
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
import com.ehviewer.core.i18n.R
class NotLoggedInException : EhException(R.string.sign_in_first)
@@ -1,5 +0,0 @@
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
class QuotaExceededException : EhException(R.string.error_509)
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.client.parser
import com.ehviewer.core.util.unescapeXml
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml
import java.nio.ByteBuffer
import kotlinx.serialization.Serializable
import moe.tarsin.coroutines.runSuspendCatching
@@ -15,11 +15,11 @@
*/
package com.hippo.ehviewer.client.parser
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.unescapeXml
import com.hippo.ehviewer.client.EhUtils.getCategory
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.unescapeXml
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -17,9 +17,9 @@ package com.hippo.ehviewer.client.parser
import arrow.core.Either
import arrow.core.getOrElse
import com.hippo.ehviewer.client.data.GalleryCommentList
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryPreviewList
import com.ehviewer.core.model.GalleryCommentList
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryPreviewList
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlinx.serialization.Serializable
@@ -1,6 +1,6 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.ehviewer.core.model.BaseGalleryInfo
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
import kotlinx.serialization.Serializable
@@ -13,14 +13,14 @@ object GalleryListParser {
throw ParseException("Can't parse gallery list", it)
}
val emptyResult = GalleryListResult(null, null, arrayListOf())
val emptyResult = GalleryListResult(null, null, [])
}
@Serializable
data class GalleryListResult(
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
val galleryInfoList: MutableList<BaseGalleryInfo>,
)
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
@@ -26,7 +26,7 @@ object GalleryListUrlParser {
fun parse(urlStr: String) = Either.catch {
val url = Url(urlStr)
if (url.host != EhUrl.DOMAIN_E && url.host != EhUrl.DOMAIN_EX) {
return null
return@catch null
}
val segments = url.segments
if (segments.isEmpty()) {
@@ -42,20 +42,18 @@ object GalleryListUrlParser {
mode = if (head == "uploader") ListUrlBuilder.MODE_UPLOADER else ListUrlBuilder.MODE_TAG
keyword = segments[1].decodeURLQueryComponent(plusIsSpace = true)
}
"toplist.php" -> {
val tl = url.parameters["tl"]
if (tl != null && tl in arrayOf("11", "12", "13", "15")) {
ListUrlBuilder(
mode = ListUrlBuilder.MODE_TOPLIST,
jumpTo = url.parameters["p"],
mKeyword = tl,
keyword = tl,
)
} else {
null
}
}
else -> {
val category = EhUtils.getCategory(head)
if (category != EhUtils.UNKNOWN) {
@@ -15,8 +15,8 @@
*/
package com.hippo.ehviewer.client.parser
import com.ehviewer.core.util.unescapeXml
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml
object GalleryPageParser {
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
@@ -15,9 +15,9 @@
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.util.toEpochMillis
import com.hippo.ehviewer.util.toIntOrDefault
import com.hippo.ehviewer.util.toLocalDateTime
import com.ehviewer.core.util.toEpochMillis
import com.ehviewer.core.util.toIntOrDefault
import com.ehviewer.core.util.toLocalDateTime
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format.char
@@ -19,7 +19,7 @@ package com.hippo.ehviewer.client.parser
import arrow.core.Either.Companion.catch
import arrow.core.getOrElse
import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.ehviewer.core.model.GalleryTagGroup
import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer
@@ -9,7 +9,7 @@ import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.os.SystemClock
import androidx.core.graphics.createBitmap
import eu.kanade.tachiyomi.util.system.logcat
import com.ehviewer.core.util.logcat
import java.nio.ByteBuffer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -12,9 +12,9 @@ import coil3.asImage
import coil3.intercept.Interceptor
import coil3.request.ImageResult
import coil3.request.SuccessResult
import com.ehviewer.core.util.isAtLeastQ
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.image.copyBitmapToAHB
import com.hippo.ehviewer.util.isAtLeastQ
import eu.kanade.tachiyomi.util.system.logcat
import moe.tarsin.coroutines.runSuspendCatching
@RequiresApi(Build.VERSION_CODES.O)
@@ -6,16 +6,16 @@ import coil3.intercept.Interceptor
import coil3.request.ImageRequest
import coil3.request.ImageResult
import coil3.request.SuccessResult
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.isFile
import com.ehviewer.core.files.sendTo
import com.ehviewer.core.files.toUri
import com.hippo.ehviewer.EhApplication.Companion.thumbCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.util.sendTo
import com.hippo.files.delete
import com.hippo.files.isDirectory
import com.hippo.files.isFile
import com.hippo.files.toUri
private val downloadInfoKey = Extras.Key<DownloadInfo?>(default = null)
@@ -35,7 +35,7 @@ object DownloadThumbInterceptor : Interceptor {
info.thumbKey = thumbKey
EhDB.putGalleryInfo(info.galleryInfo)
}
val dir = downloadLocation / info.dirname
val dir = downloadLocation / info.dirname!!
val format = thumbKey.substringAfterLast('.', "")
check(format.isNotBlank())
val thumb = dir / "thumb.$format"
@@ -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()
@@ -7,10 +7,13 @@ import coil3.decode.BlackholeDecoder
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.size.SizeResolver
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryPreview
import com.ehviewer.core.model.V2GalleryPreview
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.client.getV2PreviewKey
import com.hippo.ehviewer.client.thumbUrl
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ktbuilder.execute
@@ -30,7 +33,7 @@ fun ImageRequest.Builder.ehUrl(info: GalleryInfo) = apply {
// Load in original size so the memory cache can be reused for preload requests
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
with(preview) {
val key = imageKey
val key = if (this is V2GalleryPreview) getV2PreviewKey(url) else getThumbKey(url)
data(url)
size(SizeResolver.ORIGINAL)
memoryCacheKey(key)
@@ -19,11 +19,11 @@ import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import java.time.Duration
import java.util.concurrent.TimeUnit
import kotlin.time.Clock
@@ -22,17 +22,29 @@ import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.util.size
import arrow.fx.coroutines.parMapNotNull
import com.ehviewer.core.data.model.asEntity
import com.ehviewer.core.database.model.DownloadArtist
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.database.model.DownloadLabel
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.find
import com.ehviewer.core.files.toOkioPath
import com.ehviewer.core.files.toUri
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.preferences.edit
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.mapNotNull
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.client.exception.FatalException
import com.hippo.ehviewer.spider.COMIC_INFO_FILE
import com.hippo.ehviewer.spider.SpeedTracker
import com.hippo.ehviewer.spider.SpiderQueen
@@ -44,13 +56,7 @@ import com.hippo.ehviewer.spider.readCompatFromPath
import com.hippo.ehviewer.spider.toSimpleTags
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.insertWith
import com.hippo.ehviewer.util.mapNotNull
import com.hippo.ehviewer.util.runAssertingNotMainThread
import com.hippo.files.delete
import com.hippo.files.find
import com.hippo.files.toOkioPath
import com.hippo.files.toUri
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -64,15 +70,12 @@ import logcat.LogPriority
import okio.Path
import okio.Path.Companion.toOkioPath
import okio.Path.Companion.toPath
import splitties.preferences.edit
object DownloadManager : OnSpiderListener, CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
// All download info list
private val allInfoList = runAssertingNotMainThread {
(EhDB.getAllDownloadInfo() as MutableList).apply { sortWith(comparator()) }
}
private val allInfoList = runAssertingNotMainThread { EhDB.getAllDownloadInfo() as MutableList }
val downloadInfoList: List<DownloadInfo>
get() = allInfoList
@@ -97,6 +100,21 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
private val mutex = Mutex()
private val sortMutex = Mutex()
private val sortMode
get() = SortMode.from(Settings.downloadSortMode.value)
var isInitialized by mutableStateOf(false)
private set
init {
launch {
val mode = sortMode
if (mode != SortMode.Default) sortDownloads(mode)
isInitialized = true
}
}
fun containLabel(label: String?): Boolean {
if (label == null) {
return false
@@ -130,7 +148,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
mCurrentTask = info
with(info) {
archiveFile?.let {
return onFinish(pages, pages, pages)
return onFinish(pages, pages, pages, failed = false)
}
state = DownloadInfo.STATE_DOWNLOAD
speed = -1
@@ -178,11 +196,13 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
} else {
// It is new download info
info = DownloadInfo(galleryInfo, galleryInfo.downloadDirname())
info = DownloadInfo(galleryInfo.asEntity(), galleryInfo.downloadDirname())
info.label = label
info.state = DownloadInfo.STATE_WAIT
// Add to all download list and map
allInfoList.insertWith(info, comparator())
sortMutex.withLock {
allInfoList.insertWith(info, sortMode.comparator())
}
mAllInfoMap[galleryInfo.gid] = info
// Add to wait list
@@ -197,7 +217,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
ensureDownload()
// Add it to history
EhDB.putHistoryInfo(info.galleryInfo)
EhDB.putHistoryInfo(info)
}
}
@@ -239,12 +259,14 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}.collectAsState(transform(info)).value
suspend fun startAllDownload() {
val updateList = allInfoList.filter {
val updateList = sortMutex.withLock {
allInfoList.filter {
when (it.state) {
DownloadInfo.STATE_NONE, DownloadInfo.STATE_FAILED -> true
else -> false
}
}
}
if (updateList.isNotEmpty()) {
updateList.onEach {
it.state = DownloadInfo.STATE_WAIT
@@ -256,8 +278,8 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
}
suspend fun addDownload(downloadInfoList: List<DownloadInfo>) {
val comparator = comparator()
suspend fun addDownload(downloadInfoList: List<DownloadInfo>) = sortMutex.withLock {
val comparator = sortMode.comparator()
downloadInfoList.forEach { info ->
if (containDownloadInfo(info.gid)) return@forEach
@@ -287,11 +309,13 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
suspend fun restoreDownload(galleryInfo: BaseGalleryInfo, dirname: String) {
val info = DownloadInfo(galleryInfo, dirname)
val info = DownloadInfo(galleryInfo.asEntity(), dirname)
info.state = DownloadInfo.STATE_NONE
// Add to all download list and map
allInfoList.insertWith(info, comparator())
sortMutex.withLock {
allInfoList.insertWith(info, sortMode.comparator())
}
mAllInfoMap[galleryInfo.gid] = info
// Save to
@@ -350,7 +374,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
EhDB.removeDownloadInfo(info)
// Remove all list and map
sortMutex.withLock {
allInfoList.remove(info)
}
mAllInfoMap.remove(info.gid)
// Update listener
@@ -373,7 +399,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
mAllInfoMap.remove(gid)
}
EhDB.removeDownloadInfo(list)
sortMutex.withLock {
allInfoList.removeAll(list.toSet())
}
// Update listener
list.forEach { mutableNotifyFlow.emit(it) }
@@ -382,7 +410,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
ensureDownload()
}
fun sortDownloads(mode: SortMode) {
suspend fun sortDownloads(mode: SortMode) = sortMutex.withLock {
allInfoList.sortWith(mode.comparator())
}
@@ -501,6 +529,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
val new = exist.copy(label = to)
labelList.add(index, new)
EhDB.updateDownloadLabel(new)
sortMutex.withLock {
allInfoList.forEach {
if (it.label == from) {
it.label = to
@@ -508,6 +537,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
}
}
}
suspend fun deleteLabel(label: String) {
with(labelList) {
@@ -519,15 +549,18 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
removeAt(index)
}
sortMutex.withLock {
allInfoList.forEach {
if (it.label == label) {
it.label = null
}
}
}
}
suspend fun readMetadataFromLocal() {
val list = allInfoList.mapNotNull {
val list = sortMutex.withLock {
allInfoList.mapNotNull {
val updateGallery = it.pages == 0 || it.simpleTags == null
val updateArtist = it.artistInfoList.isEmpty()
if (updateGallery || updateArtist) {
@@ -535,6 +568,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
} else {
null
}
}
}.parMapNotNull(concurrency = 5) { (info, updateGallery, updateArtist) ->
info.downloadDir?.run {
val comicInfo = find(COMIC_INFO_FILE)?.let { readComicInfo(it) }
@@ -574,15 +608,16 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
}
val galleryInfoList = mutableListOf<BaseGalleryInfo>()
val galleryInfoList = buildList {
list.forEach { (info, artists) ->
info?.let {
galleryInfoList.add(it)
add(it)
}
artists?.let { (gid, updateList) ->
EhDB.putDownloadArtist(gid, updateList)
}
}
}
if (galleryInfoList.isNotEmpty()) EhDB.updateGalleryInfo(galleryInfoList)
}
@@ -598,9 +633,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
}
override fun onGet509(index: Int) {
override fun onFatal(error: FatalException) {
launch {
mDownloadListener?.onGet509()
mDownloadListener?.onFatal(error)
stopAllDownload()
}
}
@@ -647,7 +682,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
}
override fun onFinish(finished: Int, downloaded: Int, total: Int) {
override fun onFinish(finished: Int, downloaded: Int, total: Int, failed: Boolean) {
launch {
mSpeedReminder.onFinish()
mCurrentSpider?.let { spider ->
@@ -662,11 +697,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
info.downloaded = downloaded
info.total = total
info.legacy = total - finished
if (info.legacy == 0) {
info.state = DownloadInfo.STATE_FINISH
} else {
info.state = DownloadInfo.STATE_FAILED
}
info.state = if (failed) DownloadInfo.STATE_FAILED else DownloadInfo.STATE_FINISH
EhDB.putDownloadInfo(info)
mDownloadListener?.onFinish(info)
mutableNotifyFlow.emit(info)
@@ -677,9 +708,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
interface DownloadListener {
/**
* Get 509 error
* Fatal error
*/
fun onGet509()
fun onFatal(error: FatalException)
/**
* Start download
@@ -711,9 +742,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
fun start() {
if (currentJob == null) {
currentJob = launch {
tracker.speedFlow().collect { speed ->
updateSpeed(speed.toLong())
}
tracker.speedFlow().collect(::updateSpeed)
}
}
}
@@ -784,8 +813,6 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
private const val TAG = "DownloadManager"
private fun comparator() = SortMode.from(Settings.downloadSortMode.value).comparator()
}
var downloadLocation: Path
@@ -31,16 +31,20 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.savedstate.serialization.decodeFromSavedState
import androidx.savedstate.serialization.encodeToSavedState
import com.hippo.ehviewer.R
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.unsafeLazy
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.client.exception.FatalException
import com.hippo.ehviewer.client.exception.InsufficientGpException
import com.hippo.ehviewer.client.exception.IpBannedException
import com.hippo.ehviewer.client.exception.QuotaExceededException
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.isAtLeastS
import com.hippo.ehviewer.util.unsafeLazy
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -48,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 :
@@ -61,7 +63,7 @@ class DownloadService :
private val notifyManager by unsafeLazy { NotificationManagerCompat.from(this) }
private val downloadingNotification by unsafeLazy { initDownloadingNotification() }
private val downloadedNotification by lazy { initDownloadedNotification() }
private val error509Notification by lazy { init509Notification() }
private val fatalNotification by lazy { initFatalNotification() }
private val channelId by unsafeLazy { "$packageName.download" }
override fun onCreate() {
@@ -109,14 +111,12 @@ class DownloadService :
val label = intent.getStringExtra(KEY_LABEL)
deferredMgr.await().startDownload(decodeFromSavedState(gi), label)
}
ACTION_START_RANGE -> {
val gidList = intent.getLongArrayExtra(KEY_GID_LIST)
if (gidList != null) {
deferredMgr.await().startRangeDownload(gidList)
}
}
ACTION_START_ALL -> deferredMgr.await().startAllDownload()
ACTION_STOP_ALL -> deferredMgr.await().stopAllDownload()
ACTION_CLEAR -> clear()
@@ -137,7 +137,7 @@ class DownloadService :
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(
R.drawable.v_pause_x24,
com.hippo.ehviewer.R.drawable.v_pause_x24,
getString(R.string.stat_download_action_stop_all),
piStopAll,
)
@@ -172,24 +172,27 @@ class DownloadService :
.apply { launch { run() } }
}
private fun init509Notification(): NotificationHandler {
private fun initFatalNotification(): NotificationHandler {
val builder = NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(R.drawable.ic_baseline_warning_24)
.setContentTitle(getString(R.string.stat_509_alert_title))
.setContentText(getString(R.string.stat_509_alert_text))
.setStyle(
NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)),
)
.setSmallIcon(com.hippo.ehviewer.R.drawable.ic_baseline_warning_24)
.setAutoCancel(true)
.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR)
return NotificationHandler(this, notifyManager, builder, ID_509)
return NotificationHandler(this, notifyManager, builder, ID_FATAL)
.apply { launch { run() } }
}
override fun onGet509() {
error509Notification.run {
override fun onFatal(error: FatalException) {
val (title, text) = when (error) {
is IpBannedException -> R.string.error_ip_banned to error.message
is InsufficientGpException -> R.string.insufficient_funds to error.message
is QuotaExceededException -> R.string.stat_509_alert_title to getString(R.string.stat_509_alert_text)
}
fatalNotification.run {
builder.setWhen(System.currentTimeMillis())
.setContentTitle(getString(title))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
show()
}
}
@@ -370,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,
@@ -381,10 +384,10 @@ class DownloadService :
notifyManager.notify(id, builder.build())
}
}
Ops.Cancel -> notifyManager.cancel(id)
Ops.StartForeground -> service.startForeground(id, builder.build())
}
delay(DELAY)
}
}
@@ -412,7 +415,7 @@ class DownloadService :
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
private const val ID_DOWNLOADING = 1
private const val ID_DOWNLOADED = 2
private const val ID_509 = 3
private const val ID_FATAL = 3
private const val DELAY = 1000L // 1s
private val sItemStateArray = LongSparseArray<Boolean>()
private val sItemTitleArray = LongSparseArray<String>()
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.download
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.artists
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.database.model.artists
enum class DownloadsFilterMode(
val flag: Int,
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.download
import com.ehviewer.core.database.model.DownloadInfo
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.dao.DownloadInfo
data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel: Boolean = false) {
val flag
@@ -11,7 +11,6 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
GID(0),
TITLE(1),
TIME(2),
LABEL(3),
PAGES(4),
}
@@ -23,13 +22,16 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
fun comparator(): Comparator<DownloadInfo> {
var comparator = when (field) {
Field.GID -> compareBy { it.gid }
Field.TITLE, Field.LABEL -> compareBy { EhUtils.getSuitableTitle(it) }
Field.TITLE -> compareBy { EhUtils.getSuitableTitle(it) }
Field.TIME -> compareBy { it.time }
Field.PAGES -> compareBy<DownloadInfo> { it.pages }.thenBy { EhUtils.getSuitableTitle(it) }
Field.PAGES -> compareBy<DownloadInfo> { it.pages }
}
if (order == Order.DESC) {
comparator = comparator.reversed()
}
if (field == Field.PAGES) {
comparator = comparator.thenByDescending { it.time }
}
if (groupByDownloadLabel) {
comparator = compareBy<DownloadInfo> { it.label }.then(comparator)
}
@@ -38,15 +40,16 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
companion object {
fun from(flag: Int): SortMode {
val field = flag and 0xF shr 1
val order = flag and 0x1
val fieldValue = flag and 0xF shr 1
val orderValue = flag and 0x1
val groupByDownloadLabel = flag shr 4 == 1
require(field in Field.entries.indices && order in Order.entries.indices)
return SortMode(Field.entries[field], Order.entries[order], groupByDownloadLabel)
val field = Field.entries.find { it.value == fieldValue } ?: Default.field
val order = Order.entries.find { it.value == orderValue } ?: Default.order
return SortMode(field, order, groupByDownloadLabel)
}
val Default = SortMode(Field.TIME, Order.DESC)
val All = listOf(
SortMode(Field.TIME, Order.DESC),
val All = [
Default,
SortMode(Field.TIME, Order.ASC),
SortMode(Field.GID, Order.DESC),
SortMode(Field.GID, Order.ASC),
@@ -54,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),
)
]
}
}
@@ -16,7 +16,11 @@
package com.hippo.ehviewer.gallery
import arrow.autoCloseScope
import com.ehviewer.core.files.openFileDescriptor
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.Settings.archivePasswds
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.image.ImageSource
import com.hippo.ehviewer.image.byteBufferSource
import com.hippo.ehviewer.jni.closeArchive
@@ -29,8 +33,6 @@ import com.hippo.ehviewer.jni.providePassword
import com.hippo.ehviewer.jni.releaseByteBuffer
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.displayName
import com.hippo.files.openFileDescriptor
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.coroutineScope
import moe.tarsin.kt.install
import okio.Path
@@ -40,7 +42,7 @@ typealias PasswdProvider = suspend (PasswdInvalidator) -> String
suspend inline fun <T> useArchivePageLoader(
file: Path,
gid: Long = 0,
info: GalleryInfo? = null,
startPage: Int = 0,
hasAds: Boolean = false,
crossinline passwdProvider: PasswdProvider,
@@ -49,16 +51,22 @@ suspend inline fun <T> useArchivePageLoader(
coroutineScope {
val pfd = install(file.openFileDescriptor("r"))
val size = install(
{ openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name.endsWith(".zip")) },
{ openArchive(pfd.fd, pfd.statSize, info == null || file.name.endsWith(".zip")) },
{ _, _ -> closeArchive() },
)
check(size > 0) { "Archive have no content!" }
if (needPassword() && archivePasswds.filterNotNull().none(::providePassword)) {
if (needPassword() && archivePasswds.none(::providePassword)) {
archivePasswds += passwdProvider(::providePassword)
}
val loader = install(
object : PageLoader(this, gid, startPage, size, hasAds) {
override val title by lazy { FileUtils.getNameFromFilename(file.displayName)!! }
object : PageLoader(this, info, startPage, size, hasAds) {
override val title by lazy {
if (info != null) {
EhUtils.getSuitableTitle(info)
} else {
FileUtils.getNameFromFilename(file.displayName)!!
}
}
override fun getImageExtension(index: Int) = getExtension(index)
@@ -1,8 +1,8 @@
package com.hippo.ehviewer.gallery
import arrow.autoCloseScope
import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.spider.SpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
@@ -24,7 +24,7 @@ suspend inline fun <T> useEhPageLoader(
)
queen.awaitReady()
val loader = install(
object : PageLoader(this, info.gid, startPage, queen.size, info.hasAds) {
object : PageLoader(this, info, startPage, queen.size, info.hasAds) {
override val title by lazy { EhUtils.getSuitableTitle(info) }
override fun getImageExtension(index: Int) = queen.getExtension(index)
@@ -4,6 +4,9 @@ import androidx.collection.SieveCache
import androidx.collection.mutableIntObjectMapOf
import arrow.fx.coroutines.ExitCase
import arrow.fx.coroutines.bracketCase
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.isAtLeastO
import com.ehviewer.core.util.withNonCancellableContext
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.image.Image
@@ -12,8 +15,6 @@ import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.OSUtils
import com.hippo.ehviewer.util.detectAds
import com.hippo.ehviewer.util.displayString
import com.hippo.ehviewer.util.isAtLeastO
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.read
@@ -35,7 +36,7 @@ private val progressScope = CoroutineScope(Dispatchers.IO)
private const val MAX_CACHE_SIZE = 512 * 1024 * 1024
private const val MIN_CACHE_SIZE = 256 * 1024 * 1024
abstract class PageLoader(val scope: CoroutineScope, val gid: Long, startPage: Int, val size: Int, val hasAds: Boolean = false) : AutoCloseable {
abstract class PageLoader(val scope: CoroutineScope, val info: GalleryInfo?, startPage: Int, val size: Int, val hasAds: Boolean = false) : AutoCloseable {
var startPage = startPage.coerceIn(0, size - 1)
private val jobs = mutableIntObjectMapOf<Job>()
@@ -111,14 +112,14 @@ abstract class PageLoader(val scope: CoroutineScope, val gid: Long, startPage: I
override fun close() {
lock.write { cache.evictAll() }
if (gid != 0L) {
info?.run {
progressScope.launch {
EhDB.putReadProgress(gid, startPage)
}
}
}
protected abstract val title: String
abstract val title: String
protected abstract fun getImageExtension(index: Int): String?
@@ -32,10 +32,15 @@ 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
import com.ehviewer.core.files.toUri
import com.ehviewer.core.util.isAtLeastP
import com.ehviewer.core.util.isAtLeastU
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.coil.AnimatedWebPDrawable
import com.hippo.ehviewer.coil.BitmapImageWithExtraInfo
@@ -48,21 +53,17 @@ import com.hippo.ehviewer.jni.munmap
import com.hippo.ehviewer.jni.rewriteGifSource
import com.hippo.ehviewer.ktbuilder.execute
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastU
import com.hippo.ehviewer.util.updateAndGet
import com.hippo.files.openFileDescriptor
import com.hippo.files.toUri
import java.nio.ByteBuffer
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.decrementAndFetch
import kotlin.concurrent.atomics.updateAndFetch
import okio.Path
import splitties.init.appCtx
class Image private constructor(image: CoilImage, private val src: ImageSource) {
val refcnt = AtomicInt(1)
fun pin() = refcnt.updateAndGet { if (it != 0) it + 1 else 0 } != 0
fun pin() = refcnt.updateAndFetch { if (it != 0) it + 1 else 0 } != 0
fun unpin() = (refcnt.decrementAndFetch() == 0).also { if (it) recycle() }
@@ -90,8 +91,10 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
}
companion object {
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
private val sizeResolver = SizeResolver(Size(targetWidth, Dimension.Undefined))
private val sizeResolver = with(appCtx.resources.displayMetrics) {
val targetSize = minOf(widthPixels, heightPixels) * 4 / 3
SizeResolver(Size(targetSize, targetSize))
}
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
val request = with(appCtx) {
@@ -99,7 +102,9 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
onLeft { data(it.source) }
onRight { data(it.source.toUri()) }
size(sizeResolver)
scale(Scale.FILL)
precision(Precision.INEXACT)
maxBitmapSize(Size.ORIGINAL)
allowHardware(false)
hardwareThreshold(Settings.hardwareBitmapThreshold.value)
maybeCropBorder(Settings.cropBorder.value)
@@ -130,7 +135,6 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
}
src.right().decodeCoil(checkExtraneousAds)
}
is ByteBufferSource -> {
if (isAtLeastP && !isAtLeastU) {
rewriteGifSource(src.source)
@@ -5,8 +5,8 @@ import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.imageLoader
import coil3.request.ImageRequest
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryPreview
import com.hippo.ehviewer.coil.ehPreview
import com.hippo.ehviewer.coil.ehUrl
import kotlinx.coroutines.CoroutineScope
@@ -1,8 +1,8 @@
package com.hippo.ehviewer.ktor
import com.hippo.ehviewer.BuildConfig
import androidx.webkit.WebViewCompat
import com.ehviewer.core.network.EhCookieStore
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.plugins.HttpTimeout
@@ -14,9 +14,12 @@ import io.ktor.client.request.header
import io.ktor.http.HttpHeaders
import io.ktor.http.userAgent
import io.ktor.util.appendIfNameAbsent
import splitties.init.appCtx
const val CHROME_MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Mobile Safari/537.36"
private const val CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Safari/537.36"
// It's safe to assume the WebView package will always be present as we require CookieManager anyway
private val WebViewVersion = WebViewCompat.getCurrentWebViewPackage(appCtx)!!.versionName!!.substringBefore('.')
val CHROME_MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$WebViewVersion.0.0.0 Mobile Safari/537.36"
private val CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$WebViewVersion.0.0.0 Safari/537.36"
private const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
private const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.9"
@@ -26,7 +29,7 @@ fun <T : HttpClientEngineConfig> HttpClientConfig<T>.configureCommon(redirect: B
}
install(HttpTimeout) {
reset()
requestTimeoutMillis = 10_000
requestTimeoutMillis = 30_000
}
install(UserAgent)
defaultRequest {
@@ -10,8 +10,6 @@ import android.net.http.UrlRequest
import android.net.http.UrlResponseInfo
import android.os.Build
import androidx.annotation.RequiresExtension
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.util.restartApplication
import io.ktor.client.engine.HttpClientEngineBase
import io.ktor.client.engine.callContext
import io.ktor.client.plugins.HttpTimeoutCapability
@@ -34,21 +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
import splitties.preferences.edit
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
// Limit thread to 1 since we are async & non-blocking
override val dispatcher = Dispatchers.Default.limitedParallelism(1)
// TODO: Investigate https://github.com/FooIbar/EhViewer/issues/2854
// override val dispatcher = Dispatchers.Default.limitedParallelism(1)
override val supportedCapabilities = setOf(HttpTimeoutCapability)
private val executor = dispatcher.asExecutor()
private val executor = dispatcher.limitedParallelism(1).asExecutor()
private val pool = DirectByteBufferPool(32)
private val client by lazy {
with(config) { HttpEngine.Builder(context).apply(config).build() }
@@ -95,7 +93,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
}
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
chunkChan.trySend(byteBuffer).getOrThrow()
chunkChan.trySendBlocking(byteBuffer).getOrThrow()
}
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
@@ -103,15 +101,6 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) {
// Cronet may crash on some devices, fuck xiaomi
// https://github.com/FooIbar/EhViewer/issues/1826
val message = error.message.orEmpty()
if ("ERR_FILE_NOT_FOUND" in message || "ERR_PROXY_CONNECTION_FAILED" in message) {
Settings.edit(true) {
enableCronet.value = false
}
with(config.context) { restartApplication() }
}
if (continuation.isActive) {
continuation.resumeWithException(error)
} else {
@@ -1,12 +1,12 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.ktor
import android.net.http.HttpEngine
import android.os.Build
import androidx.annotation.RequiresExtension
import com.ehviewer.core.util.isAtLeastSExtension7
import java.io.File
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
fun CronetConfig.configureClient(enableQuic: Boolean) {
config = {
setEnableBrotli(true)
@@ -24,3 +24,14 @@ fun CronetConfig.configureClient(enableQuic: Boolean) {
}
}
}
val isCronetAvailable: Boolean
get() = isAtLeastSExtension7 && !isDeviceBlocked
// https://github.com/FooIbar/EhViewer/issues/1826
private val isDeviceBlocked = when (Build.VERSION.INCREMENTAL.substringBefore('.')) {
"V816", // HyperOS 1
"OS2", // HyperOS 2
-> true
else -> false
}
@@ -1,14 +1,18 @@
package com.hippo.ehviewer.ktor
import com.hippo.ehviewer.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)
}
}
}
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.legacy
import com.ehviewer.core.files.read
import com.hippo.ehviewer.spider.SpiderInfo
import com.hippo.files.read
import kotlinx.io.readLineStrict
import okio.Path
@@ -1,23 +1,24 @@
package com.hippo.ehviewer.spider
import com.ehviewer.core.database.util.SimpleTagsConverter
import com.ehviewer.core.files.read
import com.ehviewer.core.files.write
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryTag
import com.ehviewer.core.model.PowerStatus
import com.ehviewer.core.model.TagNamespace
import com.ehviewer.core.model.TagNamespace.Artist
import com.ehviewer.core.model.TagNamespace.Character
import com.ehviewer.core.model.TagNamespace.Cosplayer
import com.ehviewer.core.model.TagNamespace.Female
import com.ehviewer.core.model.TagNamespace.Group
import com.ehviewer.core.model.TagNamespace.Location
import com.ehviewer.core.model.TagNamespace.Male
import com.ehviewer.core.model.TagNamespace.Mixed
import com.ehviewer.core.model.TagNamespace.Other
import com.ehviewer.core.model.TagNamespace.Parody
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryTag
import com.hippo.ehviewer.client.data.PowerStatus
import com.hippo.ehviewer.client.data.SimpleTagsConverter
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.client.data.TagNamespace.Artist
import com.hippo.ehviewer.client.data.TagNamespace.Character
import com.hippo.ehviewer.client.data.TagNamespace.Cosplayer
import com.hippo.ehviewer.client.data.TagNamespace.Female
import com.hippo.ehviewer.client.data.TagNamespace.Group
import com.hippo.ehviewer.client.data.TagNamespace.Male
import com.hippo.ehviewer.client.data.TagNamespace.Mixed
import com.hippo.ehviewer.client.data.TagNamespace.Other
import com.hippo.ehviewer.client.data.TagNamespace.Parody
import com.hippo.files.read
import com.hippo.files.write
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -42,7 +43,7 @@ private val xml = XML {
}
xmlDeclMode = XmlDeclMode.Charset
xmlVersion = XmlVersion.XML10
indent = 2
setIndent(2)
}
fun GalleryInfo.getComicInfo(): ComicInfo {
@@ -60,14 +61,13 @@ fun GalleryInfo.getComicInfo(): ComicInfo {
Group -> groups.addAll(list)
Character -> characters.addAll(list)
Parody -> parodies.addAll(list)
Other -> otherTags.addAll(list)
Location, Other -> otherTags.addAll(list)
Female, Male, Mixed -> ns.prefix.let { prefix ->
list.forEach { tag -> otherTags.add("$prefix:$tag") }
}
else -> Unit
}
}
else -> simpleTags?.forEach { tagString ->
val (namespace, tag) = tagString.split(':', limit = 2)
.takeIf { it.size == 2 } ?: return@forEach // Ignore temp tags that don't have namespace
@@ -76,7 +76,7 @@ fun GalleryInfo.getComicInfo(): ComicInfo {
Group -> groups.add(tag)
Character -> characters.add(tag)
Parody -> if (tag != TAG_ORIGINAL) parodies.add(tag)
Other -> otherTags.add(tag)
Location, Other -> otherTags.add(tag)
Female, Male, Mixed -> ns.prefix.let { otherTags.add("$it:$tag") }
else -> Unit
}
@@ -24,7 +24,7 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.Instant
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -47,7 +47,7 @@ class SpeedTracker(val window: Duration = 1.seconds) {
}
}
fun speedFlow(sample: Duration = 1.seconds) = fixedRate(sample).map {
fun speedFlow(sample: Duration = 1.seconds) = fixedRate(sample).mapNotNull {
mutex.withLock {
start?.let { start ->
val now = Clock.System.now()
@@ -55,8 +55,8 @@ class SpeedTracker(val window: Duration = 1.seconds) {
val window = passed.coerceIn(10.milliseconds, window)
val cutoff = now - window
received.removeIf { time, _ -> time < cutoff }
received.fold(0) { total, _, v -> total + v } / (window / 1.seconds)
} ?: 0.0
(received.fold(0) { total, _, v -> total + v } / (window / 1.seconds)).toLong()
}
}
}
}
@@ -72,16 +72,21 @@ suspend inline fun <R> timeoutBySpeed(
) = coroutineScope {
val onTimeout = AtomicReference<OnTimeout?>(t).let { { e: IOException -> it.exchange(null)?.invoke(e) } }
val watchdog = launch {
delay(Settings.connTimeout.value.seconds)
onTimeout(ConnectTimeoutException(url, Settings.connTimeout.value * 1000L))
val timeout = 10.seconds
delay(timeout)
onTimeout(ConnectTimeoutException(url, timeout.inWholeMilliseconds))
}
val tracker = SpeedTracker(2.seconds)
request {
var prev = 0L
onDownload { done, total ->
val bytesRead = (done - prev).toInt()
if (done == total!!) {
tracker.reset()
} else {
tracker.track(bytesRead)
l(total!!, done, bytesRead)
}
l(total, done, bytesRead)
prev = done
}
timeout { reset() }
@@ -89,10 +94,10 @@ suspend inline fun <R> timeoutBySpeed(
watchdog.cancel()
resp.status.ensureSuccess()
val speedWatchdog = launch {
val timeoutSpeed = speedLevelToSpeed(Settings.timeoutSpeed.value) * 1024.0
tracker.speedFlow(1.seconds).collect { speed ->
if (timeoutSpeed != 0.0 && speed < timeoutSpeed) {
onTimeout(LowSpeedException(url, speed.toLong()))
val timeoutSpeed = speedLevelToSpeed(Settings.timeoutSpeed.value) * 1024L
tracker.speedFlow().collect { speed ->
if (speed < timeoutSpeed) {
onTimeout(LowSpeedException(url, speed))
}
}
}
@@ -104,7 +109,9 @@ suspend inline fun <R> timeoutBySpeed(
}
}
fun speedLevelToSpeed(level: Int) = 2f.pow(level).roundToInt()
const val MIN_SPEED_LEVEL = 3
fun speedLevelToSpeed(level: Int) = if (level == MIN_SPEED_LEVEL) 0 else 2f.pow(level).roundToInt()
inline fun <T, R> MutableObjectIntMap<T>.fold(initial: R, operation: (acc: R, T, Int) -> R): R {
var accumulator = initial
@@ -20,44 +20,45 @@ import arrow.fx.coroutines.autoCloseable
import arrow.fx.coroutines.closeable
import arrow.fx.coroutines.parMap
import arrow.fx.coroutines.resourceScope
import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache
import com.ehviewer.core.database.model.DownloadArtist
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.exists
import com.ehviewer.core.files.find
import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.list
import com.ehviewer.core.files.mkdirs
import com.ehviewer.core.files.moveTo
import com.ehviewer.core.files.openFileDescriptor
import com.ehviewer.core.files.sendTo
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.ehRequest
import com.hippo.ehviewer.client.getImageKey
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.coil.suspendEdit
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.download.tempDownloadDir
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.sendTo
import com.hippo.ehviewer.util.sha1
import com.hippo.files.delete
import com.hippo.files.exists
import com.hippo.files.find
import com.hippo.files.isDirectory
import com.hippo.files.list
import com.hippo.files.mkdirs
import com.hippo.files.moveTo
import com.hippo.files.openFileDescriptor
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsChannel
import io.ktor.client.statement.request
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
import kotlinx.coroutines.CancellationException
import okio.Path
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
class SpiderDen(val info: GalleryInfo) {
private val gid = info.gid
@@ -129,11 +130,9 @@ class SpiderDen(val info: GalleryInfo) {
SpiderQueen.MODE_READ -> {
containInCache(index) || containInDownloadDir(index)
}
SpiderQueen.MODE_DOWNLOAD -> {
containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
}
else -> {
false
}
@@ -154,6 +153,10 @@ class SpiderDen(val info: GalleryInfo) {
removeTempFile(index)
}
fun clearCache() {
repeat(info.pages, ::removeFromCache)
}
private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) {
val name = perFilename(index, extension)
read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } }
@@ -270,22 +273,18 @@ class SpiderDen(val info: GalleryInfo) {
}
}
suspend fun archive() {
if (saveAsCbz) {
downloadDir?.run {
suspend fun archive() = saveAsCbz && downloadDir?.run {
resolve(archiveName).let { file ->
runCatching {
archiveTo(file)
}.onFailure {
file.delete()
if (it is CancellationException) throw it
logcat(it)
}.isFailure
}
}
}
}
}
} == true
// Postpone this to `SpiderQueen.stop` because files may still be in use by reader
suspend fun postArchive(): Boolean {
val dir = downloadDir
val archived = saveAsCbz && dir?.find(archiveName) != null
@@ -335,7 +334,7 @@ class SpiderDen(val info: GalleryInfo) {
downloadDir?.run {
resolve(COMIC_INFO_FILE).also {
if (info !is GalleryDetail && fetchMetadata) {
EhEngine.fillGalleryListByApi(listOf(info))
EhEngine.fillGalleryListByApi([info])
}
info.getComicInfo().apply {
writeComicInfo(this, it)
@@ -349,6 +348,12 @@ class SpiderDen(val info: GalleryInfo) {
}
}
private val sCache by lazy {
diskCache {
directory(appCtx.cacheDir.toOkioPath() / "image_cache")
maxSizeBytes(Settings.readCacheSize.value.coerceIn(320, 5120).toLong() * 1024 * 1024)
}
}
private const val TEMP_SUFFIX = ".tmp"
private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
private val FileHashRegex = Regex("/h/([0-9a-f]{40})")
@@ -1,12 +1,12 @@
package com.hippo.ehviewer.spider
import com.ehviewer.core.files.read
import com.ehviewer.core.files.write
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.coil.edit
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.legacy.readLegacySpiderInfo
import com.hippo.files.read
import com.hippo.files.write
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.io.readByteArray
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor
@@ -20,23 +20,20 @@ import androidx.annotation.IntDef
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.partially1
import com.hippo.ehviewer.R
import com.ehviewer.core.files.find
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryPreview
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
import com.hippo.ehviewer.client.EhUrl.referer
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.ehRequest
import com.hippo.ehviewer.client.exception.FatalException
import com.hippo.ehviewer.client.exception.QuotaExceededException
import com.hippo.ehviewer.client.fetchUsingAsText
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
import com.hippo.ehviewer.util.displayString
import com.hippo.files.find
import eu.kanade.tachiyomi.util.system.logcat
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.decrementAndFetch
import kotlin.concurrent.atomics.incrementAndFetch
@@ -91,9 +88,9 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
}
fun notifyGet509(index: Int) {
fun notifyFatal(error: FatalException) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach { it.onGet509(index) }
mSpiderListeners.forEach { it.onFatal(error) }
}
}
@@ -137,13 +134,14 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
}
private fun notifyAllPageDownloaded() {
private fun notifyAllPageDownloaded(failed: Boolean) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onFinish(
mFinishedPages.load(),
mDownloadedPages.load(),
pageStates.size,
failed,
)
}
}
@@ -216,7 +214,10 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private val prepareScope = CoroutineScope(coroutineContext + SupervisorJob())
private val prepareJob = prepareScope.async { doPrepare() }
private val archiveJob = launch(start = CoroutineStart.LAZY) { spiderDen.archive() }
private val archiveJob = launch(start = CoroutineStart.LAZY) {
val failed = spiderDen.archive()
notifyAllPageDownloaded(failed)
}
private suspend fun doPrepare() {
spiderDen.initDownloadDirIfExist()
@@ -331,7 +332,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
private suspend fun readSpiderInfoFromInternet() = EhEngine.getPreviewList(
getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, 0, false),
getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token),
).run {
val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token, total)
readPreviews(previews, 0, spiderInfo)
@@ -342,17 +343,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
suspend fun getPTokenFromMultiPageViewer(index: Int): String? {
if (!isMpvAvailable) return null
val url = getGalleryMultiPageViewerUrl(
galleryInfo.gid,
galleryInfo.token,
)
return runSuspendCatching {
ehRequest(url, referer).fetchUsingAsText {
GalleryMultiPageViewerPTokenParser.parse(this).forEachIndexed { index, s ->
EhEngine.getPTokenListFromMpv(galleryInfo.gid, galleryInfo.token).forEachIndexed { index, s ->
spiderInfo.pTokenMap[index] = s
}
spiderInfo.pTokenMap[index]
}
}.getOrElse {
logcat(it)
null
@@ -365,12 +360,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
} else {
0
}
val url = getGalleryDetailUrl(
galleryInfo.gid,
galleryInfo.token,
previewIndex,
false,
)
val url = getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, previewIndex)
return runSuspendCatching {
EhEngine.getPreviewList(url).run {
readPreviews(previews, previewIndex, spiderInfo)
@@ -414,8 +404,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
notifyPageSuccess(index)
}
if (mDownloadedPages.load() == size) {
if (mFinishedPages.load() == size) archiveJob.start()
notifyAllPageDownloaded()
if (mFinishedPages.load() == size) {
archiveJob.start()
} else {
notifyAllPageDownloaded(failed = true)
}
}
}
@@ -428,11 +421,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
annotation class State
interface OnSpiderListener {
fun onGetPages(pages: Int) {}
fun onGet509(index: Int) {}
fun onFatal(error: FatalException) {}
fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {}
fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}
fun onPageFailure(index: Int, error: String?, finished: Int, downloaded: Int, total: Int) {}
fun onFinish(finished: Int, downloaded: Int, total: Int) {}
fun onFinish(finished: Int, downloaded: Int, total: Int, failed: Boolean) {}
fun onPageReady(index: Int) {}
}
@@ -555,7 +548,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
if (!force && index in spiderDen) {
return updatePageState(index, STATE_FINISHED)
}
pToken = getPToken(index) ?: return updatePageState(index, STATE_FAILED, PTOKEN_FAILED_MESSAGE)
pToken = getPToken(index) ?: return updatePageState(index, STATE_FAILED, pTokenFailedMessage)
previousPToken = getPToken(index - 1)
// The lock for delay should be acquired before anything else to maintain FIFO order
@@ -601,6 +594,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
previousPToken,
)
}.getOrElse {
if (it is FatalException) throw it
forceHtml = true
return@repeat
}.let {
@@ -650,10 +644,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
}
}
}.onFailure {
when (it) {
is QuotaExceededException -> notifyGet509(index)
// TODO: Check IP ban
}
if (it is FatalException) notifyFatal(it)
error = it.displayString()
if (error == "Invalid page.") {
pTokenLock.withLock {
@@ -664,13 +655,13 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
updatePageState(index, STATE_FAILED, error)
}
}
private val pTokenFailedMessage = appCtx.getString(R.string.error_get_ptoken_error)
}
private val PTOKEN_FAILED_MESSAGE = appCtx.getString(R.string.error_get_ptoken_error)
private val URL_509_PATTERN = Regex("\\.org/.+/509s?\\.gif")
private val Url509Regex = Regex("https://(?:ehgt\\.org/|exhentai\\.org/im)g/509s?\\.gif")
private const val FORCE_RETRY = "Force retry"
private const val WORKER_DEBUG_TAG = "SpiderQueenWorker"
private fun check509(url: String) {
if (URL_509_PATTERN in url) throw QuotaExceededException()
if (Url509Regex.matches(url)) throw QuotaExceededException()
}
@@ -35,17 +35,27 @@ import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.files.delete
import com.ehviewer.core.files.exists
import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.write
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryInfo.Companion.LOCAL_FAVORITED
import com.ehviewer.core.model.GalleryInfo.Companion.NOT_FAVORITED
import com.ehviewer.core.ui.component.LabeledCheckbox
import com.ehviewer.core.util.isAtLeastT
import com.ehviewer.core.util.mapToLongArray
import com.ehviewer.core.util.toEpochMillis
import com.ehviewer.core.util.toLocalDateTime
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.downloadDir
@@ -54,7 +64,6 @@ import com.hippo.ehviewer.download.tempDownloadDir
import com.hippo.ehviewer.ui.destinations.ReaderScreenDestination
import com.hippo.ehviewer.ui.reader.ReaderScreenArgs
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitResult
import com.hippo.ehviewer.ui.tools.awaitSelectDate
@@ -64,18 +73,9 @@ import com.hippo.ehviewer.ui.tools.awaitSelectItemWithIcon
import com.hippo.ehviewer.ui.tools.awaitSelectItemWithIconAndTextField
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.bgWork
import com.hippo.ehviewer.util.isAtLeastT
import com.hippo.ehviewer.util.mapToLongArray
import com.hippo.ehviewer.util.requestPermission
import com.hippo.ehviewer.util.restartApplication
import com.hippo.ehviewer.util.toEpochMillis
import com.hippo.ehviewer.util.toLocalDateTime
import com.hippo.files.delete
import com.hippo.files.exists
import com.hippo.files.isDirectory
import com.hippo.files.write
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlin.time.Clock
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -100,10 +100,10 @@ private fun ensureNoMediaFile(downloadDir: Path) {
private val lck = Mutex()
suspend fun keepNoMediaFileStatus(downloadDir: Path = downloadLocation) {
suspend fun keepNoMediaFileStatus(downloadDir: Path = downloadLocation, mediaScan: Boolean = Settings.mediaScan.value) {
if (downloadDir.isDirectory) {
lck.withLock {
if (Settings.mediaScan.value) {
if (mediaScan) {
removeNoMediaFile(downloadDir)
} else {
ensureNoMediaFile(downloadDir)
@@ -114,7 +114,7 @@ suspend fun keepNoMediaFileStatus(downloadDir: Path = downloadLocation) {
fun getFavoriteIcon(favorited: Boolean) = if (favorited) Icons.Default.Favorite else Icons.Default.FavoriteBorder
context(_: DialogState, context: MainActivity)
context(_: DialogState, _: MainActivity)
suspend fun startDownload(forceDefault: Boolean, vararg galleryInfos: BaseGalleryInfo) {
if (isAtLeastT) {
requestPermission(Manifest.permission.POST_NOTIFICATIONS)
@@ -177,7 +177,7 @@ suspend fun startDownload(forceDefault: Boolean, vararg galleryInfos: BaseGaller
}
context(_: DialogState)
suspend fun modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
suspend fun modifyFavorites(galleryInfo: GalleryInfo): Boolean {
val localFavorited = EhDB.containLocalFavorites(galleryInfo.gid)
if (Settings.hasSignedIn.value) {
val isFavorited = galleryInfo.favoriteSlot != NOT_FAVORITED
@@ -215,7 +215,7 @@ suspend fun modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
}
private suspend fun doModifyFavorites(
galleryInfo: BaseGalleryInfo,
galleryInfo: GalleryInfo,
slot: Int = NOT_FAVORITED,
localFavorited: Boolean = true,
note: String = "",
@@ -233,7 +233,6 @@ private suspend fun doModifyFavorites(
}
false
}
LOCAL_FAVORITED -> {
if (localFavorited) {
EhDB.removeLocalFavorites(galleryInfo)
@@ -248,7 +247,6 @@ private suspend fun doModifyFavorites(
}
!localFavorited
}
in 0..9 -> {
EhEngine.modifyFavorites(gid, token, slot, note)
favoriteSlot = slot
@@ -256,14 +254,13 @@ private suspend fun doModifyFavorites(
favoriteNote = note
true
}
else -> throw EhException("Invalid favorite slot!")
}
FavouriteStatusRouter.notify(galleryInfo)
add
}
suspend fun removeFromFavorites(galleryInfo: BaseGalleryInfo) = doModifyFavorites(
suspend fun removeFromFavorites(galleryInfo: GalleryInfo) = doModifyFavorites(
galleryInfo = galleryInfo,
localFavorited = EhDB.containLocalFavorites(galleryInfo.gid),
)
@@ -305,13 +302,11 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
EhDB.putHistoryInfo(info)
navToReader(info)
}
1 -> if (downloaded) {
confirmRemoveDownload(info)
} else {
startDownload(false, info)
}
2 -> if (favorited) {
runSuspendCatching {
removeFromFavorites(info)
@@ -327,7 +322,6 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
tip(R.string.add_to_favorite_failure)
}
}
3 -> showMoveDownloadLabel(info)
}
}
@@ -400,7 +394,7 @@ suspend fun showMoveDownloadLabel(info: GalleryInfo) {
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
val label = if (selected == 0) null else labels[selected]
DownloadManager.changeLabel(listOf(downloadInfo), label)
DownloadManager.changeLabel([downloadInfo], label)
}
context(_: DialogState)
@@ -419,7 +413,7 @@ suspend fun showMoveDownloadLabelList(list: Collection<DownloadInfo>): String? {
}
context(_: DialogState)
suspend fun awaitSelectDate(): String? {
suspend fun awaitSelectDate(): String {
val initial = LocalDate(2007, 3, 21)
val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY)
val initialMillis = initial.toEpochMillis()
@@ -432,8 +426,7 @@ suspend fun awaitSelectDate(): String? {
override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis in dateRange
},
)
val date = dateMillis?.run { toLocalDateTime().date.toString() }
return date
return dateMillis.toLocalDateTime().date.toString()
}
context(_: Context, _: DialogState)
@@ -10,11 +10,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.ehviewer.core.ui.util.NoopTransitionsVisibilityScope
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
import com.ehviewer.core.ui.util.togetherWith
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LocalGlobalDialogState
import com.hippo.ehviewer.ui.tools.NoopTransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.togetherWith
import com.hippo.ehviewer.util.findActivity
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.CoroutineScope
@@ -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.hippo.ehviewer.Settings
import com.hippo.ehviewer.util.isAtLeastR
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,12 +24,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.thumbUrl
import com.hippo.ehviewer.ui.screen.navWithUrl
import com.hippo.ehviewer.util.addTextToClipboard
@@ -76,7 +76,7 @@ fun GalleryInfoBottomSheet(detail: GalleryDetail) {
LazyColumn(contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues()) {
itemsIndexed(data) { index, (key, content) ->
Row(
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.keyline_margin)).clickable {
modifier = Modifier.padding(horizontal = dimensionResource(id = com.hippo.ehviewer.R.dimen.keyline_margin)).clickable {
if (index == INDEX_PARENT) {
if (content != null) {
navWithUrl(content)
@@ -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,14 +66,13 @@ 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
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.currentCompositeKeyHashCode
@@ -97,14 +99,28 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import androidx.compose.ui.zIndex
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.navigation.compose.rememberNavController
import com.hippo.ehviewer.R
import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.toOkioPath
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.component.LabeledCheckbox
import com.ehviewer.core.ui.component.LocalSideSheetState
import com.ehviewer.core.ui.component.MutableSideSheet
import com.ehviewer.core.ui.icons.EhIcons
import com.ehviewer.core.ui.icons.filled.Subscriptions
import com.ehviewer.core.ui.util.LocalSnackBarFabPadding
import com.ehviewer.core.ui.util.LocalWindowSizeClass
import com.ehviewer.core.util.isAtLeastQ
import com.ehviewer.core.util.isAtLeastR
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.withIOContext
import com.hippo.ehviewer.EhApplication.Companion.initialized
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
@@ -112,8 +128,6 @@ import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.filled.Subscriptions
import com.hippo.ehviewer.ui.destinations.DownloadScreenDestination
import com.hippo.ehviewer.ui.destinations.DownloadsScreenDestination
import com.hippo.ehviewer.ui.destinations.FavouritesScreenDestination
@@ -130,8 +144,6 @@ import com.hippo.ehviewer.ui.screen.asDstWith
import com.hippo.ehviewer.ui.screen.navWithUrl
import com.hippo.ehviewer.ui.settings.showNewVersion
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LabeledCheckbox
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitInputText
import com.hippo.ehviewer.updater.AppUpdater
@@ -141,16 +153,12 @@ import com.hippo.ehviewer.util.calculateFraction
import com.hippo.ehviewer.util.displayString
import com.hippo.ehviewer.util.getParcelableExtraCompat
import com.hippo.ehviewer.util.getUrlFromClipboard
import com.hippo.ehviewer.util.isAtLeastQ
import com.hippo.ehviewer.util.isAtLeastS
import com.hippo.ehviewer.util.sha1
import com.hippo.files.isDirectory
import com.hippo.files.toOkioPath
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.spec.Direction
import com.ramcosta.composedestinations.utils.currentDestinationAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.view.setSecureScreen
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
@@ -173,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
@@ -199,8 +207,17 @@ class MainActivity : EhActivity() {
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
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)
@@ -251,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 ->
@@ -274,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)
@@ -354,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(
@@ -394,7 +420,7 @@ class MainActivity : EhActivity() {
.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)),
) {
Image(
painter = painterResource(id = R.drawable.sadpanda_low_poly),
painter = painterResource(id = com.hippo.ehviewer.R.drawable.sadpanda_low_poly),
contentDescription = null,
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
contentScale = ContentScale.FillWidth,
@@ -532,7 +558,6 @@ val LocalNavDrawerState = compositionLocalOf<DrawerState> { error("CompositionLo
val LocalDrawerHandle = compositionLocalOf<SnapshotStateList<Long>> { error("CompositionLocal LocalDrawerHandle not present!") }
val LocalSnackBarHostState = compositionLocalOf<SnackbarHostState> { error("CompositionLocal LocalSnackBarHostState not present!") }
val LocalSnackBarFabPadding = compositionLocalOf<State<Dp>> { error("CompositionLocal LocalSnackBarFabPadding not present!") }
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope> { error("CompositionLocal LocalSharedTransitionScope not present!") }
@Composable
@@ -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.hippo.ehviewer.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()) {}
}
}
@@ -14,10 +14,10 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavBackStackEntry
import com.ehviewer.core.ui.util.ProvideVectorPainterCache
import com.hippo.ehviewer.ui.theme.EhTheme
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LocalGlobalDialogState
import com.hippo.ehviewer.ui.tools.ProvideVectorPainterCache
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import me.zhanghai.compose.preference.ProvidePreferenceTheme
import me.zhanghai.compose.preference.preferenceTheme
@@ -20,8 +20,8 @@ import android.content.Context
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.GalleryDetail
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryDetail
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -32,7 +32,7 @@ fun openBrowser(url: String) {
if (url.isEmpty()) return
try {
intent.launchUrl(ctx, url.toUri())
} catch (e: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
Toast.makeText(ctx, R.string.no_browser_installed, Toast.LENGTH_LONG).show()
}
}
@@ -7,8 +7,9 @@ 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 com.hippo.ehviewer.util.toIntOrDefault
import kotlin.math.roundToInt
import splitties.init.appCtx
@@ -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) {
@@ -41,6 +41,7 @@ class LinkifyTextView(
val x = event.x + scrollX - totalPaddingLeft
val y = event.y + scrollY - totalPaddingTop
val line = layout.getLineForVertical(y.toInt())
if (x > layout.getLineMax(line)) return@let
val off = layout.getOffsetForHorizontal(line, x)
val spans = text.getSpans<URLSpan>(off, off)
if (spans.isNotEmpty()) {
@@ -1,11 +1,11 @@
package com.hippo.ehviewer.ui.login
import arrow.core.Either.Companion.catch
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -36,7 +36,7 @@ fun postLogin() = GlobalScope.async(Dispatchers.IO) {
// Get cookies for image limits
launch {
catch {
EhEngine.getUConfig(EhUrl.URL_UCONFIG_E)
EhEngine.getUConfig(EhUrl.SITE_E)
EhCookieStore.flush()
}.onLeft {
logcat(it)
@@ -45,7 +45,7 @@ fun postLogin() = GlobalScope.async(Dispatchers.IO) {
// Sad panda check
catch {
EhEngine.getUConfig(EhUrl.URL_UCONFIG_EX)
EhEngine.getUConfig(EhUrl.SITE_EX)
EhCookieStore.flush()
Settings.gallerySite.value = EhUrl.SITE_EX
}.onLeft {
@@ -58,7 +58,13 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.util.LocalWindowSizeClass
import com.ehviewer.core.ui.util.ifTrueThen
import com.ehviewer.core.ui.util.isExpanded
import com.ehviewer.core.ui.util.thenIf
import com.ehviewer.core.util.launchIO
import com.ehviewer.core.util.withUIContext
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
@@ -66,18 +72,12 @@ import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.destinations.WebViewSignInScreenDestination
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
import com.hippo.ehviewer.ui.tools.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.isExpanded
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.displayString
import com.jamal.composeprefs3.ui.ifTrueThen
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Job
import moe.tarsin.launchIO
import moe.tarsin.navigate
@Destination<RootGraph>(start = true)
@@ -142,7 +142,7 @@ fun AnimatedVisibilityScope.SignInScreen(navigator: DestinationsNavigator) = Scr
fun UsernameAndPasswordTextField() {
OutlinedTextField(
state = username,
modifier = Modifier.width(dimensionResource(id = R.dimen.single_max_width))
modifier = Modifier.width(dimensionResource(id = com.hippo.ehviewer.R.dimen.single_max_width))
.semantics { contentType = ContentType.Username }
.thenIf(!showUsernameError) { padding(bottom = 16.dp) },
label = { Text(stringResource(R.string.username)) },
@@ -154,7 +154,7 @@ fun AnimatedVisibilityScope.SignInScreen(navigator: DestinationsNavigator) = Scr
)
OutlinedSecureTextField(
state = password,
modifier = Modifier.width(dimensionResource(id = R.dimen.single_max_width))
modifier = Modifier.width(dimensionResource(id = com.hippo.ehviewer.R.dimen.single_max_width))
.semantics { contentType = ContentType.Password }
.thenIf(!showPasswordError) { padding(bottom = 16.dp) },
label = { Text(stringResource(R.string.password)) },
@@ -180,27 +180,27 @@ fun AnimatedVisibilityScope.SignInScreen(navigator: DestinationsNavigator) = Scr
when {
!windowSizeClass.isExpanded -> {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = R.dimen.keyline_margin)),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = com.hippo.ehviewer.R.dimen.keyline_margin)),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
painter = painterResource(id = com.hippo.ehviewer.R.drawable.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin)),
modifier = Modifier.padding(dimensionResource(id = com.hippo.ehviewer.R.dimen.keyline_margin)),
)
UsernameAndPasswordTextField()
Text(
text = stringResource(id = R.string.app_waring),
modifier = Modifier.widthIn(max = dimensionResource(id = R.dimen.single_max_width)).padding(top = 24.dp),
modifier = Modifier.widthIn(max = dimensionResource(id = com.hippo.ehviewer.R.dimen.single_max_width)).padding(top = 24.dp),
style = MaterialTheme.typography.titleMedium,
)
Text(
text = stringResource(id = R.string.app_waring_2),
modifier = Modifier.widthIn(max = dimensionResource(id = R.dimen.single_max_width)).padding(top = 12.dp),
modifier = Modifier.widthIn(max = dimensionResource(id = com.hippo.ehviewer.R.dimen.single_max_width)).padding(top = 12.dp),
style = MaterialTheme.typography.titleLarge,
)
Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.keyline_margin)))
Row(modifier = Modifier.padding(top = dimensionResource(R.dimen.keyline_margin))) {
Spacer(modifier = Modifier.height(dimensionResource(id = com.hippo.ehviewer.R.dimen.keyline_margin)))
Row(modifier = Modifier.padding(top = dimensionResource(com.hippo.ehviewer.R.dimen.keyline_margin))) {
FilledTonalButton(
onClick = { openBrowser(EhUrl.URL_REGISTER) },
shapes = ButtonDefaults.shapes(),
@@ -255,18 +255,18 @@ fun AnimatedVisibilityScope.SignInScreen(navigator: DestinationsNavigator) = Scr
}
else -> {
Row(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = R.dimen.keyline_margin)),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).systemBarsPadding().padding(dimensionResource(id = com.hippo.ehviewer.R.dimen.keyline_margin)),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.width(dimensionResource(id = R.dimen.signinscreen_landscape_caption_frame_width)).padding(dimensionResource(id = R.dimen.keyline_margin)),
modifier = Modifier.width(dimensionResource(id = com.hippo.ehviewer.R.dimen.signinscreen_landscape_caption_frame_width)).padding(dimensionResource(id = com.hippo.ehviewer.R.dimen.keyline_margin)),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
painter = painterResource(id = com.hippo.ehviewer.R.drawable.ic_launcher_foreground),
contentDescription = null,
alignment = Alignment.Center,
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin)),
modifier = Modifier.padding(dimensionResource(id = com.hippo.ehviewer.R.dimen.keyline_margin)),
)
Text(
text = stringResource(id = R.string.app_waring),
@@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import com.ehviewer.core.network.EhCookieStore
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.Screen
@@ -31,6 +31,7 @@ fun AnimatedVisibilityScope.WebViewSignInScreen(navigator: DestinationsNavigator
Settings.desktopSite.value = false
}
if (EhCookieStore.hasSignedIn()) {
EhCookieStore.flush()
postLogin()
state.webView?.destroy()
bgWork { awaitCancellation() }
@@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.client.parser.Archive
import com.hippo.ehviewer.client.parser.Funds
@@ -43,7 +43,11 @@ import arrow.core.none
import arrow.core.some
import coil3.compose.AsyncImage
import coil3.network.HttpException
import com.hippo.ehviewer.R
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.component.RollingNumber
import com.ehviewer.core.ui.component.RollingNumberPlaceholder
import com.ehviewer.core.util.launch
import com.ehviewer.core.util.launchIO
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils
@@ -65,8 +69,6 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import moe.tarsin.coroutines.runSwallowingWithUI
import moe.tarsin.launch
import moe.tarsin.launchIO
private val limitScope = CoroutineScope(Dispatchers.IO)
private val refreshEvent = MutableSharedFlow<Unit>()
@@ -107,7 +109,7 @@ fun AvatarIcon() {
horizontalAlignment = Alignment.CenterHorizontally,
) {
CircularWavyProgressIndicator()
Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin)))
Spacer(modifier = Modifier.size(dimensionResource(id = com.hippo.ehviewer.R.dimen.keyline_margin)))
Text(text = placeholder)
}
}
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DownloadDone
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Card
@@ -26,6 +27,7 @@ import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableIntStateOf
@@ -35,19 +37,18 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.component.CrystalCard
import com.ehviewer.core.ui.component.GalleryListCardRating
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
import com.ehviewer.core.ui.util.listThumbGenerator
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.GalleryListCardRating
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.listThumbGenerator
import com.hippo.ehviewer.util.FileUtils
@Composable
@@ -60,6 +61,7 @@ fun DownloadCard(
onStop: () -> Unit,
info: DownloadInfo,
selectMode: Boolean,
showProgress: Boolean,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) = CrystalCard(modifier = modifier, onClick = onClick, onLongClick = onLongClick, interactionSource = interactionSource) {
@@ -99,7 +101,14 @@ fun DownloadCard(
DownloadInfo.STATE_NONE -> stringResource(R.string.download_state_none)
DownloadInfo.STATE_WAIT -> stringResource(R.string.download_state_wait)
DownloadInfo.STATE_FAILED -> if (info.legacy <= 0) stateFailed else stateFailed2
DownloadInfo.STATE_FINISH -> stringResource(R.string.download_state_finish)
DownloadInfo.STATE_FINISH -> {
if (showProgress) {
val readProgress by remember { EhDB.getReadProgressFlow(info.gid) }.collectAsState(0)
if (readProgress > 0) "${readProgress + 1}/${info.pages}P" else "${info.pages}P"
} else {
"${info.pages}P"
}
}
else -> null // The item has been removed and this will be disposed soon
}
ProvideTextStyle(MaterialTheme.typography.labelLarge) {
@@ -149,24 +158,24 @@ fun DownloadCard(
Text(
text = categoryText,
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
color = if (Settings.harmonizeCategoryColor.value) Color.Unspecified else EhUtils.categoryTextColor,
color = EhUtils.getCategoryTextColor(categoryColor),
style = MaterialTheme.typography.labelLarge,
)
}
Spacer(modifier = Modifier.weight(1f))
val running = downloadState == DownloadInfo.STATE_WAIT || downloadState == DownloadInfo.STATE_DOWNLOAD
val icon = remember {
movableContentOf<Boolean> {
Icon(imageVector = if (it) Icons.Default.Pause else Icons.Default.PlayArrow, contentDescription = null)
}
val icon = when (downloadState) {
DownloadInfo.STATE_WAIT, DownloadInfo.STATE_DOWNLOAD -> Icons.Default.Pause
DownloadInfo.STATE_FINISH -> Icons.Default.DownloadDone
else -> Icons.Default.PlayArrow
}
if (selectMode) {
Box(modifier = Modifier.offset(4.dp).minimumInteractiveComponentSize()) {
icon(running)
Icon(imageVector = icon, contentDescription = null)
}
} else {
IconButton(onClick = if (running) onStop else onStart, shapes = IconButtonDefaults.shapes(), modifier = Modifier.offset(4.dp)) {
icon(running)
Icon(imageVector = icon, contentDescription = null)
}
}
}
@@ -18,13 +18,13 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil3.compose.AsyncImagePainter
import coil3.compose.rememberAsyncImagePainter
import com.hippo.ehviewer.client.data.GalleryInfo
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.ui.util.SETNodeGenerator
import com.ehviewer.core.ui.util.SharedElementBox
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
import com.ehviewer.core.ui.util.thenIf
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.tools.SETNodeGenerator
import com.hippo.ehviewer.ui.tools.SharedElementBox
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.shouldCrop
import com.hippo.ehviewer.ui.tools.thenIf
@Composable
@NonRestartableComposable
Loaded 100 of 409 files, more files were not shown because too many files have changed in this diff. Show more