Compare commits

..
391 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
368 changed files with 4626 additions and 4403 deletions

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
[*.{kt,kts}] [*.{kt,kts}]
ij_kotlin_imports_layout = * ij_kotlin_imports_layout = *
ij_kotlin_line_break_after_multiline_when_entry = false
ktlint_code_style = intellij_idea ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_class-signature = disabled ktlint_standard_class-signature = disabled
ktlint_standard_mixed-condition-operators = disabled
File renamed without changes.
File renamed without changes.
@@ -13,42 +13,39 @@ on:
jobs: jobs:
baseline-profile: baseline-profile:
runs-on: ubuntu-latest runs-on: nekomoee
steps: steps:
- name: Free disk space
run: rm -rf $AGENT_TOOLSDIRECTORY
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version-file: .java-version
- name: Install Rust Toolchain - name: Setup Android SDK
run: rustup target add armv7-linux-androideabi uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: https://github.com/Swatinem/rust-cache@v2
with: with:
workspaces: "./app/src/main/rust" workspaces: "./app/src/main/rust"
- name: CMake Cache - name: CMake Cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@v5
with: with:
path: app/.cxx path: app/.cxx
key: cmake- key: cmake-
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v6
with: with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }} cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true cache-read-only: true
@@ -59,7 +56,7 @@ jobs:
- name: Upload reports - name: Upload reports
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: reports name: reports
path: benchmark/build/reports path: benchmark/build/reports
+128
View File
@@ -0,0 +1,128 @@
name: CI
on:
push:
branches:
- '*'
pull_request:
workflow_dispatch:
jobs:
check:
runs-on: nekomoee
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
- name: Clippy Check
working-directory: ./app/src/main/rust
run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: true
- name: Check build-logic
run: ./gradlew :build-logic:convention:check
- name: Spotless Check
run: ./gradlew spotlessCheck
default:
runs-on: nekomoee
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Android SDK
uses: https://github.com/android-actions/setup-android@v4
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v5
with:
path: app/.cxx
key: cmake-default-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
restore-keys: cmake-default-
- name: Build
run: ./gradlew assembleDefaultRelease
- name: Upload ARM64
uses: actions/upload-artifact@v3
with:
name: default-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v3
with:
name: default-x86_64-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v3
with:
name: default-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
- name: Upload mapping
uses: actions/upload-artifact@v3
with:
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease/*.txt
- name: Upload native debug symbols
uses: actions/upload-artifact@v3
with:
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
+79
View File
@@ -0,0 +1,79 @@
name: Releases
on:
push:
tags:
- "*"
jobs:
build:
runs-on: nekomoee
steps:
- name: Free disk space
run: rm -rf $AGENT_TOOLSDIRECTORY
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version-file: .java-version
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-10-07
targets: aarch64-linux-android, thumbv7neon-linux-androideabi
- name: Rust Cache
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache/restore@v5
with:
path: app/.cxx
key: cmake-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
cache-read-only: true
- name: Generate Baseline Profile
run: ./gradlew generateBaselineProfile
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
-Prelease
- name: Build
run: ./gradlew assembleRelease
-Prelease
- name: Rename Apks
run: |
mkdir release
mv app/build/outputs/apk/default/release/app-default-universal-release.apk release/EhViewer-${{ github.ref_name }}-default-universal.apk
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk release/EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk release/EhViewer-${{ github.ref_name }}-default-x86_64.apk
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk release/EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultRelease/mapping.txt release/EhViewer-${{ github.ref_name }}-default-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip release/EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
- name: Releases
uses: actions/forgejo-release@v2
with:
direction: upload
url: https://codeberg.org
repo: EhViewer/EhViewer
release-dir: release
body: Bump Version
-1
View File
@@ -1 +0,0 @@
*.sh eol=lf
-188
View File
@@ -1,188 +0,0 @@
name: CI
on:
push:
branches:
- '*'
pull_request:
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
- name: Clippy Check
working-directory: ./app/src/main/rust
run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: true
- name: Check build-logic
run: ./gradlew :build-logic:convention:check
- name: Spotless Check
run: ./gradlew spotlessCheck
- name: Lint Check
run: ./gradlew lintMarshmallowRelease
default:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v4
with:
path: app/.cxx
key: cmake-default-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
restore-keys: cmake-default-
- name: Build
run: ./gradlew assembleDefaultRelease
- name: Upload ARM64
uses: actions/upload-artifact@v4
with:
name: default-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
- name: Upload x86_64
uses: actions/upload-artifact@v4
with:
name: default-x86_64-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
- name: Upload ARM32
uses: actions/upload-artifact@v4
with:
name: default-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
- name: Upload mapping
if: always()
uses: actions/upload-artifact@v4
with:
name: default-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultRelease
- name: Upload native debug symbols
uses: actions/upload-artifact@v4
with:
name: default-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
marshmallow:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Install Rust Toolchain
run: rustup target add armv7-linux-androideabi
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
shared-key: ${{ runner.os }}
save-if: "false"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
- name: CMake Cache
uses: actions/cache@v4
with:
path: app/.cxx
key: cmake-marshmallow-${{ hashFiles('rust-toolchain.toml', 'settings.gradle.kts', 'app/src/main/cpp/CMakeLists.txt', 'app/src/main/rust/Cargo.lock') }}
restore-keys: cmake-marshmallow-
- 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@v5
- 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
- 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
+1
View File
@@ -0,0 +1 @@
25
+71
View File
@@ -1,5 +1,76 @@
# 变更日志 # 变更日志
## [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 ## [1.14.2] - 2025-08-16
### 改进 ### 改进
+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"> <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> <br>EhViewer<br>
</h1> </h1>
<p align="center"> <p align="center">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml"> Standalone fork of
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions"> <a href="https://codeberg.org/EhViewer/EhViewer">EhViewer (codeberg)</a>
</a> maintained at <a href="https://gitea.yrkaierx.com/yenru0/EhViewer">gitea.yrkaierx.com/yenru0/EhViewer</a>
<a href="/LICENSE">
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
</a>
<a href="https://github.com/FooIbar/EhViewer/releases">
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
</a>
<a href="https://github.com/FooIbar/EhViewer/issues">
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
</a>
</p> </p>
<div align="center"> # Changes over upstream
<h3>
<a href="#description">
Description
</a>
<span> | </span>
<a href="#download">
Download
</a>
<span> | </span>
<a href="#screenshot">
Screenshot
</a>
<span> | </span>
<a href="#thanks">
Thanks
</a>
<span> | </span>
<a href="#license">
License
</a>
</h3>
</div>
# Description - Built-in hosts, Cloudflare CDN fallback and domain fronting with SNI spoofing for censored networks (`EhHostsConfig.kt`, `EhDns.kt`, `EhSSLSocketFactory.kt`)
- Cronet engine disabled by default (bypasses the anti-DPI OkHttp stack; `enable_quic` re-enables it)
- Update check disabled by default (manual check still available)
- API request timeout raised to 30 s
- Own application id (`yenru0.yrkaierx.ehviewer`), installs side by side with upstream builds
A modern EhViewer fork dedicated to high-performance # Upstream
with [Material Design 3](https://m3.material.io/) Synced from [EhViewer/EhViewer on codeberg](https://codeberg.org/EhViewer/EhViewer), the continuation of FooIbar's EhViewer. Feature work happens there; this fork tracks it and carries the network/resilience patches above.
and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview) Support
# Download
| Flavor | Minimum Android Version | Notes |
|-------------|-------------------------|--------------------------------|
| Default | 8.0 | Full support |
| Marshmallow | 6.0* | Limited support, no guarantees |
*Devices running Android 6 need to have the [ISRG Root X1](https://letsencrypt.org/certs/isrgrootx1.pem) certificate installed
<a href="https://github.com/FooIbar/EhViewer/releases">
<img alt="Get it on GitHub" src="https://github.com/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/)
# License # License
+19 -19
View File
@@ -9,7 +9,8 @@ plugins {
alias(libs.plugins.ehviewer.android.application) alias(libs.plugins.ehviewer.android.application)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.aboutlibrariesPlugin) alias(libs.plugins.aboutlibraries)
alias(libs.plugins.aboutlibrariesAndroid)
alias(libs.plugins.baselineprofile) alias(libs.plugins.baselineprofile)
} }
@@ -48,21 +49,23 @@ android {
val repoName = providers.exec { val repoName = providers.exec {
commandLine = "git remote get-url origin".split(' ') commandLine = "git remote get-url origin".split(' ')
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:") }.standardOutput.asText.get().trim().removeSuffix(".git").removePrefix("https://codeberg.org/")
.removeSuffix(".git")
val chromeVersion = rootProject.layout.projectDirectory.file("chrome.version").asFile.readText().trim() val snapshot = !hasProperty("release")
defaultConfig { defaultConfig {
applicationId = "moe.tarsin.ehviewer" applicationId = "yenru0.yrkaierx.ehviewer"
versionCode = 180063 versionCode = 180064
versionName = "1.14.2" versionName = if (snapshot) {
// versionNameSuffix = "-SNAPSHOT" "1.15.0-SNAPSHOT"
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"") } else {
"1.15.0"
}
buildConfigField("boolean", "SNAPSHOT", "$snapshot")
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName\"")
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"") buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("long", "COMMIT_TIME", commitTime) buildConfigField("long", "COMMIT_TIME", commitTime)
buildConfigField("String", "REPO_NAME", "\"$repoName\"") buildConfigField("String", "REPO_NAME", "\"$repoName\"")
buildConfigField("String", "CHROME_VERSION", "\"$chromeVersion\"")
ndk { ndk {
if (isRelease) { if (isRelease) {
abiFilters.addAll(supportedAbis) abiFilters.addAll(supportedAbis)
@@ -97,6 +100,9 @@ android {
dex { dex {
useLegacyPackaging = false useLegacyPackaging = false
} }
jniLibs {
excludes += "**/libdatastore_shared_counter.so" // DataStore multi-process
}
resources { resources {
// Required by Layout Inspector // Required by Layout Inspector
pickFirsts += "/META-INF/androidx.compose.ui_ui.version" pickFirsts += "/META-INF/androidx.compose.ui_ui.version"
@@ -168,16 +174,14 @@ baselineProfile {
dependencies { dependencies {
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.core.i18n) implementation(projects.core.i18n)
implementation(projects.core.ui)
// https://developer.android.com/jetpack/androidx/releases/activity // https://developer.android.com/jetpack/androidx/releases/activity
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.biometric) implementation(libs.androidx.biometric)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)
implementation(libs.androidx.webkit)
// https://developer.android.com/jetpack/androidx/releases/compose-material3
api(platform(libs.compose.bom))
implementation(libs.bundles.compose)
implementation(libs.compose.destinations.core) implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.compiler) ksp(libs.compose.destinations.compiler)
@@ -200,7 +204,6 @@ dependencies {
implementation(libs.androidx.paging.compose) implementation(libs.androidx.paging.compose)
// https://developer.android.com/jetpack/androidx/releases/room // https://developer.android.com/jetpack/androidx/releases/room
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.paging) implementation(libs.androidx.room.paging)
implementation(libs.androidx.work.runtime) implementation(libs.androidx.work.runtime)
@@ -212,8 +215,6 @@ dependencies {
// https://square.github.io/okhttp/changelogs/changelog/ // https://square.github.io/okhttp/changelogs/changelog/
implementation(platform(libs.okhttp.bom)) implementation(platform(libs.okhttp.bom))
implementation(platform(libs.okio.bom))
implementation(libs.logcat) implementation(libs.logcat)
implementation(libs.diff) implementation(libs.diff)
@@ -254,13 +255,12 @@ kotlin {
optIn.addAll( optIn.addAll(
"coil3.annotation.ExperimentalCoilApi", "coil3.annotation.ExperimentalCoilApi",
"androidx.paging.ExperimentalPagingApi", "androidx.paging.ExperimentalPagingApi",
"kotlinx.serialization.ExperimentalSerializationApi", "me.saket.telephoto.ExperimentalTelephotoApi",
) )
} }
} }
ksp { ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui") arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
} }
+4
View File
@@ -15,6 +15,10 @@
java.nio.ByteBuffer source; java.nio.ByteBuffer source;
} }
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
static boolean isLinkBufferComposerEnabled() return true;
}
# Ktor logger # Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder -dontwarn org.slf4j.impl.StaticLoggerBinder
+1 -5
View File
@@ -39,7 +39,7 @@
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
android:banner="@mipmap/ic_leanback_banner" android:banner="@mipmap/ic_leanback_banner"
android:configChanges="screenSize" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/AppTheme.Splash" android:theme="@style/AppTheme.Splash"
@@ -118,10 +118,6 @@
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings" />
<activity <activity
android:name=".shortcuts.ShortcutsActivity" android:name=".shortcuts.ShortcutsActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
+34 -26
View File
@@ -4,17 +4,20 @@ include(ExternalProject)
include(FetchContent) include(FetchContent)
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto \ string(CONCAT CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -ffast-math -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto "
-mllvm -polly \ "-ffile-prefix-map=${ANDROID_TOOLCHAIN_ROOT}= "
-mllvm -polly-run-dce \ "-ffile-prefix-map=${CMAKE_SOURCE_DIR}= "
-mllvm -polly-run-inliner \ "-ffile-prefix-map=${CMAKE_BINARY_DIR}= "
-mllvm -polly-isl-arg=--no-schedule-serialize-sccs \ "-mllvm -polly "
-mllvm -polly-ast-use-context \ "-mllvm -polly-run-dce "
-mllvm -polly-detect-keep-going \ "-mllvm -polly-run-inliner "
-mllvm -polly-position=before-vectorizer \ "-mllvm -polly-isl-arg=--no-schedule-serialize-sccs "
-mllvm -polly-vectorizer=stripmine \ "-mllvm -polly-ast-use-context "
-mllvm -polly-detect-profitability-min-per-loop-insts=40 \ "-mllvm -polly-detect-keep-going "
-mllvm -polly-invariant-load-hoisting") "-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") endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
option(BUILD_TESTING OFF) option(BUILD_TESTING OFF)
@@ -29,7 +32,7 @@ option(XZ_TOOL_XZDEC OFF)
FetchContent_Declare( FetchContent_Declare(
liblzma liblzma
GIT_REPOSITORY https://github.com/tukaani-project/xz.git GIT_REPOSITORY https://github.com/tukaani-project/xz.git
GIT_TAG v5.8.1 GIT_TAG v5.8.3
GIT_SHALLOW 1 GIT_SHALLOW 1
) )
@@ -38,23 +41,27 @@ include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
# Build GNUTLS libnettle # Build GNUTLS libnettle
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
execute_process( set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}")
COMMAND wsl wslpath ${CMAKE_CURRENT_LIST_DIR} set(TOOLCHAIN "$ANDROID_HOME/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin")
OUTPUT_VARIABLE CURRENT_DIR set(WSL wsl)
OUTPUT_STRIP_TRAILING_WHITESPACE
)
else () else ()
set(CURRENT_DIR ${CMAKE_CURRENT_LIST_DIR}) set(TOOLCHAIN "${ANDROID_TOOLCHAIN_ROOT}/bin")
endif () endif ()
set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}") set(CONFIGURE_COMMAND ${WSL} ./.bootstrap && ${WSL} ./configure --disable-public-key --disable-dependency-tracking
"--host=${CMAKE_C_COMPILER_TARGET}"
"CFLAGS=${CMAKE_C_FLAGS} -Qunused-arguments"
"AR=${TOOLCHAIN}/llvm-ar"
"CC=${TOOLCHAIN}/clang --target=${CMAKE_C_COMPILER_TARGET}"
"RANLIB=${TOOLCHAIN}/llvm-ranlib"
)
ExternalProject_Add( ExternalProject_Add(
nettle nettle
GIT_REPOSITORY https://gitlab.com/gnutls/nettle.git 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_SHALLOW 1
GIT_CONFIG core.autocrlf=input GIT_CONFIG core.autocrlf=input
CONFIGURE_COMMAND bash ${CURRENT_DIR}/configure_nettle.sh ${NDK_VERSION} ${CMAKE_C_COMPILER_TARGET} CONFIGURE_COMMAND ${CONFIGURE_COMMAND}
BUILD_COMMAND bash -c "make -j4 libnettle.a CFLAGS='${CMAKE_C_FLAGS} -Qunused-arguments'" BUILD_COMMAND ${WSL} make -j4 libnettle.a
INSTALL_COMMAND "" INSTALL_COMMAND ""
BUILD_IN_SOURCE 1 BUILD_IN_SOURCE 1
BUILD_BYPRODUCTS <SOURCE_DIR>/libnettle.a BUILD_BYPRODUCTS <SOURCE_DIR>/libnettle.a
@@ -96,7 +103,7 @@ set(LIBARCHIVE_PATCH
FetchContent_Declare( FetchContent_Declare(
libarchive libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive.git GIT_REPOSITORY https://github.com/libarchive/libarchive.git
GIT_TAG v3.8.1 GIT_TAG v3.8.7
GIT_SHALLOW 1 GIT_SHALLOW 1
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH} PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
) )
@@ -133,8 +140,8 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ../../../../rust-
unset(Rust_TOOLCHAIN CACHE) unset(Rust_TOOLCHAIN CACHE)
FetchContent_Declare( FetchContent_Declare(
Corrosion Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git GIT_REPOSITORY https://github.com/FooIbar/corrosion.git
GIT_TAG v0.5.2 GIT_TAG v0.6.1
GIT_SHALLOW 1 GIT_SHALLOW 1
) )
@@ -143,8 +150,9 @@ corrosion_import_crate(
MANIFEST_PATH ../rust/Cargo.toml MANIFEST_PATH ../rust/Cargo.toml
LOCKED LOCKED
FEATURES $<IF:$<VERSION_GREATER_EQUAL:${ANDROID_NATIVE_API_LEVEL},26>,android-26,android> 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) corrosion_link_libraries(ehviewer_rust android jnigraphics log webp webpdemux)
# Build and link our app's native lib # 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]; byte digest[SHA1_DIGEST_SIZE];
sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest); sha1_digest(&ctx, digest);
byte byte; byte byte;
char hex_digest[2 * SHA1_DIGEST_SIZE + 1]; char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
@@ -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 -> { is WebContent.Url -> {
wv.loadUrl(content.url, content.additionalHttpHeaders) wv.loadUrl(content.url, content.additionalHttpHeaders)
} }
is WebContent.Data -> { is WebContent.Data -> {
wv.loadDataWithBaseURL( wv.loadDataWithBaseURL(
content.baseUrl, content.baseUrl,
@@ -190,14 +189,12 @@ fun WebView(
content.historyUrl, content.historyUrl,
) )
} }
is WebContent.Post -> { is WebContent.Post -> {
wv.postUrl( wv.postUrl(
content.url, content.url,
content.postData, content.postData,
) )
} }
is WebContent.NavigatorOnly -> { is WebContent.NavigatorOnly -> {
// NO-OP // NO-OP
} }
@@ -502,11 +499,9 @@ class WebViewNavigator(private val coroutineScope: CoroutineScope) {
event.encoding, event.encoding,
event.historyUrl, event.historyUrl,
) )
is NavigationEvent.LoadUrl -> { is NavigationEvent.LoadUrl -> {
loadUrl(event.url, event.additionalHttpHeaders) loadUrl(event.url, event.additionalHttpHeaders)
} }
is NavigationEvent.PostUrl -> { is NavigationEvent.PostUrl -> {
postUrl(event.url, event.postData) postUrl(event.url, event.postData)
} }
@@ -22,6 +22,7 @@ import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composer import androidx.compose.runtime.Composer
import androidx.compose.runtime.tooling.ComposeStackTraceMode
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import coil3.EventListener import coil3.EventListener
@@ -37,6 +38,16 @@ import coil3.request.allowRgb565
import coil3.request.crossfade import coil3.request.crossfade
import coil3.serviceLoaderEnabled import coil3.serviceLoaderEnabled
import coil3.util.DebugLogger 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.client.EhTagDatabase
import com.hippo.ehviewer.coil.AnimatedWebPDecoder import com.hippo.ehviewer.coil.AnimatedWebPDecoder
import com.hippo.ehviewer.coil.CropBorderInterceptor 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.MergeInterceptor
import com.hippo.ehviewer.coil.QrCodeInterceptor import com.hippo.ehviewer.coil.QrCodeInterceptor
import com.hippo.ehviewer.dailycheck.checkDawn import com.hippo.ehviewer.dailycheck.checkDawn
import com.hippo.ehviewer.dao.SearchDatabase
import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadsFilterMode import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.ktbuilder.diskCache 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.Cronet
import com.hippo.ehviewer.ktor.configureClient import com.hippo.ehviewer.ktor.configureClient
import com.hippo.ehviewer.ktor.configureCommon import com.hippo.ehviewer.ktor.configureCommon
import com.hippo.ehviewer.ktor.isCronetAvailable
import com.hippo.ehviewer.ui.keepNoMediaFileStatus import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.lockObserver
import com.hippo.ehviewer.ui.screen.detailCache import com.hippo.ehviewer.ui.screen.detailCache
import com.hippo.ehviewer.ui.tools.dataStateFlow import com.hippo.ehviewer.ui.tools.dataStateFlow
import com.hippo.ehviewer.ui.tools.initSETConnection
import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.CrashHandler import com.hippo.ehviewer.util.CrashHandler
import com.hippo.ehviewer.util.FavouriteStatusRouter import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.FileUtils import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.OSUtils 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.HttpClient
import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.engine.okhttp.OkHttp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -83,19 +83,16 @@ import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import logcat.asLog import logcat.asLog
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import splitties.arch.room.roomDb
import splitties.init.appCtx import splitties.init.appCtx
private val lifecycle = ProcessLifecycleOwner.get().lifecycle private val lifecycle = ProcessLifecycleOwner.get().lifecycle
private val lifecycleScope = lifecycle.coroutineScope private val lifecycleScope = lifecycle.coroutineScope
class EhApplication : class EhApplication : Application(), SingletonImageLoader.Factory {
Application(), override fun onCreate() = with(lifecycleScope) {
SingletonImageLoader.Factory {
override fun onCreate() {
initSETConnection() initSETConnection()
// Initialize Settings on first access // Initialize Settings on first access
lifecycleScope.launchIO { launchIO {
val mode = Settings.theme.value val mode = Settings.theme.value
if (!isAtLeastS) { if (!isAtLeastS) {
withUIContext { withUIContext {
@@ -111,28 +108,29 @@ class EhApplication :
} }
} }
} }
lifecycle.addObserver(lockObserver)
CrashHandler.install() CrashHandler.install()
super.onCreate() super.onCreate()
System.loadLibrary("ehviewer") System.loadLibrary("ehviewer")
lifecycleScope.launchIO { launch {
launchUI { FavouriteStatusRouter.collect { info ->
FavouriteStatusRouter.collect { info -> detailCache[info.gid]?.apply {
detailCache[info.gid]?.apply { favoriteSlot = info.favoriteSlot
favoriteSlot = info.favoriteSlot favoriteName = info.favoriteName
favoriteName = info.favoriteName favoriteNote = info.favoriteNote
favoriteNote = info.favoriteNote
}
} }
} }
}
launchIO {
EhTagDatabase.launchUpdate() EhTagDatabase.launchUpdate()
@Suppress("UNUSED_EXPRESSION")
launch { EhDB } launch { EhDB }
launchIO { dataStateFlow.value } launch { dataStateFlow.value }
launchIO { OSUtils.totalMemory } launch { OSUtils.totalMemory }
launch { launch {
if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode !in Settings.snapshot()) { if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode !in Settings.snapshot()) {
Settings.downloadFilterMode.value = DownloadsFilterMode.CUSTOM.flag Settings.downloadFilterMode.value = DownloadsFilterMode.CUSTOM.flag
} }
initialized = true
DownloadManager.readMetadataFromLocal() DownloadManager.readMetadataFromLocal()
} }
launch { launch {
@@ -146,7 +144,9 @@ class EhApplication :
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
StrictMode.enableDefaults() StrictMode.enableDefaults()
Composer.setDiagnosticStackTraceEnabled(true) Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.SourceInformation)
} else {
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.Auto)
} }
} }
@@ -196,7 +196,7 @@ class EhApplication :
add(GifDecoder.Factory()) add(GifDecoder.Factory())
} }
} }
diskCache { imageCache } diskCache { thumbCache }
crossfade(300) crossfade(300)
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed) val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
if (drawable != null) error(drawable.asImage(true)) if (drawable != null) error(drawable.asImage(true))
@@ -214,8 +214,12 @@ class EhApplication :
} }
companion object { companion object {
@Volatile
var initialized = false
private set
val ktorClient by lazy { val ktorClient by lazy {
if (isAtLeastSExtension7 && Settings.enableCronet.value) { if (isCronetAvailable && Settings.enableQuic.value) {
HttpClient(Cronet) { HttpClient(Cronet) {
engine { configureClient(Settings.enableQuic.value) } engine { configureClient(Settings.enableQuic.value) }
configureCommon() configureCommon()
@@ -234,10 +238,10 @@ class EhApplication :
} }
} }
val imageCache by lazy { val thumbCache by lazy {
diskCache { diskCache {
directory(appCtx.cacheDir.toOkioPath() / "image_cache") directory(appCtx.cacheDir.toOkioPath() / "thumb")
maxSizeBytes(Settings.readCacheSize.value.coerceIn(320, 5120).toLong() * 1024 * 1024) maxSizeBytes(80L * 1024 * 1024)
} }
} }
+45 -37
View File
@@ -15,28 +15,31 @@
*/ */
package com.hippo.ehviewer package com.hippo.ehviewer
import android.content.Context import androidx.room.execSQL
import android.net.Uri import androidx.room.useWriterConnection
import arrow.fx.coroutines.resource import arrow.fx.coroutines.resource
import arrow.fx.coroutines.resourceScope import arrow.fx.coroutines.resourceScope
import com.hippo.ehviewer.client.data.BaseGalleryInfo import com.ehviewer.core.data.model.asEntity
import com.hippo.ehviewer.dao.DownloadArtist import com.ehviewer.core.database.EhDatabase
import com.hippo.ehviewer.dao.DownloadDirname import com.ehviewer.core.database.Schema17to18
import com.hippo.ehviewer.dao.DownloadInfo import com.ehviewer.core.database.getDatabasePath
import com.hippo.ehviewer.dao.DownloadLabel import com.ehviewer.core.database.model.DownloadArtist
import com.hippo.ehviewer.dao.EhDatabase import com.ehviewer.core.database.model.DownloadDirname
import com.hippo.ehviewer.dao.Filter import com.ehviewer.core.database.model.DownloadInfo
import com.hippo.ehviewer.dao.HistoryInfo import com.ehviewer.core.database.model.DownloadLabel
import com.hippo.ehviewer.dao.LocalFavoriteInfo import com.ehviewer.core.database.model.Filter
import com.hippo.ehviewer.dao.ProgressInfo import com.ehviewer.core.database.model.GalleryEntity
import com.hippo.ehviewer.dao.QuickSearch import com.ehviewer.core.database.model.HistoryInfo
import com.hippo.ehviewer.dao.Schema17to18 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.download.DownloadManager
import com.hippo.ehviewer.util.sendTo
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import okio.Path import okio.Path
import okio.Path.Companion.toOkioPath
import splitties.arch.room.roomDb
object EhDB { object EhDB {
private const val DB_NAME = "eh.db" private const val DB_NAME = "eh.db"
@@ -44,15 +47,15 @@ object EhDB {
addMigrations(Schema17to18()) addMigrations(Schema17to18())
} }
suspend fun putGalleryInfo(galleryInfo: BaseGalleryInfo) { suspend fun putGalleryInfo(galleryInfo: GalleryEntity) {
db.galleryDao().upsert(galleryInfo) db.galleryDao().upsert(galleryInfo)
} }
private suspend fun deleteGalleryInfo(galleryInfo: BaseGalleryInfo) { private suspend fun deleteGalleryInfo(galleryInfo: GalleryEntity) {
runCatching { db.galleryDao().delete(galleryInfo) } runCatching { db.galleryDao().delete(galleryInfo) }
} }
suspend fun updateGalleryInfo(galleryInfoList: List<BaseGalleryInfo>) { suspend fun updateGalleryInfo(galleryInfoList: List<GalleryEntity>) {
db.galleryDao().update(galleryInfoList) db.galleryDao().update(galleryInfoList)
} }
@@ -146,6 +149,8 @@ object EhDB {
dao.fill(raw.position) dao.fill(raw.position)
} }
suspend fun searchDownloadLabel(keyword: String, limit: Int) = db.downloadLabelDao().search("%$keyword%", limit)
suspend fun putDownloadArtist(gid: Long, artists: List<DownloadArtist>) { suspend fun putDownloadArtist(gid: Long, artists: List<DownloadArtist>) {
if (artists.isNotEmpty()) { if (artists.isNotEmpty()) {
val dao = db.downloadArtistDao() 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) db.localFavoritesDao().deleteByKey(galleryInfo.gid)
deleteGalleryInfo(galleryInfo) deleteGalleryInfo(galleryInfo.asEntity())
} }
suspend fun removeLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) { suspend fun removeLocalFavorites(galleryInfoList: Collection<GalleryInfo>) {
galleryInfoList.forEach { galleryInfoList.forEach {
removeLocalFavorites(it) removeLocalFavorites(it)
} }
@@ -170,12 +175,12 @@ object EhDB {
return dao.contains(gid) return dao.contains(gid)
} }
suspend fun putLocalFavorites(galleryInfo: BaseGalleryInfo) { suspend fun putLocalFavorites(galleryInfo: GalleryInfo) {
putGalleryInfo(galleryInfo) putGalleryInfo(galleryInfo.asEntity())
db.localFavoritesDao().upsert(LocalFavoriteInfo(galleryInfo.gid)) db.localFavoritesDao().upsert(LocalFavoriteInfo(galleryInfo.gid))
} }
suspend fun putLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) { suspend fun putLocalFavorites(galleryInfoList: Collection<GalleryInfo>) {
galleryInfoList.forEach { galleryInfoList.forEach {
putLocalFavorites(it) putLocalFavorites(it)
} }
@@ -221,8 +226,8 @@ object EhDB {
fun searchLocalFav(keyword: String) = db.localFavoritesDao().joinListLazy("*$keyword*") fun searchLocalFav(keyword: String) = db.localFavoritesDao().joinListLazy("*$keyword*")
suspend fun putHistoryInfo(galleryInfo: BaseGalleryInfo) { suspend fun putHistoryInfo(galleryInfo: GalleryInfo) {
putGalleryInfo(galleryInfo) putGalleryInfo(galleryInfo.asEntity())
db.historyDao().upsert(HistoryInfo(galleryInfo.gid)) db.historyDao().upsert(HistoryInfo(galleryInfo.gid))
} }
@@ -239,7 +244,7 @@ object EhDB {
dao.insertOrIgnore(historyInfoList) dao.insertOrIgnore(historyInfoList)
} }
suspend fun deleteHistoryInfo(galleryInfo: BaseGalleryInfo) { suspend fun deleteHistoryInfo(galleryInfo: GalleryEntity) {
val dao = db.historyDao() val dao = db.historyDao()
dao.deleteByKey(galleryInfo.gid) dao.deleteByKey(galleryInfo.gid)
deleteGalleryInfo(galleryInfo) deleteGalleryInfo(galleryInfo)
@@ -273,23 +278,26 @@ object EhDB {
db.filterDao().update(filter) db.filterDao().update(filter)
} }
fun exportDB(context: Context, file: Path) { suspend fun exportDB(file: Path) {
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() } db.useWriterConnection { conn ->
val dbFile = context.getDatabasePath(DB_NAME) conn.execSQL("PRAGMA wal_checkpoint(FULL)")
dbFile.toOkioPath() sendTo file 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 tempDBName = "tmp.db"
val dbFile = getDatabasePath(tempDBName)
file sendTo dbFile
val oldDB = resource { val oldDB = resource {
context.deleteDatabase(tempDBName)
roomDb<EhDatabase>(tempDBName) { roomDb<EhDatabase>(tempDBName) {
createFromInputStream { context.contentResolver.openInputStream(uri) }
addMigrations(Schema17to18()) addMigrations(Schema17to18())
} }
} release { db -> } release { db ->
db.close() db.close()
context.deleteDatabase(tempDBName) dbFile.delete()
} }
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list()) db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
@@ -0,0 +1,56 @@
package com.hippo.ehviewer
import com.hippo.ehviewer.client.hosts
import com.hippo.ehviewer.client.hostsDsl
val builtInHosts = hostsDsl {
hosts("exhentai.org") {
"178.175.128.251" blockedInCN false
"178.175.128.252" blockedInCN false
"178.175.128.253" blockedInCN false
"178.175.128.254" blockedInCN false
"178.175.129.251" blockedInCN false
"178.175.129.252" blockedInCN false
"178.175.129.253" blockedInCN false
"178.175.129.254" blockedInCN false
"178.175.132.19" blockedInCN false
"178.175.132.20" blockedInCN false
"178.175.132.21" blockedInCN false
"178.175.132.22" blockedInCN false
}
hosts("api.github.com") {
"140.82.114.5" blockedInCN false
"140.82.114.6" blockedInCN false
"140.82.116.5" blockedInCN false
"140.82.116.6" blockedInCN false
"140.82.121.5" blockedInCN true
"140.82.121.6" blockedInCN true
"20.200.245.245" blockedInCN false
"20.201.28.148" blockedInCN false
"20.205.243.168" blockedInCN false
"20.26.156.210" blockedInCN false
"20.27.177.116" blockedInCN false
"20.87.245.6" blockedInCN false
"4.237.22.34" blockedInCN false
}
hosts("github.com") {
"140.82.113.4" blockedInCN false
"140.82.114.3" blockedInCN false
"140.82.116.4" blockedInCN false
"140.82.121.3" blockedInCN true
"140.82.121.4" blockedInCN true
"20.200.245.247" blockedInCN false
"20.201.28.151" blockedInCN false
"20.205.243.166" blockedInCN false
"20.26.156.215" blockedInCN false
"20.27.177.113" blockedInCN false
"20.87.245.0" blockedInCN false
"4.237.22.38" blockedInCN false
}
hosts("raw.githubusercontent.com") {
"185.199.108.133" blockedInCN false
"185.199.109.133" blockedInCN false
"185.199.110.133" blockedInCN false
"185.199.111.133" blockedInCN false
}
}
@@ -9,14 +9,13 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import com.ehviewer.core.network.EhCookieStore
import com.ehviewer.core.preferences.DataStorePreferences import com.ehviewer.core.preferences.DataStorePreferences
import com.ehviewer.core.preferences.PrefDelegate import com.ehviewer.core.preferences.PrefDelegate
import com.ehviewer.core.preferences.edit import com.ehviewer.core.preferences.edit
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.data.FavListUrlBuilder import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.download.DownloadsFilterMode import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.download.SortMode import com.hippo.ehviewer.download.SortMode
import com.hippo.ehviewer.util.AppConfig
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import java.util.Locale import java.util.Locale
@@ -82,6 +81,7 @@ object Settings : DataStorePreferences(null) {
val detailSize = intPref("detail_size_2", 0) val detailSize = intPref("detail_size_2", 0)
val thumbColumns = intPref("thumb_columns", 3) val thumbColumns = intPref("thumb_columns", 3)
val showGalleryPages = boolPref("show_gallery_pages", true) val showGalleryPages = boolPref("show_gallery_pages", true)
val showReadingProgress = boolPref("show_reading_progress", false)
val showVoteStatus = boolPref("show_vote_status", false) val showVoteStatus = boolPref("show_vote_status", false)
val showComments = boolPref("show_gallery_comments", true) val showComments = boolPref("show_gallery_comments", true)
val commentThreshold = intPref("comment_threshold", -100) val commentThreshold = intPref("comment_threshold", -100)
@@ -111,16 +111,16 @@ object Settings : DataStorePreferences(null) {
val saveCrashLog = boolPref("save_crash_log", true) val saveCrashLog = boolPref("save_crash_log", true)
val readCacheSize = intPref("read_cache_size_2", 640) val readCacheSize = intPref("read_cache_size_2", 640)
val enableCronet = boolPref("enable_cronet", true) val enableCronet = boolPref("enable_cronet", true)
val enableQuic = boolPref("enable_quic", true) val enableQuic = boolPref("enable_quic", false)
val hardwareBitmapThreshold = intPref("hardware_bitmap_threshold", 16384) val hardwareBitmapThreshold = intPref("hardware_bitmap_threshold", 16384)
val preloadThumbAggressively = boolPref("preload_thumb_aggressively", false) val preloadThumbAggressively = boolPref("preload_thumb_aggressively", false)
val animateItems = boolPref("animate_items", true) val animateItems = boolPref("animate_items", true)
val desktopSite = boolPref("desktop_site", true) val desktopSite = boolPref("desktop_site", true)
// About // About
val backupBeforeUpdate = boolPref("backup_before_update", true) val backupBeforeUpdate = boolPref("backup_before_update", false)
val useCIUpdateChannel = boolPref("ci_update_channel", AppConfig.isSnapshot) val useCIUpdateChannel = boolPref("ci_update_channel", BuildConfig.SNAPSHOT)
val updateIntervalDays = intPref("update_interval_days", 7) val updateIntervalDays = intPref("update_interval_days", 0)
// Misc // Misc
val languageFilter = intPref("language_filter", -1) val languageFilter = intPref("language_filter", -1)
@@ -158,8 +158,8 @@ object Settings : DataStorePreferences(null) {
val colorFilterMode = intPref("color_filter_mode", 0) val colorFilterMode = intPref("color_filter_mode", 0)
val customBrightness = boolPref("pref_custom_brightness_key", false) val customBrightness = boolPref("pref_custom_brightness_key", false)
val customBrightnessValue = intPref("custom_brightness_value", 0) val customBrightnessValue = intPref("custom_brightness_value", 0)
val readingMode = intPref("pref_default_reading_mode_key", ReadingModeType.LEFT_TO_RIGHT.flagValue) val readingMode = intPref("pref_default_reading_mode_key", ReadingModeType.DEFAULT.prefValue)
val orientationMode = intPref("pref_default_orientation_type_key", OrientationType.FREE.flagValue) val orientationMode = intPref("pref_default_orientation_type_key", OrientationType.DEFAULT.prefValue)
val showReaderSeekbar = boolPref("pref_show_reader_seekbar", true) val showReaderSeekbar = boolPref("pref_show_reader_seekbar", true)
val showPageNumber = boolPref("pref_show_page_number_key", true) val showPageNumber = boolPref("pref_show_page_number_key", true)
val readerTheme = intPref("pref_reader_theme_key", 1) val readerTheme = intPref("pref_reader_theme_key", 1)
@@ -169,7 +169,8 @@ object Settings : DataStorePreferences(null) {
val keepScreenOn = boolPref("pref_keep_screen_on_key", true) val keepScreenOn = boolPref("pref_keep_screen_on_key", true)
val readerLongTapAction = boolPref("reader_long_tap", true) val readerLongTapAction = boolPref("reader_long_tap", true)
val pageTransitions = boolPref("pref_enable_transitions_key", 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 grayScale = boolPref("pref_grayscale", false)
val invertedColors = boolPref("pref_inverted_colors", false) val invertedColors = boolPref("pref_inverted_colors", false)
val readerWebtoonNav = intPref("reader_navigation_mode_webtoon", 0) val readerWebtoonNav = intPref("reader_navigation_mode_webtoon", 0)
@@ -193,6 +194,10 @@ object Settings : DataStorePreferences(null) {
if (pref[downloadFilterMode] == DownloadsFilterMode.ARTIST.flag && pref[recentDownloadLabel] == null) { if (pref[downloadFilterMode] == DownloadsFilterMode.ARTIST.flag && pref[recentDownloadLabel] == null) {
pref[recentDownloadLabel] = "" pref[recentDownloadLabel] = ""
} }
val orientation = pref[orientationMode]
if (OrientationType.entries.none { it.prefValue == orientation }) {
pref.remove(orientationMode)
}
} }
} }
@@ -4,13 +4,13 @@ import android.app.UiModeManager
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import arrow.core.Either.Companion.catch import arrow.core.Either.Companion.catch
import com.ehviewer.core.preferences.PrefDelegate 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.EhEngine
import com.hippo.ehviewer.client.EhTagDatabase import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
import com.hippo.ehviewer.ui.keepNoMediaFileStatus import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.util.isAtLeastS
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -15,7 +15,7 @@
*/ */
package com.hippo.ehviewer.client 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) // 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/*** // 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()
}
@@ -22,14 +22,21 @@ import arrow.core.right
import arrow.fx.coroutines.parMap import arrow.fx.coroutines.parMap
import arrow.fx.coroutines.parZip import arrow.fx.coroutines.parZip
import com.ehviewer.core.i18n.R 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.EhDB
import com.hippo.ehviewer.Settings 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.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.CloudflareBypassException
import com.hippo.ehviewer.client.exception.EhError
import com.hippo.ehviewer.client.exception.EhException import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.InsufficientFundsException 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.NoHathClientException
import com.hippo.ehviewer.client.exception.NoHitsFoundException import com.hippo.ehviewer.client.exception.NoHitsFoundException
import com.hippo.ehviewer.client.exception.NotLoggedInException 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.GalleryApiParser
import com.hippo.ehviewer.client.parser.GalleryDetailParser import com.hippo.ehviewer.client.parser.GalleryDetailParser
import com.hippo.ehviewer.client.parser.GalleryListParser 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.GalleryPageParser
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
import com.hippo.ehviewer.client.parser.HomeParser 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.ReadableTime
import com.hippo.ehviewer.util.bodyAsUtf8Text import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess import com.hippo.ehviewer.util.ensureSuccess
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.HttpStatement import io.ktor.client.statement.HttpStatement
import io.ktor.client.statement.bodyAsChannel 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.DirectByteBufferPool
import io.ktor.utils.io.pool.useInstance import io.ktor.utils.io.pool.useInstance
import io.ktor.utils.io.readAvailable import io.ktor.utils.io.readAvailable
@@ -72,6 +82,8 @@ import kotlin.system.measureTimeMillis
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.json.add import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonArray import kotlinx.serialization.json.addJsonArray
import kotlinx.serialization.json.put 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 MAX_SEQUENTIAL_REQUESTS = 5
private const val REQUEST_INTERVAL = 5000L 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 dir = AppConfig.externalParseErrorDir ?: return
val file = File(dir, ReadableTime.getFilenamableTime() + ".txt") val file = File(dir, ReadableTime.getFilenamableTime() + ".txt")
file.sink().buffer().use { sink -> 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 // Don't translate coroutine cancellation
if (e is CancellationException) throw e if (e is CancellationException) throw e
@@ -110,36 +124,40 @@ fun rethrowExactly(response: HttpResponse, body: Either<String, ByteBuffer>, e:
{ !it.hasRemaining() }, { !it.hasRemaining() },
) )
if (empty) { if (empty) {
if (EhUtils.isExHentai) { val message = if (response.request.url.host == EhUrl.DOMAIN_EX) {
throw EhException("Sad Panda\n(without panda)") "Sad Panda\n(without panda)"
} else { } else {
throw EhException("IP banned") appCtx.getString(R.string.error_ip_banned)
} }
throw IpBannedException(message)
} }
// Check Gallery Not Available // Parse error from rust
body.onLeft { val error = body.getOrNull()?.runCatching {
if ("Gallery Not Available - " in it) throw e val array = moveToByteArray()
} rewind()
Cbor.decodeFromByteArray<EhError>(array)
}?.getOrNull()
// Check parse error thrown by rust // Check Gallery Not Available - 404
val cause = e.cause if (error is EhError.GalleryUnavailable) throw EhException(error.message)
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 bad response code // Check bad response code
response.status.ensureSuccess() 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) { 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) if (Settings.saveParseErrorBody.value) body.saveParseError(e)
throw EhException(appCtx.getString(R.string.error_parse_error), 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 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() val body = response.bodyAsUtf8Text()
runSuspendCatching { runSuspendCatching {
block(body) block(body)
@@ -159,7 +178,8 @@ suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend
}.getOrThrow() }.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 -> httpContentPool.useInstance { buffer ->
with(response.bodyAsChannel()) { while (!isClosedForRead) readAvailable(buffer) } with(response.bodyAsChannel()) { while (!isClosedForRead) readAvailable(buffer) }
buffer.flip() buffer.flip()
@@ -174,10 +194,17 @@ suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: s
object EhEngine { object EhEngine {
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response -> suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
val location = response.headers["Location"] ?: throw InsufficientFundsException() val location = response.headers["Location"] ?: run {
location.takeIf { "bounce_login" !in it } ?: throw NotLoggedInException() 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 { suspend fun getTorrentList(gid: Long, token: String): TorrentResult {
val url = EhUrl.getTorrentUrl(gid, token) val url = EhUrl.getTorrentUrl(gid, token)
val referer = EhUrl.getGalleryDetailUrl(gid, token) val referer = EhUrl.getGalleryDetailUrl(gid, token)
@@ -1,10 +1,10 @@
package com.hippo.ehviewer.client package com.hippo.ehviewer.client
import arrow.core.memoize 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.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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -15,11 +15,11 @@
*/ */
package com.hippo.ehviewer.client package com.hippo.ehviewer.client
import com.ehviewer.core.util.logcat
import com.hippo.ehviewer.EhApplication.Companion.ktorClient import com.hippo.ehviewer.EhApplication.Companion.ktorClient
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
import com.hippo.ehviewer.util.bodyAsUtf8Text import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess import com.hippo.ehviewer.util.ensureSuccess
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.FormDataContent import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.header 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.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import arrow.core.Either 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.EhApplication.Companion.ktorClient
import com.hippo.ehviewer.R import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.bodyAsUtf8Text import com.hippo.ehviewer.util.bodyAsUtf8Text
import com.hippo.ehviewer.util.ensureSuccess import com.hippo.ehviewer.util.ensureSuccess
import com.hippo.ehviewer.util.utf8 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 io.ktor.client.request.prepareGet
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
@@ -53,12 +53,7 @@ import splitties.init.appCtx
object EhTagDatabase : CoroutineScope { object EhTagDatabase : CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job() override val coroutineContext = Dispatchers.IO + Job()
private const val NAMESPACE_PREFIX = "n" private const val NAMESPACE_PREFIX = "n"
private val PREFIXES = arrayOf( private val PREFIXES = ["-", "~", "tag:", "weak:"]
"-",
"~",
"tag:",
"weak:",
)
private lateinit var tagGroups: Map<String, Map<String, String>> private lateinit var tagGroups: Map<String, Map<String, String>>
private val updateLock = Mutex() private val updateLock = Mutex()
var initialized by mutableStateOf(false) var initialized by mutableStateOf(false)
@@ -107,14 +107,14 @@ object EhUrl {
else -> URL_IMAGE_SEARCH_E else -> URL_IMAGE_SEARCH_E
} }
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) = ehUrl(listOf("g", "$gid", token)) { fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) = ehUrl(["g", "$gid", token]) {
if (index != 0) addQueryParameter("p", "$index") if (index != 0) addQueryParameter("p", "$index")
if (allComment) addQueryParameter("hc", "1") if (allComment) addQueryParameter("hc", "1")
}.buildString() }.buildString()
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", "$gid", token)).buildString() fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(["mpv", "$gid", token]).buildString()
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(listOf("s", pToken, "$gid-${index + 1}")) { fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(["s", pToken, "$gid-${index + 1}"]) {
addQueryParameterIfNotBlank("nl", nl) addQueryParameterIfNotBlank("nl", nl)
}.buildString() }.buildString()
@@ -143,7 +143,7 @@ object EhUrl {
} }
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl( inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
path = if (path.isNullOrBlank()) emptyList() else listOf(path), path = if (path.isNullOrBlank()) [] else [path],
host = host, host = host,
parameters = Parameters.build(builder), parameters = Parameters.build(builder),
) )
@@ -20,16 +20,21 @@ import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Environment import android.os.Environment
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.net.toUri import androidx.core.net.toUri
import arrow.core.memoize 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.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.client.parser.Archive
import com.hippo.ehviewer.spider.SpiderDen import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.AppConfig
@@ -38,7 +43,7 @@ import com.hippo.ehviewer.util.addTextToClipboard
import com.materialkolor.hct.Hct import com.materialkolor.hct.Hct
import com.materialkolor.ktx.from import com.materialkolor.ktx.from
import com.materialkolor.ktx.toColor import com.materialkolor.ktx.toColor
import eu.kanade.tachiyomi.util.lang.withUIContext import com.materialkolor.utils.ColorUtils.lstarFromArgb
import splitties.systemservices.downloadManager import splitties.systemservices.downloadManager
object EhUtils { 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) 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( data class FavListUrlBuilder(
val favCat: Int = FAV_CAT_ALL, val favCat: Int = FAV_CAT_ALL,
val keyword: String? = null, val keyword: String? = null,
var jumpTo: String? = null, private var jumpTo: String? = null,
var prev: String? = null, private var prev: String? = null,
var next: String? = null, private var next: String? = null,
) { ) {
val isLocal val isLocal
get() = favCat == FAV_CAT_LOCAL get() = favCat == FAV_CAT_LOCAL
@@ -35,6 +35,7 @@ data class FavListUrlBuilder(
fun setIndex(index: String?, isNext: Boolean) { fun setIndex(index: String?, isNext: Boolean) {
next = index.takeIf { isNext } next = index.takeIf { isNext }
prev = index.takeUnless { isNext } prev = index.takeUnless { isNext }
jumpTo = null
} }
fun build() = ehUrl(EhUrl.FAV_PATH) { fun build() = ehUrl(EhUrl.FAV_PATH) {
@@ -16,10 +16,13 @@
package com.hippo.ehviewer.client.data package com.hippo.ehviewer.client.data
import androidx.compose.ui.util.fastFlatMap 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.EhDB
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhFilter import com.hippo.ehviewer.client.EhFilter
import kotlinx.serialization.Serializable
private val LANGUAGES = arrayOf( private val LANGUAGES = arrayOf(
"English", "English",
@@ -38,45 +41,27 @@ private val LANGUAGES = arrayOf(
"Dutch", "Dutch",
) )
@Serializable suspend fun GalleryDetail.fillInfo() {
data class GalleryDetail( val index = LANGUAGES.indexOf(language)
val galleryInfo: BaseGalleryInfo = BaseGalleryInfo(), if (index != -1) simpleLanguage = GalleryInfo.S_LANGS[index]
var apiUid: Long = -1L, simpleTags = tagGroups.fastFlatMap(GalleryTagGroup::tags).map { (text, power, _) ->
var apiKey: String? = null, if (power == PowerStatus.Weak) "_$text" else text
var torrentCount: Int = 0,
var parent: String? = null,
var newerVersions: List<BaseGalleryInfo> = emptyList(),
var visible: String? = null,
var language: String? = null,
var size: String? = null,
var favoriteCount: Int = 0,
var ratingCount: Int = 0,
val tagGroups: List<GalleryTagGroup>,
var comments: GalleryCommentList,
val previewList: List<GalleryPreview>,
) : GalleryInfo by galleryInfo {
suspend fun fillInfo() {
val index = LANGUAGES.indexOf(language)
if (index != -1) simpleLanguage = GalleryInfo.S_LANGS[index]
simpleTags = tagGroups.fastFlatMap(GalleryTagGroup::tags).map { (text, power, _) ->
if (power == PowerStatus.Weak) "_$text" else text
}
if (favoriteSlot == GalleryInfo.NOT_FAVORITED && EhDB.containLocalFavorites(gid)) {
favoriteSlot = GalleryInfo.LOCAL_FAVORITED
}
} }
if (favoriteSlot == GalleryInfo.NOT_FAVORITED && EhDB.containLocalFavorites(gid)) {
favoriteSlot = GalleryInfo.LOCAL_FAVORITED
}
}
suspend fun filterComments() { suspend fun GalleryDetail.filterComments() {
comments = with(comments) { comments = with(comments) {
val scoreThreshold = Settings.commentThreshold.value val scoreThreshold = Settings.commentThreshold.value
copy( copy(
comments = comments.filter { comments = comments.filter {
it.uploader || it.uploader ||
it.score > scoreThreshold && it.score > scoreThreshold &&
!EhFilter.filterCommenter(it.user.orEmpty()) && !EhFilter.filterCommenter(it.user.orEmpty()) &&
!EhFilter.filterComment(it.comment) !EhFilter.filterComment(it.comment)
}, },
) )
}
} }
} }
@@ -15,28 +15,27 @@
*/ */
package com.hippo.ehviewer.client.data 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.Settings
import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.addQueryParameter import com.hippo.ehviewer.client.addQueryParameter
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
import com.hippo.ehviewer.client.ehUrl import com.hippo.ehviewer.client.ehUrl
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.ui.main.AdvanceTable import com.hippo.ehviewer.ui.main.AdvanceTable
import io.ktor.http.Parameters import io.ktor.http.Parameters
import kotlin.text.toIntOrNull
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ListUrlBuilder( data class ListUrlBuilder(
var mode: Int = MODE_NORMAL, var mode: Int = MODE_NORMAL,
private var prev: String? = null, private var prev: String? = null,
var next: String? = null, private var next: String? = null,
// Reset to null after initial loading private var jumpTo: String? = null,
var jumpTo: String? = null,
private var range: Int = 0, private var range: Int = 0,
var category: Int = EhUtils.NONE, var category: Int = EhUtils.NONE,
private var mKeyword: String? = null, var keyword: String? = null,
var hash: String? = null, var hash: String? = null,
var language: Int = -1, var language: Int = -1,
var advanceSearch: Int = -1, var advanceSearch: Int = -1,
@@ -49,11 +48,14 @@ data class ListUrlBuilder(
next = index.takeIf { isNext } next = index.takeIf { isNext }
prev = index.takeUnless { isNext } prev = index.takeUnless { isNext }
range = 0 range = 0
jumpTo = null
} }
fun setJumpTo(to: Int) { var page: Int
jumpTo = to.takeUnless { it == 0 }?.toString() get() = (jumpTo?.toInt() ?: 0) + 1
} set(value) {
jumpTo = (value - 1).takeUnless { it == 0 }?.toString()
}
fun setRange(to: Int) { fun setRange(to: Int) {
range = to range = to
@@ -62,16 +64,15 @@ data class ListUrlBuilder(
jumpTo = null jumpTo = null
} }
var keyword: String? fun setSeek(date: String) {
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword setIndex("2", true)
set(keyword) { jumpTo = date
mKeyword = keyword }
}
constructor(q: QuickSearch) : this( constructor(q: QuickSearch) : this(
mode = q.mode, mode = q.mode,
category = q.category, category = q.category,
mKeyword = q.keyword, keyword = q.keyword,
advanceSearch = q.advanceSearch, advanceSearch = q.advanceSearch,
minRating = q.minRating, minRating = q.minRating,
pageFrom = q.pageFrom, pageFrom = q.pageFrom,
@@ -83,7 +84,7 @@ data class ListUrlBuilder(
name = name, name = name,
mode = mode, mode = mode,
category = category, category = category,
keyword = mKeyword, keyword = keyword,
advanceSearch = advanceSearch, advanceSearch = advanceSearch,
minRating = minRating, minRating = minRating,
pageFrom = pageFrom, pageFrom = pageFrom,
@@ -100,7 +101,7 @@ data class ListUrlBuilder(
if (q.category != this.category) { if (q.category != this.category) {
return false return false
} }
if (q.keyword != mKeyword) { if (q.keyword != keyword) {
return false return false
} }
if (q.advanceSearch != advanceSearch) { if (q.advanceSearch != advanceSearch) {
@@ -122,7 +123,7 @@ data class ListUrlBuilder(
jumpTo = params["seek"], jumpTo = params["seek"],
range = params["range"]?.toIntOrNull() ?: 0, range = params["range"]?.toIntOrNull() ?: 0,
category = params["f_cats"]?.toIntOrNull()?.let(EhUtils::invCategory) ?: EhUtils.NONE, category = params["f_cats"]?.toIntOrNull()?.let(EhUtils::invCategory) ?: EhUtils.NONE,
mKeyword = params["f_search"], keyword = params["f_search"],
) { ) {
if (params["advsearch"] == "1") { if (params["advsearch"] == "1") {
advanceSearch = 0 advanceSearch = 0
@@ -153,7 +154,7 @@ data class ListUrlBuilder(
if (category > 0) { if (category > 0) {
addQueryParameter("f_cats", "${EhUtils.invCategory(category)}") addQueryParameter("f_cats", "${EhUtils.invCategory(category)}")
} }
val query = mKeyword?.let { keyword -> val query = keyword?.let { keyword ->
if (language == -1 || "gid:" in keyword || "l:" in keyword || "language:" in keyword) { if (language == -1 || "gid:" in keyword || "l:" in keyword || "language:" in keyword) {
keyword keyword
} else { } else {
@@ -205,26 +206,23 @@ data class ListUrlBuilder(
} }
} }
}.buildString() }.buildString()
MODE_UPLOADER, MODE_TAG -> { MODE_UPLOADER, MODE_TAG -> {
val path = if (mode == MODE_UPLOADER) "uploader" else "tag" val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
ehUrl(listOf(path, requireNotNull(mKeyword))) { ehUrl([path, requireNotNull(keyword)]) {
addQueryParameterIfNotBlank("prev", prev) addQueryParameterIfNotBlank("prev", prev)
addQueryParameterIfNotBlank("next", next) addQueryParameterIfNotBlank("next", next)
addQueryParameterIfNotBlank("seek", jumpTo) addQueryParameterIfNotBlank("seek", jumpTo)
addQueryParameterIfNotBlank("range", range.takeIf { it > 0 }?.toString()) addQueryParameterIfNotBlank("range", range.takeIf { it > 0 }?.toString())
}.buildString() }.buildString()
} }
MODE_WHATS_HOT -> EhUrl.popularUrl MODE_WHATS_HOT -> EhUrl.popularUrl
MODE_IMAGE_SEARCH -> ehUrl { MODE_IMAGE_SEARCH -> ehUrl {
addQueryParameter("f_shash", requireNotNull(hash)) addQueryParameter("f_shash", requireNotNull(hash))
}.buildString() }.buildString()
MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) { MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) {
addQueryParameter("tl", requireNotNull(mKeyword)) addQueryParameter("tl", requireNotNull(keyword))
addQueryParameterIfNotBlank("p", jumpTo) addQueryParameterIfNotBlank("p", jumpTo)
}.buildString() }.buildString()
else -> throw IllegalStateException("Unexpected value: $mode") else -> throw IllegalStateException("Unexpected value: $mode")
} }
@@ -0,0 +1,48 @@
package com.hippo.ehviewer.client.exception
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.CborArray
@Serializable
sealed interface EhError {
@CborArray
@Serializable
@SerialName("NoHits")
data object NoHits : EhError
@CborArray
@Serializable
@SerialName("NoWatched")
data object NoWatched : EhError
@CborArray
@Serializable
@SerialName("NeedLogin")
data object NeedLogin : EhError
@CborArray
@Serializable
@SerialName("NoHathClient")
data object NoHathClient : EhError
@CborArray
@Serializable
@SerialName("InsufficientFunds")
data object InsufficientFunds : EhError
@CborArray
@Serializable
@SerialName("GalleryUnavailable")
data class GalleryUnavailable(val message: String) : EhError
@CborArray
@Serializable
@SerialName("IpBanned")
data class IpBanned(val message: String) : EhError
@CborArray
@Serializable
@SerialName("Error")
data class Error(val message: String) : EhError
}
@@ -0,0 +1,13 @@
package com.hippo.ehviewer.client.exception
import androidx.annotation.StringRes
import com.ehviewer.core.i18n.R
sealed class FatalException : EhException {
constructor(message: String) : super(message)
constructor(@StringRes messageId: Int) : super(messageId)
}
class IpBannedException(message: String) : FatalException(message)
class InsufficientGpException : FatalException(R.string.insufficient_gp)
class QuotaExceededException : FatalException(R.string.error_509)
@@ -1,5 +0,0 @@
package com.hippo.ehviewer.client.exception
import com.ehviewer.core.i18n.R
class QuotaExceededException : EhException(R.string.error_509)
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.client.parser package com.hippo.ehviewer.client.parser
import com.ehviewer.core.util.unescapeXml
import com.hippo.ehviewer.client.exception.ParseException import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import moe.tarsin.coroutines.runSuspendCatching import moe.tarsin.coroutines.runSuspendCatching
@@ -15,11 +15,11 @@
*/ */
package com.hippo.ehviewer.client.parser 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.EhUtils.getCategory
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.getThumbKey import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.client.parseAs import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.unescapeXml
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -17,9 +17,9 @@ package com.hippo.ehviewer.client.parser
import arrow.core.Either import arrow.core.Either
import arrow.core.getOrElse import arrow.core.getOrElse
import com.hippo.ehviewer.client.data.GalleryCommentList import com.ehviewer.core.model.GalleryCommentList
import com.hippo.ehviewer.client.data.GalleryDetail import com.ehviewer.core.model.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryPreviewList import com.ehviewer.core.model.GalleryPreviewList
import com.hippo.ehviewer.client.exception.ParseException import com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -1,6 +1,6 @@
package com.hippo.ehviewer.client.parser 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 com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -13,14 +13,14 @@ object GalleryListParser {
throw ParseException("Can't parse gallery list", it) throw ParseException("Can't parse gallery list", it)
} }
val emptyResult = GalleryListResult(null, null, arrayListOf()) val emptyResult = GalleryListResult(null, null, [])
} }
@Serializable @Serializable
data class GalleryListResult( data class GalleryListResult(
val prev: String?, val prev: String?,
val next: String?, val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>, val galleryInfoList: MutableList<BaseGalleryInfo>,
) )
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
@@ -26,7 +26,7 @@ object GalleryListUrlParser {
fun parse(urlStr: String) = Either.catch { fun parse(urlStr: String) = Either.catch {
val url = Url(urlStr) val url = Url(urlStr)
if (url.host != EhUrl.DOMAIN_E && url.host != EhUrl.DOMAIN_EX) { if (url.host != EhUrl.DOMAIN_E && url.host != EhUrl.DOMAIN_EX) {
return null return@catch null
} }
val segments = url.segments val segments = url.segments
if (segments.isEmpty()) { if (segments.isEmpty()) {
@@ -42,20 +42,18 @@ object GalleryListUrlParser {
mode = if (head == "uploader") ListUrlBuilder.MODE_UPLOADER else ListUrlBuilder.MODE_TAG mode = if (head == "uploader") ListUrlBuilder.MODE_UPLOADER else ListUrlBuilder.MODE_TAG
keyword = segments[1].decodeURLQueryComponent(plusIsSpace = true) keyword = segments[1].decodeURLQueryComponent(plusIsSpace = true)
} }
"toplist.php" -> { "toplist.php" -> {
val tl = url.parameters["tl"] val tl = url.parameters["tl"]
if (tl != null && tl in arrayOf("11", "12", "13", "15")) { if (tl != null && tl in arrayOf("11", "12", "13", "15")) {
ListUrlBuilder( ListUrlBuilder(
mode = ListUrlBuilder.MODE_TOPLIST, mode = ListUrlBuilder.MODE_TOPLIST,
jumpTo = url.parameters["p"], jumpTo = url.parameters["p"],
mKeyword = tl, keyword = tl,
) )
} else { } else {
null null
} }
} }
else -> { else -> {
val category = EhUtils.getCategory(head) val category = EhUtils.getCategory(head)
if (category != EhUtils.UNKNOWN) { if (category != EhUtils.UNKNOWN) {
@@ -15,8 +15,8 @@
*/ */
package com.hippo.ehviewer.client.parser package com.hippo.ehviewer.client.parser
import com.ehviewer.core.util.unescapeXml
import com.hippo.ehviewer.client.exception.ParseException import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml
object GalleryPageParser { object GalleryPageParser {
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style") private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
@@ -15,9 +15,9 @@
*/ */
package com.hippo.ehviewer.client.parser package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.util.toEpochMillis import com.ehviewer.core.util.toEpochMillis
import com.hippo.ehviewer.util.toIntOrDefault import com.ehviewer.core.util.toIntOrDefault
import com.hippo.ehviewer.util.toLocalDateTime import com.ehviewer.core.util.toLocalDateTime
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format.char import kotlinx.datetime.format.char
@@ -19,7 +19,7 @@ package com.hippo.ehviewer.client.parser
import arrow.core.Either.Companion.catch import arrow.core.Either.Companion.catch
import arrow.core.getOrElse 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 com.hippo.ehviewer.client.exception.ParseException
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -9,7 +9,7 @@ import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.SystemClock import android.os.SystemClock
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
import eu.kanade.tachiyomi.util.system.logcat import com.ehviewer.core.util.logcat
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -12,9 +12,9 @@ import coil3.asImage
import coil3.intercept.Interceptor import coil3.intercept.Interceptor
import coil3.request.ImageResult import coil3.request.ImageResult
import coil3.request.SuccessResult 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.image.copyBitmapToAHB
import com.hippo.ehviewer.util.isAtLeastQ
import eu.kanade.tachiyomi.util.system.logcat
import moe.tarsin.coroutines.runSuspendCatching import moe.tarsin.coroutines.runSuspendCatching
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@@ -6,16 +6,16 @@ import coil3.intercept.Interceptor
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.ImageResult import coil3.request.ImageResult
import coil3.request.SuccessResult 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.EhDB
import com.hippo.ehviewer.client.getThumbKey import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.downloadLocation 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) private val downloadInfoKey = Extras.Key<DownloadInfo?>(default = null)
@@ -35,7 +35,7 @@ object DownloadThumbInterceptor : Interceptor {
info.thumbKey = thumbKey info.thumbKey = thumbKey
EhDB.putGalleryInfo(info.galleryInfo) EhDB.putGalleryInfo(info.galleryInfo)
} }
val dir = downloadLocation / info.dirname val dir = downloadLocation / info.dirname!!
val format = thumbKey.substringAfterLast('.', "") val format = thumbKey.substringAfterLast('.', "")
check(format.isNotBlank()) check(format.isNotBlank())
val thumb = dir / "thumb.$format" val thumb = dir / "thumb.$format"
@@ -53,7 +53,7 @@ object DownloadThumbInterceptor : Interceptor {
// Accessing the recreated file immediately after deleting it throws // Accessing the recreated file immediately after deleting it throws
// FileNotFoundException, so we just overwrite the existing file. // FileNotFoundException, so we just overwrite the existing file.
val key = requireNotNull(chain.request.memoryCacheKey) val key = requireNotNull(chain.request.memoryCacheKey)
imageCache.read(key) { thumbCache.read(key) {
data sendTo thumb data sendTo thumb
} }
if (thumb != v1Thumb) v1Thumb.delete() if (thumb != v1Thumb) v1Thumb.delete()
@@ -7,10 +7,13 @@ import coil3.decode.BlackholeDecoder
import coil3.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.size.SizeResolver import coil3.size.SizeResolver
import com.hippo.ehviewer.client.data.GalleryInfo import com.ehviewer.core.database.model.DownloadInfo
import com.hippo.ehviewer.client.data.GalleryPreview 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.client.thumbUrl
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ktbuilder.execute 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 // Load in original size so the memory cache can be reused for preload requests
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply { fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
with(preview) { with(preview) {
val key = imageKey val key = if (this is V2GalleryPreview) getV2PreviewKey(url) else getThumbKey(url)
data(url) data(url)
size(SizeResolver.ORIGINAL) size(SizeResolver.ORIGINAL)
memoryCacheKey(key) memoryCacheKey(key)
@@ -19,11 +19,11 @@ import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters 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.R
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine 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.time.Duration
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Clock import kotlin.time.Clock
@@ -22,18 +22,29 @@ import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.util.size import androidx.core.util.size
import arrow.fx.coroutines.parMapNotNull 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.preferences.edit
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.mapNotNull
import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo import com.hippo.ehviewer.client.exception.FatalException
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.spider.COMIC_INFO_FILE import com.hippo.ehviewer.spider.COMIC_INFO_FILE
import com.hippo.ehviewer.spider.SpeedTracker import com.hippo.ehviewer.spider.SpeedTracker
import com.hippo.ehviewer.spider.SpiderQueen import com.hippo.ehviewer.spider.SpiderQueen
@@ -45,13 +56,7 @@ import com.hippo.ehviewer.spider.readCompatFromPath
import com.hippo.ehviewer.spider.toSimpleTags import com.hippo.ehviewer.spider.toSimpleTags
import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.insertWith import com.hippo.ehviewer.util.insertWith
import com.hippo.ehviewer.util.mapNotNull
import com.hippo.ehviewer.util.runAssertingNotMainThread 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -70,9 +75,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job() override val coroutineContext = Dispatchers.IO + Job()
// All download info list // All download info list
private val allInfoList = runAssertingNotMainThread { private val allInfoList = runAssertingNotMainThread { EhDB.getAllDownloadInfo() as MutableList }
(EhDB.getAllDownloadInfo() as MutableList).apply { sortWith(comparator()) }
}
val downloadInfoList: List<DownloadInfo> val downloadInfoList: List<DownloadInfo>
get() = allInfoList get() = allInfoList
@@ -97,6 +100,21 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
private val mutex = Mutex() 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 { fun containLabel(label: String?): Boolean {
if (label == null) { if (label == null) {
return false return false
@@ -130,7 +148,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
mCurrentTask = info mCurrentTask = info
with(info) { with(info) {
archiveFile?.let { archiveFile?.let {
return onFinish(pages, pages, pages) return onFinish(pages, pages, pages, failed = false)
} }
state = DownloadInfo.STATE_DOWNLOAD state = DownloadInfo.STATE_DOWNLOAD
speed = -1 speed = -1
@@ -178,11 +196,13 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
} }
} else { } else {
// It is new download info // It is new download info
info = DownloadInfo(galleryInfo, galleryInfo.downloadDirname()) info = DownloadInfo(galleryInfo.asEntity(), galleryInfo.downloadDirname())
info.label = label info.label = label
info.state = DownloadInfo.STATE_WAIT info.state = DownloadInfo.STATE_WAIT
// Add to all download list and map // Add to all download list and map
allInfoList.insertWith(info, comparator()) sortMutex.withLock {
allInfoList.insertWith(info, sortMode.comparator())
}
mAllInfoMap[galleryInfo.gid] = info mAllInfoMap[galleryInfo.gid] = info
// Add to wait list // Add to wait list
@@ -197,7 +217,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
ensureDownload() ensureDownload()
// Add it to history // Add it to history
EhDB.putHistoryInfo(info.galleryInfo) EhDB.putHistoryInfo(info)
} }
} }
@@ -239,10 +259,12 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}.collectAsState(transform(info)).value }.collectAsState(transform(info)).value
suspend fun startAllDownload() { suspend fun startAllDownload() {
val updateList = allInfoList.filter { val updateList = sortMutex.withLock {
when (it.state) { allInfoList.filter {
DownloadInfo.STATE_NONE, DownloadInfo.STATE_FAILED -> true when (it.state) {
else -> false DownloadInfo.STATE_NONE, DownloadInfo.STATE_FAILED -> true
else -> false
}
} }
} }
if (updateList.isNotEmpty()) { if (updateList.isNotEmpty()) {
@@ -256,8 +278,8 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
} }
} }
suspend fun addDownload(downloadInfoList: List<DownloadInfo>) { suspend fun addDownload(downloadInfoList: List<DownloadInfo>) = sortMutex.withLock {
val comparator = comparator() val comparator = sortMode.comparator()
downloadInfoList.forEach { info -> downloadInfoList.forEach { info ->
if (containDownloadInfo(info.gid)) return@forEach if (containDownloadInfo(info.gid)) return@forEach
@@ -287,11 +309,13 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
} }
suspend fun restoreDownload(galleryInfo: BaseGalleryInfo, dirname: String) { suspend fun restoreDownload(galleryInfo: BaseGalleryInfo, dirname: String) {
val info = DownloadInfo(galleryInfo, dirname) val info = DownloadInfo(galleryInfo.asEntity(), dirname)
info.state = DownloadInfo.STATE_NONE info.state = DownloadInfo.STATE_NONE
// Add to all download list and map // Add to all download list and map
allInfoList.insertWith(info, comparator()) sortMutex.withLock {
allInfoList.insertWith(info, sortMode.comparator())
}
mAllInfoMap[galleryInfo.gid] = info mAllInfoMap[galleryInfo.gid] = info
// Save to // Save to
@@ -350,7 +374,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
EhDB.removeDownloadInfo(info) EhDB.removeDownloadInfo(info)
// Remove all list and map // Remove all list and map
allInfoList.remove(info) sortMutex.withLock {
allInfoList.remove(info)
}
mAllInfoMap.remove(info.gid) mAllInfoMap.remove(info.gid)
// Update listener // Update listener
@@ -373,7 +399,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
mAllInfoMap.remove(gid) mAllInfoMap.remove(gid)
} }
EhDB.removeDownloadInfo(list) EhDB.removeDownloadInfo(list)
allInfoList.removeAll(list.toSet()) sortMutex.withLock {
allInfoList.removeAll(list.toSet())
}
// Update listener // Update listener
list.forEach { mutableNotifyFlow.emit(it) } list.forEach { mutableNotifyFlow.emit(it) }
@@ -382,7 +410,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
ensureDownload() ensureDownload()
} }
fun sortDownloads(mode: SortMode) { suspend fun sortDownloads(mode: SortMode) = sortMutex.withLock {
allInfoList.sortWith(mode.comparator()) allInfoList.sortWith(mode.comparator())
} }
@@ -501,9 +529,11 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
val new = exist.copy(label = to) val new = exist.copy(label = to)
labelList.add(index, new) labelList.add(index, new)
EhDB.updateDownloadLabel(new) EhDB.updateDownloadLabel(new)
allInfoList.forEach { sortMutex.withLock {
if (it.label == from) { allInfoList.forEach {
it.label = to if (it.label == from) {
it.label = to
}
} }
} }
} }
@@ -519,21 +549,25 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
} }
removeAt(index) removeAt(index)
} }
allInfoList.forEach { sortMutex.withLock {
if (it.label == label) { allInfoList.forEach {
it.label = null if (it.label == label) {
it.label = null
}
} }
} }
} }
suspend fun readMetadataFromLocal() { suspend fun readMetadataFromLocal() {
val list = allInfoList.mapNotNull { val list = sortMutex.withLock {
val updateGallery = it.pages == 0 || it.simpleTags == null allInfoList.mapNotNull {
val updateArtist = it.artistInfoList.isEmpty() val updateGallery = it.pages == 0 || it.simpleTags == null
if (updateGallery || updateArtist) { val updateArtist = it.artistInfoList.isEmpty()
Triple(it, updateGallery, updateArtist) if (updateGallery || updateArtist) {
} else { Triple(it, updateGallery, updateArtist)
null } else {
null
}
} }
}.parMapNotNull(concurrency = 5) { (info, updateGallery, updateArtist) -> }.parMapNotNull(concurrency = 5) { (info, updateGallery, updateArtist) ->
info.downloadDir?.run { info.downloadDir?.run {
@@ -574,13 +608,14 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
} }
} }
val galleryInfoList = mutableListOf<BaseGalleryInfo>() val galleryInfoList = buildList {
list.forEach { (info, artists) -> list.forEach { (info, artists) ->
info?.let { info?.let {
galleryInfoList.add(it) add(it)
} }
artists?.let { (gid, updateList) -> artists?.let { (gid, updateList) ->
EhDB.putDownloadArtist(gid, updateList) EhDB.putDownloadArtist(gid, updateList)
}
} }
} }
if (galleryInfoList.isNotEmpty()) EhDB.updateGalleryInfo(galleryInfoList) 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 { launch {
mDownloadListener?.onGet509() mDownloadListener?.onFatal(error)
stopAllDownload() 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 { launch {
mSpeedReminder.onFinish() mSpeedReminder.onFinish()
mCurrentSpider?.let { spider -> mCurrentSpider?.let { spider ->
@@ -662,11 +697,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
info.downloaded = downloaded info.downloaded = downloaded
info.total = total info.total = total
info.legacy = total - finished info.legacy = total - finished
if (info.legacy == 0) { info.state = if (failed) DownloadInfo.STATE_FAILED else DownloadInfo.STATE_FINISH
info.state = DownloadInfo.STATE_FINISH
} else {
info.state = DownloadInfo.STATE_FAILED
}
EhDB.putDownloadInfo(info) EhDB.putDownloadInfo(info)
mDownloadListener?.onFinish(info) mDownloadListener?.onFinish(info)
mutableNotifyFlow.emit(info) mutableNotifyFlow.emit(info)
@@ -677,9 +708,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
interface DownloadListener { interface DownloadListener {
/** /**
* Get 509 error * Fatal error
*/ */
fun onGet509() fun onFatal(error: FatalException)
/** /**
* Start download * Start download
@@ -782,8 +813,6 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
} }
private const val TAG = "DownloadManager" private const val TAG = "DownloadManager"
private fun comparator() = SortMode.from(Settings.downloadSortMode.value).comparator()
} }
var downloadLocation: Path var downloadLocation: Path
@@ -31,16 +31,20 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.savedstate.serialization.decodeFromSavedState import androidx.savedstate.serialization.decodeFromSavedState
import androidx.savedstate.serialization.encodeToSavedState import androidx.savedstate.serialization.encodeToSavedState
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.i18n.R 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.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo import com.hippo.ehviewer.client.exception.FatalException
import com.hippo.ehviewer.dao.DownloadInfo 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.ui.MainActivity
import com.hippo.ehviewer.util.FileUtils import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.ReadableTime 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -48,8 +52,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class DownloadService : class DownloadService :
@@ -61,7 +63,7 @@ class DownloadService :
private val notifyManager by unsafeLazy { NotificationManagerCompat.from(this) } private val notifyManager by unsafeLazy { NotificationManagerCompat.from(this) }
private val downloadingNotification by unsafeLazy { initDownloadingNotification() } private val downloadingNotification by unsafeLazy { initDownloadingNotification() }
private val downloadedNotification by lazy { initDownloadedNotification() } 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" } private val channelId by unsafeLazy { "$packageName.download" }
override fun onCreate() { override fun onCreate() {
@@ -109,14 +111,12 @@ class DownloadService :
val label = intent.getStringExtra(KEY_LABEL) val label = intent.getStringExtra(KEY_LABEL)
deferredMgr.await().startDownload(decodeFromSavedState(gi), label) deferredMgr.await().startDownload(decodeFromSavedState(gi), label)
} }
ACTION_START_RANGE -> { ACTION_START_RANGE -> {
val gidList = intent.getLongArrayExtra(KEY_GID_LIST) val gidList = intent.getLongArrayExtra(KEY_GID_LIST)
if (gidList != null) { if (gidList != null) {
deferredMgr.await().startRangeDownload(gidList) deferredMgr.await().startRangeDownload(gidList)
} }
} }
ACTION_START_ALL -> deferredMgr.await().startAllDownload() ACTION_START_ALL -> deferredMgr.await().startAllDownload()
ACTION_STOP_ALL -> deferredMgr.await().stopAllDownload() ACTION_STOP_ALL -> deferredMgr.await().stopAllDownload()
ACTION_CLEAR -> clear() ACTION_CLEAR -> clear()
@@ -172,24 +172,27 @@ class DownloadService :
.apply { launch { run() } } .apply { launch { run() } }
} }
private fun init509Notification(): NotificationHandler { private fun initFatalNotification(): NotificationHandler {
val builder = NotificationCompat.Builder(applicationContext, channelId) val builder = NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(com.hippo.ehviewer.R.drawable.ic_baseline_warning_24) .setSmallIcon(com.hippo.ehviewer.R.drawable.ic_baseline_warning_24)
.setContentTitle(getString(R.string.stat_509_alert_title))
.setContentText(getString(R.string.stat_509_alert_text))
.setStyle(
NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)),
)
.setAutoCancel(true) .setAutoCancel(true)
.setOngoing(false) .setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR) .setCategory(NotificationCompat.CATEGORY_ERROR)
return NotificationHandler(this, notifyManager, builder, ID_509) return NotificationHandler(this, notifyManager, builder, ID_FATAL)
.apply { launch { run() } } .apply { launch { run() } }
} }
override fun onGet509() { override fun onFatal(error: FatalException) {
error509Notification.run { 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()) builder.setWhen(System.currentTimeMillis())
.setContentTitle(getString(title))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
show() show()
} }
} }
@@ -370,8 +373,8 @@ class DownloadService :
} }
suspend fun run() { suspend fun run() {
channel.receiveAsFlow().sample(DELAY).collect { while (true) {
when (it) { when (channel.receive()) {
Ops.Notify -> { Ops.Notify -> {
if (ActivityCompat.checkSelfPermission( if (ActivityCompat.checkSelfPermission(
service, service,
@@ -381,10 +384,10 @@ class DownloadService :
notifyManager.notify(id, builder.build()) notifyManager.notify(id, builder.build())
} }
} }
Ops.Cancel -> notifyManager.cancel(id) Ops.Cancel -> notifyManager.cancel(id)
Ops.StartForeground -> service.startForeground(id, builder.build()) Ops.StartForeground -> service.startForeground(id, builder.build())
} }
delay(DELAY)
} }
} }
@@ -412,7 +415,7 @@ class DownloadService :
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service" const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
private const val ID_DOWNLOADING = 1 private const val ID_DOWNLOADING = 1
private const val ID_DOWNLOADED = 2 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 const val DELAY = 1000L // 1s
private val sItemStateArray = LongSparseArray<Boolean>() private val sItemStateArray = LongSparseArray<Boolean>()
private val sItemTitleArray = LongSparseArray<String>() private val sItemTitleArray = LongSparseArray<String>()
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.download package com.hippo.ehviewer.download
import com.hippo.ehviewer.dao.DownloadInfo import com.ehviewer.core.database.model.DownloadInfo
import com.hippo.ehviewer.dao.artists import com.ehviewer.core.database.model.artists
enum class DownloadsFilterMode( enum class DownloadsFilterMode(
val flag: Int, val flag: Int,
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.download package com.hippo.ehviewer.download
import com.ehviewer.core.database.model.DownloadInfo
import com.hippo.ehviewer.client.EhUtils 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) { data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel: Boolean = false) {
val flag val flag
@@ -11,7 +11,6 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
GID(0), GID(0),
TITLE(1), TITLE(1),
TIME(2), TIME(2),
LABEL(3),
PAGES(4), PAGES(4),
} }
@@ -23,13 +22,16 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
fun comparator(): Comparator<DownloadInfo> { fun comparator(): Comparator<DownloadInfo> {
var comparator = when (field) { var comparator = when (field) {
Field.GID -> compareBy { it.gid } 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.TIME -> compareBy { it.time }
Field.PAGES -> compareBy<DownloadInfo> { it.pages }.thenBy { EhUtils.getSuitableTitle(it) } Field.PAGES -> compareBy<DownloadInfo> { it.pages }
} }
if (order == Order.DESC) { if (order == Order.DESC) {
comparator = comparator.reversed() comparator = comparator.reversed()
} }
if (field == Field.PAGES) {
comparator = comparator.thenByDescending { it.time }
}
if (groupByDownloadLabel) { if (groupByDownloadLabel) {
comparator = compareBy<DownloadInfo> { it.label }.then(comparator) comparator = compareBy<DownloadInfo> { it.label }.then(comparator)
} }
@@ -38,15 +40,16 @@ data class SortMode(val field: Field, val order: Order, val groupByDownloadLabel
companion object { companion object {
fun from(flag: Int): SortMode { fun from(flag: Int): SortMode {
val field = flag and 0xF shr 1 val fieldValue = flag and 0xF shr 1
val order = flag and 0x1 val orderValue = flag and 0x1
val groupByDownloadLabel = flag shr 4 == 1 val groupByDownloadLabel = flag shr 4 == 1
require(field in Field.entries.indices && order in Order.entries.indices) val field = Field.entries.find { it.value == fieldValue } ?: Default.field
return SortMode(Field.entries[field], Order.entries[order], groupByDownloadLabel) val order = Order.entries.find { it.value == orderValue } ?: Default.order
return SortMode(field, order, groupByDownloadLabel)
} }
val Default = SortMode(Field.TIME, Order.DESC) val Default = SortMode(Field.TIME, Order.DESC)
val All = listOf( val All = [
SortMode(Field.TIME, Order.DESC), Default,
SortMode(Field.TIME, Order.ASC), SortMode(Field.TIME, Order.ASC),
SortMode(Field.GID, Order.DESC), SortMode(Field.GID, Order.DESC),
SortMode(Field.GID, Order.ASC), 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.TITLE, Order.DESC),
SortMode(Field.PAGES, Order.ASC), SortMode(Field.PAGES, Order.ASC),
SortMode(Field.PAGES, Order.DESC), SortMode(Field.PAGES, Order.DESC),
) ]
} }
} }
@@ -16,7 +16,11 @@
package com.hippo.ehviewer.gallery package com.hippo.ehviewer.gallery
import arrow.autoCloseScope 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.Settings.archivePasswds
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.image.ImageSource import com.hippo.ehviewer.image.ImageSource
import com.hippo.ehviewer.image.byteBufferSource import com.hippo.ehviewer.image.byteBufferSource
import com.hippo.ehviewer.jni.closeArchive 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.jni.releaseByteBuffer
import com.hippo.ehviewer.util.FileUtils import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.displayName import com.hippo.ehviewer.util.displayName
import com.hippo.files.openFileDescriptor
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import moe.tarsin.kt.install import moe.tarsin.kt.install
import okio.Path import okio.Path
@@ -40,7 +42,7 @@ typealias PasswdProvider = suspend (PasswdInvalidator) -> String
suspend inline fun <T> useArchivePageLoader( suspend inline fun <T> useArchivePageLoader(
file: Path, file: Path,
gid: Long = 0, info: GalleryInfo? = null,
startPage: Int = 0, startPage: Int = 0,
hasAds: Boolean = false, hasAds: Boolean = false,
crossinline passwdProvider: PasswdProvider, crossinline passwdProvider: PasswdProvider,
@@ -49,7 +51,7 @@ suspend inline fun <T> useArchivePageLoader(
coroutineScope { coroutineScope {
val pfd = install(file.openFileDescriptor("r")) val pfd = install(file.openFileDescriptor("r"))
val size = install( 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() }, { _, _ -> closeArchive() },
) )
check(size > 0) { "Archive have no content!" } check(size > 0) { "Archive have no content!" }
@@ -57,8 +59,14 @@ suspend inline fun <T> useArchivePageLoader(
archivePasswds += passwdProvider(::providePassword) archivePasswds += passwdProvider(::providePassword)
} }
val loader = install( val loader = install(
object : PageLoader(this, gid, startPage, size, hasAds) { object : PageLoader(this, info, startPage, size, hasAds) {
override val title by lazy { FileUtils.getNameFromFilename(file.displayName)!! } override val title by lazy {
if (info != null) {
EhUtils.getSuitableTitle(info)
} else {
FileUtils.getNameFromFilename(file.displayName)!!
}
}
override fun getImageExtension(index: Int) = getExtension(index) override fun getImageExtension(index: Int) = getExtension(index)
@@ -1,8 +1,8 @@
package com.hippo.ehviewer.gallery package com.hippo.ehviewer.gallery
import arrow.autoCloseScope import arrow.autoCloseScope
import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.client.EhUtils 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
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
@@ -24,7 +24,7 @@ suspend inline fun <T> useEhPageLoader(
) )
queen.awaitReady() queen.awaitReady()
val loader = install( 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 val title by lazy { EhUtils.getSuitableTitle(info) }
override fun getImageExtension(index: Int) = queen.getExtension(index) override fun getImageExtension(index: Int) = queen.getExtension(index)
@@ -4,6 +4,9 @@ import androidx.collection.SieveCache
import androidx.collection.mutableIntObjectMapOf import androidx.collection.mutableIntObjectMapOf
import arrow.fx.coroutines.ExitCase import arrow.fx.coroutines.ExitCase
import arrow.fx.coroutines.bracketCase 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.EhDB
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.image.Image 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.OSUtils
import com.hippo.ehviewer.util.detectAds import com.hippo.ehviewer.util.detectAds
import com.hippo.ehviewer.util.displayString 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 java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.read 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 MAX_CACHE_SIZE = 512 * 1024 * 1024
private const val MIN_CACHE_SIZE = 256 * 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) var startPage = startPage.coerceIn(0, size - 1)
private val jobs = mutableIntObjectMapOf<Job>() private val jobs = mutableIntObjectMapOf<Job>()
@@ -111,14 +112,14 @@ abstract class PageLoader(val scope: CoroutineScope, val gid: Long, startPage: I
override fun close() { override fun close() {
lock.write { cache.evictAll() } lock.write { cache.evictAll() }
if (gid != 0L) { info?.run {
progressScope.launch { progressScope.launch {
EhDB.putReadProgress(gid, startPage) EhDB.putReadProgress(gid, startPage)
} }
} }
} }
protected abstract val title: String abstract val title: String
protected abstract fun getImageExtension(index: Int): String? protected abstract fun getImageExtension(index: Int): String?
@@ -32,10 +32,15 @@ import coil3.request.CachePolicy
import coil3.request.ErrorResult import coil3.request.ErrorResult
import coil3.request.SuccessResult import coil3.request.SuccessResult
import coil3.request.allowHardware import coil3.request.allowHardware
import coil3.size.Dimension import coil3.request.maxBitmapSize
import coil3.size.Precision import coil3.size.Precision
import coil3.size.Scale
import coil3.size.Size import coil3.size.Size
import coil3.size.SizeResolver import coil3.size.SizeResolver
import com.ehviewer.core.files.openFileDescriptor
import com.ehviewer.core.files.toUri
import com.ehviewer.core.util.isAtLeastP
import com.ehviewer.core.util.isAtLeastU
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.coil.AnimatedWebPDrawable import com.hippo.ehviewer.coil.AnimatedWebPDrawable
import com.hippo.ehviewer.coil.BitmapImageWithExtraInfo import com.hippo.ehviewer.coil.BitmapImageWithExtraInfo
@@ -48,10 +53,6 @@ import com.hippo.ehviewer.jni.munmap
import com.hippo.ehviewer.jni.rewriteGifSource import com.hippo.ehviewer.jni.rewriteGifSource
import com.hippo.ehviewer.ktbuilder.execute import com.hippo.ehviewer.ktbuilder.execute
import com.hippo.ehviewer.ktbuilder.imageRequest import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastU
import com.hippo.files.openFileDescriptor
import com.hippo.files.toUri
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.decrementAndFetch import kotlin.concurrent.atomics.decrementAndFetch
@@ -90,8 +91,10 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
} }
companion object { companion object {
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3 private val sizeResolver = with(appCtx.resources.displayMetrics) {
private val sizeResolver = SizeResolver(Size(targetWidth, Dimension.Undefined)) val targetSize = minOf(widthPixels, heightPixels) * 4 / 3
SizeResolver(Size(targetSize, targetSize))
}
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage { private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
val request = with(appCtx) { val request = with(appCtx) {
@@ -99,7 +102,9 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
onLeft { data(it.source) } onLeft { data(it.source) }
onRight { data(it.source.toUri()) } onRight { data(it.source.toUri()) }
size(sizeResolver) size(sizeResolver)
scale(Scale.FILL)
precision(Precision.INEXACT) precision(Precision.INEXACT)
maxBitmapSize(Size.ORIGINAL)
allowHardware(false) allowHardware(false)
hardwareThreshold(Settings.hardwareBitmapThreshold.value) hardwareThreshold(Settings.hardwareBitmapThreshold.value)
maybeCropBorder(Settings.cropBorder.value) maybeCropBorder(Settings.cropBorder.value)
@@ -130,7 +135,6 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
} }
src.right().decodeCoil(checkExtraneousAds) src.right().decodeCoil(checkExtraneousAds)
} }
is ByteBufferSource -> { is ByteBufferSource -> {
if (isAtLeastP && !isAtLeastU) { if (isAtLeastP && !isAtLeastU) {
rewriteGifSource(src.source) rewriteGifSource(src.source)
@@ -5,8 +5,8 @@ import coil3.ImageLoader
import coil3.disk.DiskCache import coil3.disk.DiskCache
import coil3.imageLoader import coil3.imageLoader
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.hippo.ehviewer.client.data.GalleryInfo import com.ehviewer.core.model.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview import com.ehviewer.core.model.GalleryPreview
import com.hippo.ehviewer.coil.ehPreview import com.hippo.ehviewer.coil.ehPreview
import com.hippo.ehviewer.coil.ehUrl import com.hippo.ehviewer.coil.ehUrl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -1,8 +1,8 @@
package com.hippo.ehviewer.ktor 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.Settings
import com.hippo.ehviewer.client.EhCookieStore
import io.ktor.client.HttpClientConfig import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngineConfig import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.plugins.HttpTimeout 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.HttpHeaders
import io.ktor.http.userAgent import io.ktor.http.userAgent
import io.ktor.util.appendIfNameAbsent 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" // It's safe to assume the WebView package will always be present as we require CookieManager anyway
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" 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 = "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" 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) { install(HttpTimeout) {
reset() reset()
requestTimeoutMillis = 10_000 requestTimeoutMillis = 30_000
} }
install(UserAgent) install(UserAgent)
defaultRequest { defaultRequest {
@@ -10,9 +10,6 @@ import android.net.http.UrlRequest
import android.net.http.UrlResponseInfo import android.net.http.UrlResponseInfo
import android.os.Build import android.os.Build
import androidx.annotation.RequiresExtension import androidx.annotation.RequiresExtension
import com.ehviewer.core.preferences.edit
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.util.restartApplication
import io.ktor.client.engine.HttpClientEngineBase import io.ktor.client.engine.HttpClientEngineBase
import io.ktor.client.engine.callContext import io.ktor.client.engine.callContext
import io.ktor.client.plugins.HttpTimeoutCapability import io.ktor.client.plugins.HttpTimeoutCapability
@@ -35,20 +32,21 @@ import java.nio.ByteBuffer
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.job import kotlinx.coroutines.job
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") { class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
// Limit thread to 1 since we are async & non-blocking // Limit thread to 1 since we are async & non-blocking
override val dispatcher = Dispatchers.Default.limitedParallelism(1) // TODO: Investigate https://github.com/FooIbar/EhViewer/issues/2854
// override val dispatcher = Dispatchers.Default.limitedParallelism(1)
override val supportedCapabilities = setOf(HttpTimeoutCapability) override val supportedCapabilities = setOf(HttpTimeoutCapability)
private val executor = dispatcher.asExecutor() private val executor = dispatcher.limitedParallelism(1).asExecutor()
private val pool = DirectByteBufferPool(32) private val pool = DirectByteBufferPool(32)
private val client by lazy { private val client by lazy {
with(config) { HttpEngine.Builder(context).apply(config).build() } with(config) { HttpEngine.Builder(context).apply(config).build() }
@@ -95,7 +93,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
} }
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) { override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
chunkChan.trySend(byteBuffer).getOrThrow() chunkChan.trySendBlocking(byteBuffer).getOrThrow()
} }
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) { override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
@@ -103,15 +101,6 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
} }
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) { override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) {
// Cronet may crash on some devices, fuck xiaomi
// https://github.com/FooIbar/EhViewer/issues/1826
val message = error.message.orEmpty()
if ("ERR_FILE_NOT_FOUND" in message || "ERR_PROXY_CONNECTION_FAILED" in message) {
Settings.edit(true) {
it[enableCronet] = false
}
with(config.context) { restartApplication() }
}
if (continuation.isActive) { if (continuation.isActive) {
continuation.resumeWithException(error) continuation.resumeWithException(error)
} else { } else {
@@ -1,12 +1,12 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.ktor package com.hippo.ehviewer.ktor
import android.net.http.HttpEngine import android.net.http.HttpEngine
import android.os.Build import android.os.Build
import androidx.annotation.RequiresExtension import androidx.annotation.RequiresExtension
import com.ehviewer.core.util.isAtLeastSExtension7
import java.io.File import java.io.File
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
fun CronetConfig.configureClient(enableQuic: Boolean) { fun CronetConfig.configureClient(enableQuic: Boolean) {
config = { config = {
setEnableBrotli(true) 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 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 io.ktor.client.engine.okhttp.OkHttpConfig
import okhttp3.AsyncDns import java.net.Proxy
import okhttp3.android.AndroidAsyncDns
fun OkHttpConfig.configureClient() { fun OkHttpConfig.configureClient() {
config { config {
if (isAtLeastQ) { dns(EhDns)
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6)) proxy(Proxy.NO_PROXY)
if (!Settings.enableQuic.value) {
install(EhSSLSocketFactory)
} }
} }
} }
@@ -1,7 +1,7 @@
package com.hippo.ehviewer.legacy package com.hippo.ehviewer.legacy
import com.ehviewer.core.files.read
import com.hippo.ehviewer.spider.SpiderInfo import com.hippo.ehviewer.spider.SpiderInfo
import com.hippo.files.read
import kotlinx.io.readLineStrict import kotlinx.io.readLineStrict
import okio.Path import okio.Path
@@ -1,24 +1,24 @@
package com.hippo.ehviewer.spider 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.EhUrl
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryTag
import com.hippo.ehviewer.client.data.PowerStatus
import com.hippo.ehviewer.client.data.SimpleTagsConverter
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.client.data.TagNamespace.Artist
import com.hippo.ehviewer.client.data.TagNamespace.Character
import com.hippo.ehviewer.client.data.TagNamespace.Cosplayer
import com.hippo.ehviewer.client.data.TagNamespace.Female
import com.hippo.ehviewer.client.data.TagNamespace.Group
import com.hippo.ehviewer.client.data.TagNamespace.Location
import com.hippo.ehviewer.client.data.TagNamespace.Male
import com.hippo.ehviewer.client.data.TagNamespace.Mixed
import com.hippo.ehviewer.client.data.TagNamespace.Other
import com.hippo.ehviewer.client.data.TagNamespace.Parody
import com.hippo.files.read
import com.hippo.files.write
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -43,7 +43,7 @@ private val xml = XML {
} }
xmlDeclMode = XmlDeclMode.Charset xmlDeclMode = XmlDeclMode.Charset
xmlVersion = XmlVersion.XML10 xmlVersion = XmlVersion.XML10
indent = 2 setIndent(2)
} }
fun GalleryInfo.getComicInfo(): ComicInfo { fun GalleryInfo.getComicInfo(): ComicInfo {
@@ -68,7 +68,6 @@ fun GalleryInfo.getComicInfo(): ComicInfo {
else -> Unit else -> Unit
} }
} }
else -> simpleTags?.forEach { tagString -> else -> simpleTags?.forEach { tagString ->
val (namespace, tag) = tagString.split(':', limit = 2) val (namespace, tag) = tagString.split(':', limit = 2)
.takeIf { it.size == 2 } ?: return@forEach // Ignore temp tags that don't have namespace .takeIf { it.size == 2 } ?: return@forEach // Ignore temp tags that don't have namespace
@@ -20,44 +20,45 @@ import arrow.fx.coroutines.autoCloseable
import arrow.fx.coroutines.closeable import arrow.fx.coroutines.closeable
import arrow.fx.coroutines.parMap import arrow.fx.coroutines.parMap
import arrow.fx.coroutines.resourceScope 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.EhDB
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle 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.ehRequest
import com.hippo.ehviewer.client.getImageKey import com.hippo.ehviewer.client.getImageKey
import com.hippo.ehviewer.coil.read import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.coil.suspendEdit import com.hippo.ehviewer.coil.suspendEdit
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.downloadLocation import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.download.tempDownloadDir import com.hippo.ehviewer.download.tempDownloadDir
import com.hippo.ehviewer.image.PathSource import com.hippo.ehviewer.image.PathSource
import com.hippo.ehviewer.jni.archiveFdBatch import com.hippo.ehviewer.jni.archiveFdBatch
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.util.FileUtils import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.copyTo import com.hippo.ehviewer.util.copyTo
import com.hippo.ehviewer.util.sendTo
import com.hippo.ehviewer.util.sha1 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.HttpResponse
import io.ktor.client.statement.bodyAsChannel import io.ktor.client.statement.bodyAsChannel
import io.ktor.client.statement.request import io.ktor.client.statement.request
import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read import kotlin.concurrent.read
import kotlin.concurrent.write import kotlin.concurrent.write
import kotlinx.coroutines.CancellationException
import okio.Path import okio.Path
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
class SpiderDen(val info: GalleryInfo) { class SpiderDen(val info: GalleryInfo) {
private val gid = info.gid private val gid = info.gid
@@ -129,11 +130,9 @@ class SpiderDen(val info: GalleryInfo) {
SpiderQueen.MODE_READ -> { SpiderQueen.MODE_READ -> {
containInCache(index) || containInDownloadDir(index) containInCache(index) || containInDownloadDir(index)
} }
SpiderQueen.MODE_DOWNLOAD -> { SpiderQueen.MODE_DOWNLOAD -> {
containInDownloadDir(index) || copyFromCacheToDownloadDir(index) containInDownloadDir(index) || copyFromCacheToDownloadDir(index)
} }
else -> { else -> {
false false
} }
@@ -154,6 +153,10 @@ class SpiderDen(val info: GalleryInfo) {
removeTempFile(index) removeTempFile(index)
} }
fun clearCache() {
repeat(info.pages, ::removeFromCache)
}
private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) { private fun Path.findDownloadFileForIndex(index: Int, extension: String) = with(lock) {
val name = perFilename(index, extension) val name = perFilename(index, extension)
read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } } read { fileCache[name] } ?: resolve(name).also { write { fileCache[name] = it } }
@@ -270,22 +273,18 @@ class SpiderDen(val info: GalleryInfo) {
} }
} }
suspend fun archive() { suspend fun archive() = saveAsCbz && downloadDir?.run {
if (saveAsCbz) { resolve(archiveName).let { file ->
downloadDir?.run { runCatching {
resolve(archiveName).let { file -> archiveTo(file)
runCatching { }.onFailure {
archiveTo(file) file.delete()
}.onFailure { logcat(it)
file.delete() }.isFailure
if (it is CancellationException) throw it
logcat(it)
}
}
}
} }
} } == true
// Postpone this to `SpiderQueen.stop` because files may still be in use by reader
suspend fun postArchive(): Boolean { suspend fun postArchive(): Boolean {
val dir = downloadDir val dir = downloadDir
val archived = saveAsCbz && dir?.find(archiveName) != null val archived = saveAsCbz && dir?.find(archiveName) != null
@@ -335,7 +334,7 @@ class SpiderDen(val info: GalleryInfo) {
downloadDir?.run { downloadDir?.run {
resolve(COMIC_INFO_FILE).also { resolve(COMIC_INFO_FILE).also {
if (info !is GalleryDetail && fetchMetadata) { if (info !is GalleryDetail && fetchMetadata) {
EhEngine.fillGalleryListByApi(listOf(info)) EhEngine.fillGalleryListByApi([info])
} }
info.getComicInfo().apply { info.getComicInfo().apply {
writeComicInfo(this, it) writeComicInfo(this, it)
@@ -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 const val TEMP_SUFFIX = ".tmp"
private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}") private val FileNameRegex = Regex("^\\d{8}\\.\\w{3,4}")
private val FileHashRegex = Regex("/h/([0-9a-f]{40})") private val FileHashRegex = Regex("/h/([0-9a-f]{40})")
@@ -1,12 +1,12 @@
package com.hippo.ehviewer.spider 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.edit
import com.hippo.ehviewer.coil.read import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.ktbuilder.diskCache import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.legacy.readLegacySpiderInfo 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.io.readByteArray
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor import kotlinx.serialization.cbor.Cbor
@@ -20,23 +20,20 @@ import androidx.annotation.IntDef
import arrow.core.Either import arrow.core.Either
import arrow.core.getOrElse import arrow.core.getOrElse
import arrow.core.partially1 import arrow.core.partially1
import com.ehviewer.core.files.find
import com.ehviewer.core.i18n.R 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.Settings
import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl 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.EhUrl.referer
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo import com.hippo.ehviewer.client.exception.FatalException
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.ehRequest
import com.hippo.ehviewer.client.exception.QuotaExceededException 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.ehviewer.util.displayString
import com.hippo.files.find
import eu.kanade.tachiyomi.util.system.logcat
import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.decrementAndFetch import kotlin.concurrent.atomics.decrementAndFetch
import kotlin.concurrent.atomics.incrementAndFetch 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) { 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) { synchronized(mSpiderListeners) {
mSpiderListeners.forEach { mSpiderListeners.forEach {
it.onFinish( it.onFinish(
mFinishedPages.load(), mFinishedPages.load(),
mDownloadedPages.load(), mDownloadedPages.load(),
pageStates.size, pageStates.size,
failed,
) )
} }
} }
@@ -216,7 +214,10 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private val prepareScope = CoroutineScope(coroutineContext + SupervisorJob()) private val prepareScope = CoroutineScope(coroutineContext + SupervisorJob())
private val prepareJob = prepareScope.async { doPrepare() } 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() { private suspend fun doPrepare() {
spiderDen.initDownloadDirIfExist() spiderDen.initDownloadDirIfExist()
@@ -331,7 +332,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
} }
private suspend fun readSpiderInfoFromInternet() = EhEngine.getPreviewList( private suspend fun readSpiderInfoFromInternet() = EhEngine.getPreviewList(
getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, 0, false), getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token),
).run { ).run {
val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token, total) val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token, total)
readPreviews(previews, 0, spiderInfo) readPreviews(previews, 0, spiderInfo)
@@ -342,17 +343,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
suspend fun getPTokenFromMultiPageViewer(index: Int): String? { suspend fun getPTokenFromMultiPageViewer(index: Int): String? {
if (!isMpvAvailable) return null if (!isMpvAvailable) return null
val url = getGalleryMultiPageViewerUrl(
galleryInfo.gid,
galleryInfo.token,
)
return runSuspendCatching { return runSuspendCatching {
ehRequest(url, referer).fetchUsingAsText { EhEngine.getPTokenListFromMpv(galleryInfo.gid, galleryInfo.token).forEachIndexed { index, s ->
GalleryMultiPageViewerPTokenParser.parse(this).forEachIndexed { index, s -> spiderInfo.pTokenMap[index] = s
spiderInfo.pTokenMap[index] = s
}
spiderInfo.pTokenMap[index]
} }
spiderInfo.pTokenMap[index]
}.getOrElse { }.getOrElse {
logcat(it) logcat(it)
null null
@@ -365,12 +360,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
} else { } else {
0 0
} }
val url = getGalleryDetailUrl( val url = getGalleryDetailUrl(galleryInfo.gid, galleryInfo.token, previewIndex)
galleryInfo.gid,
galleryInfo.token,
previewIndex,
false,
)
return runSuspendCatching { return runSuspendCatching {
EhEngine.getPreviewList(url).run { EhEngine.getPreviewList(url).run {
readPreviews(previews, previewIndex, spiderInfo) readPreviews(previews, previewIndex, spiderInfo)
@@ -414,8 +404,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
notifyPageSuccess(index) notifyPageSuccess(index)
} }
if (mDownloadedPages.load() == size) { if (mDownloadedPages.load() == size) {
if (mFinishedPages.load() == size) archiveJob.start() if (mFinishedPages.load() == size) {
notifyAllPageDownloaded() archiveJob.start()
} else {
notifyAllPageDownloaded(failed = true)
}
} }
} }
@@ -428,11 +421,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
annotation class State annotation class State
interface OnSpiderListener { interface OnSpiderListener {
fun onGetPages(pages: Int) {} fun onGetPages(pages: Int) {}
fun onGet509(index: Int) {} fun onFatal(error: FatalException) {}
fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {} fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {}
fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {} fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}
fun onPageFailure(index: Int, error: String?, 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) {} fun onPageReady(index: Int) {}
} }
@@ -555,7 +548,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
if (!force && index in spiderDen) { if (!force && index in spiderDen) {
return updatePageState(index, STATE_FINISHED) 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) previousPToken = getPToken(index - 1)
// The lock for delay should be acquired before anything else to maintain FIFO order // 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, previousPToken,
) )
}.getOrElse { }.getOrElse {
if (it is FatalException) throw it
forceHtml = true forceHtml = true
return@repeat return@repeat
}.let { }.let {
@@ -650,10 +644,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
} }
} }
}.onFailure { }.onFailure {
when (it) { if (it is FatalException) notifyFatal(it)
is QuotaExceededException -> notifyGet509(index)
// TODO: Check IP ban
}
error = it.displayString() error = it.displayString()
if (error == "Invalid page.") { if (error == "Invalid page.") {
pTokenLock.withLock { pTokenLock.withLock {
@@ -664,13 +655,13 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
updatePageState(index, STATE_FAILED, error) 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 Url509Regex = Regex("https://(?:ehgt\\.org/|exhentai\\.org/im)g/509s?\\.gif")
private val URL_509_PATTERN = Regex("\\.org/.+/509s?\\.gif")
private const val FORCE_RETRY = "Force retry" private const val FORCE_RETRY = "Force retry"
private const val WORKER_DEBUG_TAG = "SpiderQueenWorker" private const val WORKER_DEBUG_TAG = "SpiderQueenWorker"
private fun check509(url: String) { 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.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import 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.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.EhDB
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils 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.client.exception.EhException
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadService import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.downloadDir 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.destinations.ReaderScreenDestination
import com.hippo.ehviewer.ui.reader.ReaderScreenArgs import com.hippo.ehviewer.ui.reader.ReaderScreenArgs
import com.hippo.ehviewer.ui.tools.DialogState 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.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitResult import com.hippo.ehviewer.ui.tools.awaitResult
import com.hippo.ehviewer.ui.tools.awaitSelectDate 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.ui.tools.awaitSelectItemWithIconAndTextField
import com.hippo.ehviewer.util.FavouriteStatusRouter import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.bgWork 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.requestPermission
import com.hippo.ehviewer.util.restartApplication 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 com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlin.time.Clock import kotlin.time.Clock
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -177,7 +177,7 @@ suspend fun startDownload(forceDefault: Boolean, vararg galleryInfos: BaseGaller
} }
context(_: DialogState) context(_: DialogState)
suspend fun modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean { suspend fun modifyFavorites(galleryInfo: GalleryInfo): Boolean {
val localFavorited = EhDB.containLocalFavorites(galleryInfo.gid) val localFavorited = EhDB.containLocalFavorites(galleryInfo.gid)
if (Settings.hasSignedIn.value) { if (Settings.hasSignedIn.value) {
val isFavorited = galleryInfo.favoriteSlot != NOT_FAVORITED val isFavorited = galleryInfo.favoriteSlot != NOT_FAVORITED
@@ -215,7 +215,7 @@ suspend fun modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
} }
private suspend fun doModifyFavorites( private suspend fun doModifyFavorites(
galleryInfo: BaseGalleryInfo, galleryInfo: GalleryInfo,
slot: Int = NOT_FAVORITED, slot: Int = NOT_FAVORITED,
localFavorited: Boolean = true, localFavorited: Boolean = true,
note: String = "", note: String = "",
@@ -233,7 +233,6 @@ private suspend fun doModifyFavorites(
} }
false false
} }
LOCAL_FAVORITED -> { LOCAL_FAVORITED -> {
if (localFavorited) { if (localFavorited) {
EhDB.removeLocalFavorites(galleryInfo) EhDB.removeLocalFavorites(galleryInfo)
@@ -248,7 +247,6 @@ private suspend fun doModifyFavorites(
} }
!localFavorited !localFavorited
} }
in 0..9 -> { in 0..9 -> {
EhEngine.modifyFavorites(gid, token, slot, note) EhEngine.modifyFavorites(gid, token, slot, note)
favoriteSlot = slot favoriteSlot = slot
@@ -256,14 +254,13 @@ private suspend fun doModifyFavorites(
favoriteNote = note favoriteNote = note
true true
} }
else -> throw EhException("Invalid favorite slot!") else -> throw EhException("Invalid favorite slot!")
} }
FavouriteStatusRouter.notify(galleryInfo) FavouriteStatusRouter.notify(galleryInfo)
add add
} }
suspend fun removeFromFavorites(galleryInfo: BaseGalleryInfo) = doModifyFavorites( suspend fun removeFromFavorites(galleryInfo: GalleryInfo) = doModifyFavorites(
galleryInfo = galleryInfo, galleryInfo = galleryInfo,
localFavorited = EhDB.containLocalFavorites(galleryInfo.gid), localFavorited = EhDB.containLocalFavorites(galleryInfo.gid),
) )
@@ -305,13 +302,11 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
EhDB.putHistoryInfo(info) EhDB.putHistoryInfo(info)
navToReader(info) navToReader(info)
} }
1 -> if (downloaded) { 1 -> if (downloaded) {
confirmRemoveDownload(info) confirmRemoveDownload(info)
} else { } else {
startDownload(false, info) startDownload(false, info)
} }
2 -> if (favorited) { 2 -> if (favorited) {
runSuspendCatching { runSuspendCatching {
removeFromFavorites(info) removeFromFavorites(info)
@@ -327,7 +322,6 @@ suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
tip(R.string.add_to_favorite_failure) tip(R.string.add_to_favorite_failure)
} }
} }
3 -> showMoveDownloadLabel(info) 3 -> showMoveDownloadLabel(info)
} }
} }
@@ -400,7 +394,7 @@ suspend fun showMoveDownloadLabel(info: GalleryInfo) {
val selected = awaitSelectItem(labels, R.string.download_move_dialog_title) val selected = awaitSelectItem(labels, R.string.download_move_dialog_title)
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
val label = if (selected == 0) null else labels[selected] val label = if (selected == 0) null else labels[selected]
DownloadManager.changeLabel(listOf(downloadInfo), label) DownloadManager.changeLabel([downloadInfo], label)
} }
context(_: DialogState) context(_: DialogState)
@@ -419,7 +413,7 @@ suspend fun showMoveDownloadLabelList(list: Collection<DownloadInfo>): String? {
} }
context(_: DialogState) context(_: DialogState)
suspend fun awaitSelectDate(): String? { suspend fun awaitSelectDate(): String {
val initial = LocalDate(2007, 3, 21) val initial = LocalDate(2007, 3, 21)
val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY) val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY)
val initialMillis = initial.toEpochMillis() val initialMillis = initial.toEpochMillis()
@@ -432,8 +426,7 @@ suspend fun awaitSelectDate(): String? {
override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis in dateRange override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis in dateRange
}, },
) )
val date = dateMillis?.run { toLocalDateTime().date.toString() } return dateMillis.toLocalDateTime().date.toString()
return date
} }
context(_: Context, _: DialogState) context(_: Context, _: DialogState)
@@ -10,11 +10,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.DialogState
import com.hippo.ehviewer.ui.tools.LocalGlobalDialogState 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.hippo.ehviewer.util.findActivity
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.CoroutineScope 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)
}
}
@@ -25,11 +25,11 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.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.Settings
import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.thumbUrl import com.hippo.ehviewer.client.thumbUrl
import com.hippo.ehviewer.ui.screen.navWithUrl import com.hippo.ehviewer.ui.screen.navWithUrl
import com.hippo.ehviewer.util.addTextToClipboard import com.hippo.ehviewer.util.addTextToClipboard
@@ -24,7 +24,10 @@ import android.content.pm.verify.domain.DomainVerificationManager
import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE import android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
@@ -63,14 +66,13 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberDrawerState2 import androidx.compose.material3.rememberDrawerState2
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.currentCompositeKeyHashCode 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.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp import androidx.compose.ui.util.lerp
import androidx.compose.ui.zIndex
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.ehviewer.core.files.isDirectory
import com.ehviewer.core.files.toOkioPath
import com.ehviewer.core.i18n.R import com.ehviewer.core.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.Settings
import com.hippo.ehviewer.client.data.ListUrlBuilder import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser 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.collectAsState
import com.hippo.ehviewer.download.DownloadService import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.downloadLocation 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.DownloadScreenDestination
import com.hippo.ehviewer.ui.destinations.DownloadsScreenDestination import com.hippo.ehviewer.ui.destinations.DownloadsScreenDestination
import com.hippo.ehviewer.ui.destinations.FavouritesScreenDestination 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.screen.navWithUrl
import com.hippo.ehviewer.ui.settings.showNewVersion import com.hippo.ehviewer.ui.settings.showNewVersion
import com.hippo.ehviewer.ui.tools.DialogState 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.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.awaitInputText import com.hippo.ehviewer.ui.tools.awaitInputText
import com.hippo.ehviewer.updater.AppUpdater 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.displayString
import com.hippo.ehviewer.util.getParcelableExtraCompat import com.hippo.ehviewer.util.getParcelableExtraCompat
import com.hippo.ehviewer.util.getUrlFromClipboard 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.ehviewer.util.sha1
import com.hippo.files.isDirectory
import com.hippo.files.toOkioPath
import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.spec.Direction import com.ramcosta.composedestinations.spec.Direction
import com.ramcosta.composedestinations.utils.currentDestinationAsState import com.ramcosta.composedestinations.utils.currentDestinationAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.view.setSecureScreen
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -173,7 +181,7 @@ private val navItems = arrayOf<Triple<Direction, Int, ImageVector>>(
Triple(SettingsScreenDestination, R.string.settings, Icons.Default.Settings), Triple(SettingsScreenDestination, R.string.settings, Icons.Default.Settings),
) )
class MainActivity : EhActivity() { class MainActivity : AppCompatActivity() {
private var shareUrl: String? = null private var shareUrl: String? = null
@@ -199,8 +207,17 @@ class MainActivity : EhActivity() {
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen().setKeepOnScreenCondition { !initialized }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge()
// https://issuetracker.google.com/204791558
// Fix system bars insets still exist in fullscreen mode on API < 30
@Suppress("DEPRECATION")
if (!isAtLeastR) {
with(window.decorView) {
systemUiVisibility = systemUiVisibility and View.SYSTEM_UI_FLAG_LAYOUT_STABLE.inv()
}
}
setMD3Content { setMD3Content {
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val navDrawerState = rememberDrawerState(DrawerValue.Closed) val navDrawerState = rememberDrawerState(DrawerValue.Closed)
@@ -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) val cannotParse = stringResource(R.string.error_cannot_parse_the_url)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
intentFlow.collect { intent -> intentFlow.collect { intent ->
@@ -274,7 +300,7 @@ class MainActivity : EhActivity() {
if ("text/plain" == type) { if ("text/plain" == type) {
val keyword = intent.getStringExtra(Intent.EXTRA_TEXT) val keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
if (keyword != null && !navWithUrl(keyword)) { if (keyword != null && !navWithUrl(keyword)) {
navigate(ListUrlBuilder(mKeyword = keyword).asDst()) navigate(ListUrlBuilder(keyword = keyword).asDst())
} }
} else if (type != null && type.startsWith("image/")) { } else if (type != null && type.startsWith("image/")) {
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM) val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
@@ -354,7 +380,7 @@ class MainActivity : EhActivity() {
var snackbarFabPadding by remember { mutableStateOf(0.dp) } var snackbarFabPadding by remember { mutableStateOf(0.dp) }
val drawerEnabled = drawerHandle.isNotEmpty() val drawerEnabled = drawerHandle.isNotEmpty()
val density = LocalDensity.current val density = LocalDensity.current
val adaptiveInfo = currentWindowAdaptiveInfo() val adaptiveInfo = currentWindowAdaptiveInfoV2()
val needSignIn by Settings.needSignIn.collectAsState() val needSignIn by Settings.needSignIn.collectAsState()
val launchPage by Settings.launchPage.collectAsState() val launchPage by Settings.launchPage.collectAsState()
CompositionLocalProvider( CompositionLocalProvider(
@@ -532,7 +558,6 @@ val LocalNavDrawerState = compositionLocalOf<DrawerState> { error("CompositionLo
val LocalDrawerHandle = compositionLocalOf<SnapshotStateList<Long>> { error("CompositionLocal LocalDrawerHandle not present!") } val LocalDrawerHandle = compositionLocalOf<SnapshotStateList<Long>> { error("CompositionLocal LocalDrawerHandle not present!") }
val LocalSnackBarHostState = compositionLocalOf<SnackbarHostState> { error("CompositionLocal LocalSnackBarHostState 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!") } val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope> { error("CompositionLocal LocalSharedTransitionScope not present!") }
@Composable @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.ehviewer.core.i18n.R
import com.hippo.ehviewer.Settings
context(ctx: Context)
fun isAuthenticationSupported(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
return BiometricManager.from(ctx).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
class SecurityActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
if (isAuthenticationSupported()) {
startAuthentication()
} else {
onSuccess()
}
}
private val authLauncher = registerForAuthenticationResult { result ->
when (result) {
is AuthenticationResult.Success -> onSuccess()
is AuthenticationResult.Error -> onError()
}
}
@Synchronized
private fun startAuthentication() {
if (isAuthenticating) return
isAuthenticating = true
val request = biometricRequest(
title = getString(R.string.settings_privacy_require_unlock),
authFallback = Biometric.Fallback.DeviceCredential,
) {}
authLauncher.launch(request)
}
private fun onSuccess() {
isAuthenticating = false
locked = false
finish()
}
private fun onError() {
moveTaskToBack(true)
isAuthenticating = false
}
}
private var isAuthenticating = false
var locked = true
var lockedLastLeaveTime: Long = 0
val lockObserver = object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
if (!locked) {
lockedLastLeaveTime = System.currentTimeMillis() / 1000
}
locked = true
}
}
fun Context.interceptSecurityOrReturn() {
val lockedResumeTime = System.currentTimeMillis() / 1000
val lockedDelayTime = lockedResumeTime - lockedLastLeaveTime
if (lockedDelayTime < Settings.securityDelay.value * 60) {
locked = false
} else if (Settings.security.value && isAuthenticationSupported() && locked) {
startActivity(Intent(this, SecurityActivity::class.java))
}
}
@@ -0,0 +1,97 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui
import android.content.Context
import androidx.biometric.AuthenticationRequest.Biometric
import androidx.biometric.AuthenticationRequest.Companion.biometricRequest
import androidx.biometric.AuthenticationResult
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.compose.rememberAuthenticationLauncher
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.LifecycleResumeEffect
import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.collectAsState
import kotlin.time.Duration.Companion.minutes
import kotlin.time.TimeSource
context(ctx: Context)
fun isAuthenticationSupported(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
return BiometricManager.from(ctx).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
@Composable
fun SecurityScreen(onError: () -> Unit, modifier: Modifier) {
val security by Settings.security.collectAsState()
var locked by remember { mutableStateOf(security) }
var isAuthenticating by remember { mutableStateOf(false) }
val title = stringResource(R.string.settings_privacy_require_unlock)
val launcher = rememberAuthenticationLauncher { result ->
isAuthenticating = false
when (result) {
is AuthenticationResult.Success -> locked = false
else -> onError()
}
}
@Synchronized
fun startAuthentication() {
if (isAuthenticating) return
isAuthenticating = true
val request = biometricRequest(title, Biometric.Fallback.DeviceCredential) {}
launcher.launch(request)
}
val securityDelay by Settings.securityDelay.collectAsState { it.minutes }
var leaveTime by remember { mutableStateOf(TimeSource.Monotonic.markNow()) }
LifecycleResumeEffect(Unit) {
if (security && leaveTime.elapsedNow() > securityDelay) {
locked = true
}
onPauseOrDispose {
if (!locked) {
leaveTime = TimeSource.Monotonic.markNow()
}
}
}
if (locked) {
val windowInfo = LocalWindowInfo.current
LaunchedEffect(windowInfo) {
snapshotFlow { windowInfo.isWindowFocused }.collect {
if (it) startAuthentication()
}
}
Surface(modifier = modifier.fillMaxSize()) {}
}
}
@@ -14,10 +14,10 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import com.ehviewer.core.ui.util.ProvideVectorPainterCache
import com.hippo.ehviewer.ui.theme.EhTheme import com.hippo.ehviewer.ui.theme.EhTheme
import com.hippo.ehviewer.ui.tools.DialogState import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.LocalGlobalDialogState import com.hippo.ehviewer.ui.tools.LocalGlobalDialogState
import com.hippo.ehviewer.ui.tools.ProvideVectorPainterCache
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import me.zhanghai.compose.preference.ProvidePreferenceTheme import me.zhanghai.compose.preference.ProvidePreferenceTheme
import me.zhanghai.compose.preference.preferenceTheme import me.zhanghai.compose.preference.preferenceTheme
@@ -21,7 +21,7 @@ import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri import androidx.core.net.toUri
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.client.data.GalleryDetail import com.ehviewer.core.model.GalleryDetail
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -7,8 +7,9 @@ import coil3.decode.DecodeUtils
import coil3.imageLoader import coil3.imageLoader
import coil3.request.crossfade import coil3.request.crossfade
import coil3.size.Scale import coil3.size.Scale
import coil3.size.Size
import com.ehviewer.core.util.toIntOrDefault
import com.hippo.ehviewer.ktbuilder.imageRequest import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.util.toIntOrDefault
import kotlin.math.roundToInt import kotlin.math.roundToInt
import splitties.init.appCtx import splitties.init.appCtx
@@ -21,7 +22,7 @@ class CoilImageGetter(private val onSuccess: () -> Unit) : Html.ImageGetter {
val srcHeight = groupValues[2].toInt() val srcHeight = groupValues[2].toInt()
val dstWidth = groupValues[3].toIntOrDefault(200) val dstWidth = groupValues[3].toIntOrDefault(200)
val dstHeight = dstWidth / 2 * 3 val dstHeight = dstWidth / 2 * 3
val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT) val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT, Size.ORIGINAL)
setBounds(0, 0, (srcWidth * multiplier).roundToInt(), (srcHeight * multiplier).roundToInt()) setBounds(0, 0, (srcWidth * multiplier).roundToInt(), (srcHeight * multiplier).roundToInt())
} ?: setBounds(0, 0, 200, 300) } ?: setBounds(0, 0, 200, 300)
with(appCtx) { with(appCtx) {
@@ -41,6 +41,7 @@ class LinkifyTextView(
val x = event.x + scrollX - totalPaddingLeft val x = event.x + scrollX - totalPaddingLeft
val y = event.y + scrollY - totalPaddingTop val y = event.y + scrollY - totalPaddingTop
val line = layout.getLineForVertical(y.toInt()) val line = layout.getLineForVertical(y.toInt())
if (x > layout.getLineMax(line)) return@let
val off = layout.getOffsetForHorizontal(line, x) val off = layout.getOffsetForHorizontal(line, x)
val spans = text.getSpans<URLSpan>(off, off) val spans = text.getSpans<URLSpan>(off, off)
if (spans.isNotEmpty()) { if (spans.isNotEmpty()) {
@@ -1,11 +1,11 @@
package com.hippo.ehviewer.ui.login package com.hippo.ehviewer.ui.login
import arrow.core.Either.Companion.catch 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.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.EhUrl
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -59,6 +59,12 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.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.Settings
import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.EhUrl
@@ -66,18 +72,12 @@ import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.Screen import com.hippo.ehviewer.ui.Screen
import com.hippo.ehviewer.ui.destinations.WebViewSignInScreenDestination import com.hippo.ehviewer.ui.destinations.WebViewSignInScreenDestination
import com.hippo.ehviewer.ui.openBrowser 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.awaitConfirmationOrCancel
import com.hippo.ehviewer.ui.tools.isExpanded
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.displayString import com.hippo.ehviewer.util.displayString
import com.jamal.composeprefs3.ui.ifTrueThen
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import moe.tarsin.launchIO
import moe.tarsin.navigate import moe.tarsin.navigate
@Destination<RootGraph>(start = true) @Destination<RootGraph>(start = true)
@@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.ehviewer.core.network.EhCookieStore
import com.google.accompanist.web.WebView import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState import com.google.accompanist.web.rememberWebViewState
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.Screen import com.hippo.ehviewer.ui.Screen
@@ -31,6 +31,7 @@ fun AnimatedVisibilityScope.WebViewSignInScreen(navigator: DestinationsNavigator
Settings.desktopSite.value = false Settings.desktopSite.value = false
} }
if (EhCookieStore.hasSignedIn()) { if (EhCookieStore.hasSignedIn()) {
EhCookieStore.flush()
postLogin() postLogin()
state.webView?.destroy() state.webView?.destroy()
bgWork { awaitCancellation() } bgWork { awaitCancellation() }
@@ -44,6 +44,10 @@ import arrow.core.some
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.network.HttpException import coil3.network.HttpException
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.ehviewer.core.ui.component.RollingNumber
import com.ehviewer.core.ui.component.RollingNumberPlaceholder
import com.ehviewer.core.util.launch
import com.ehviewer.core.util.launchIO
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
@@ -65,8 +69,6 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import moe.tarsin.coroutines.runSwallowingWithUI import moe.tarsin.coroutines.runSwallowingWithUI
import moe.tarsin.launch
import moe.tarsin.launchIO
private val limitScope = CoroutineScope(Dispatchers.IO) private val limitScope = CoroutineScope(Dispatchers.IO)
private val refreshEvent = MutableSharedFlow<Unit>() private val refreshEvent = MutableSharedFlow<Unit>()
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons 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.Pause
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Card import androidx.compose.material3.Card
@@ -26,6 +27,7 @@ import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@@ -35,19 +37,18 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.Settings 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.client.EhUtils
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager 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 import com.hippo.ehviewer.util.FileUtils
@Composable @Composable
@@ -60,6 +61,7 @@ fun DownloadCard(
onStop: () -> Unit, onStop: () -> Unit,
info: DownloadInfo, info: DownloadInfo,
selectMode: Boolean, selectMode: Boolean,
showProgress: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) = CrystalCard(modifier = modifier, onClick = onClick, onLongClick = onLongClick, interactionSource = interactionSource) { ) = 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_NONE -> stringResource(R.string.download_state_none)
DownloadInfo.STATE_WAIT -> stringResource(R.string.download_state_wait) DownloadInfo.STATE_WAIT -> stringResource(R.string.download_state_wait)
DownloadInfo.STATE_FAILED -> if (info.legacy <= 0) stateFailed else stateFailed2 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 else -> null // The item has been removed and this will be disposed soon
} }
ProvideTextStyle(MaterialTheme.typography.labelLarge) { ProvideTextStyle(MaterialTheme.typography.labelLarge) {
@@ -149,24 +158,24 @@ fun DownloadCard(
Text( Text(
text = categoryText, text = categoryText,
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp), 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, style = MaterialTheme.typography.labelLarge,
) )
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
val running = downloadState == DownloadInfo.STATE_WAIT || downloadState == DownloadInfo.STATE_DOWNLOAD val running = downloadState == DownloadInfo.STATE_WAIT || downloadState == DownloadInfo.STATE_DOWNLOAD
val icon = remember { val icon = when (downloadState) {
movableContentOf<Boolean> { DownloadInfo.STATE_WAIT, DownloadInfo.STATE_DOWNLOAD -> Icons.Default.Pause
Icon(imageVector = if (it) Icons.Default.Pause else Icons.Default.PlayArrow, contentDescription = null) DownloadInfo.STATE_FINISH -> Icons.Default.DownloadDone
} else -> Icons.Default.PlayArrow
} }
if (selectMode) { if (selectMode) {
Box(modifier = Modifier.offset(4.dp).minimumInteractiveComponentSize()) { Box(modifier = Modifier.offset(4.dp).minimumInteractiveComponentSize()) {
icon(running) Icon(imageVector = icon, contentDescription = null)
} }
} else { } else {
IconButton(onClick = if (running) onStop else onStart, shapes = IconButtonDefaults.shapes(), modifier = Modifier.offset(4.dp)) { 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 androidx.compose.ui.platform.LocalContext
import coil3.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import coil3.compose.rememberAsyncImagePainter 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.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.shouldCrop
import com.hippo.ehviewer.ui.tools.thenIf
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
@@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ehviewer.core.ui.component.RollingNumber
@Composable @Composable
fun FundsItem( fun FundsItem(
@@ -24,7 +24,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.hippo.ehviewer.client.data.GalleryComment import com.ehviewer.core.model.GalleryComment
import com.hippo.ehviewer.ui.legacy.CoilImageGetter import com.hippo.ehviewer.ui.legacy.CoilImageGetter
import com.hippo.ehviewer.ui.legacy.LinkifyTextView import com.hippo.ehviewer.ui.legacy.LinkifyTextView
import com.hippo.ehviewer.util.ReadableTime import com.hippo.ehviewer.util.ReadableTime
@@ -33,13 +33,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryDetail
import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.ui.icons.EhIcons
import com.ehviewer.core.ui.icons.big.SadAndroid
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
import com.ehviewer.core.ui.util.detailThumbGenerator
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.SadAndroid
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.detailThumbGenerator
@Composable @Composable
fun GalleryDetailHeaderInfoCard( fun GalleryDetailHeaderInfoCard(
@@ -23,8 +23,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -33,22 +33,21 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.hippo.ehviewer.Settings import com.ehviewer.core.model.GalleryInfo
import com.ehviewer.core.model.GalleryInfo.Companion.NOT_FAVORITED
import com.ehviewer.core.ui.component.CrystalCard
import com.ehviewer.core.ui.component.ElevatedCard
import com.ehviewer.core.ui.component.GalleryListCardRating
import com.ehviewer.core.ui.util.SharedElementBox
import com.ehviewer.core.ui.util.TransitionsVisibilityScope
import com.ehviewer.core.ui.util.listThumbGenerator
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.ElevatedCard
import com.hippo.ehviewer.ui.tools.GalleryListCardRating
import com.hippo.ehviewer.ui.tools.SharedElementBox
import com.hippo.ehviewer.ui.tools.TransitionsVisibilityScope
import com.hippo.ehviewer.ui.tools.listThumbGenerator
import com.hippo.ehviewer.util.FavouriteStatusRouter import com.hippo.ehviewer.util.FavouriteStatusRouter
@Composable @Composable
@@ -58,6 +57,7 @@ fun GalleryInfoListItem(
onLongClick: () -> Unit, onLongClick: () -> Unit,
info: GalleryInfo, info: GalleryInfo,
showPages: Boolean, showPages: Boolean,
showProgress: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isInFavScene: Boolean = false, isInFavScene: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@@ -126,7 +126,12 @@ fun GalleryInfoListItem(
Text(text = it) Text(text = it)
} }
if (info.pages != 0 && showPages) { if (info.pages != 0 && showPages) {
Text(text = "${info.pages}P") val readProgress = if (showProgress) {
remember { EhDB.getReadProgressFlow(info.gid) }.collectAsState(0).value
} else {
0
}
Text(text = if (readProgress > 0) "${readProgress + 1}/${info.pages}P" else "${info.pages}P")
} }
} }
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -135,7 +140,7 @@ fun GalleryInfoListItem(
Text( Text(
text = categoryText, text = categoryText,
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp), modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
color = if (Settings.harmonizeCategoryColor.value) Color.Unspecified else EhUtils.categoryTextColor, color = EhUtils.getCategoryTextColor(categoryColor),
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text(text = info.posted.orEmpty()) Text(text = info.posted.orEmpty())
@@ -154,6 +159,7 @@ fun GalleryInfoGridItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showLanguage: Boolean = true, showLanguage: Boolean = true,
showPages: Boolean = true, showPages: Boolean = true,
showProgress: Boolean = true,
showFavoriteStatus: Boolean = true, showFavoriteStatus: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) = ElevatedCard( ) = ElevatedCard(
@@ -187,11 +193,16 @@ fun GalleryInfoGridItem(
Badge( Badge(
modifier = Modifier.align(Alignment.TopEnd).widthIn(min = 32.dp).height(24.dp), modifier = Modifier.align(Alignment.TopEnd).widthIn(min = 32.dp).height(24.dp),
containerColor = categoryColor, containerColor = categoryColor,
contentColor = if (Settings.harmonizeCategoryColor.value) contentColorFor(categoryColor) else EhUtils.categoryTextColor, contentColor = EhUtils.getCategoryTextColor(categoryColor),
) { ) {
val shouldShowLanguage = showLanguage && info.simpleLanguage != null val shouldShowLanguage = showLanguage && info.simpleLanguage != null
if (showPages && info.pages > 0) { if (showPages && info.pages > 0) {
Text(text = "${info.pages}") val readProgress = if (showProgress) {
remember { EhDB.getReadProgressFlow(info.gid) }.collectAsState(0).value
} else {
0
}
Text(text = if (readProgress > 0) "${readProgress + 1}/${info.pages}" else "${info.pages}")
if (shouldShowLanguage) { if (shouldShowLanguage) {
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
} }
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
@@ -32,7 +33,6 @@ import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -45,38 +45,28 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemContentType import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey import androidx.paging.compose.itemKey
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.ui.component.FastScrollLazyVerticalGrid
import com.ehviewer.core.ui.component.FastScrollLazyVerticalStaggeredGrid
import com.ehviewer.core.ui.icons.EhIcons
import com.ehviewer.core.ui.icons.big.SadAndroid
import com.ehviewer.core.util.launch
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.exception.NoHitsFoundException import com.hippo.ehviewer.client.exception.NoHitsFoundException
import com.hippo.ehviewer.coil.PrefetchAround import com.hippo.ehviewer.coil.PrefetchAround
import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.SadAndroid
import com.hippo.ehviewer.ktbuilder.imageRequest import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.screen.collectDetailSizeAsState import com.hippo.ehviewer.ui.screen.collectDetailSizeAsState
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid
import com.hippo.ehviewer.util.displayString import com.hippo.ehviewer.util.displayString
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import moe.tarsin.launch
@Stable
operator fun PaddingValues.plus(r: PaddingValues) = object : PaddingValues {
val l = this@plus
override fun calculateBottomPadding() = l.calculateBottomPadding() + r.calculateBottomPadding()
override fun calculateLeftPadding(layoutDirection: LayoutDirection) = l.calculateLeftPadding(layoutDirection) + r.calculateLeftPadding(layoutDirection)
override fun calculateRightPadding(layoutDirection: LayoutDirection) = l.calculateRightPadding(layoutDirection) + r.calculateRightPadding(layoutDirection)
override fun calculateTopPadding() = l.calculateTopPadding() + r.calculateTopPadding()
}
@Composable @Composable
context(_: CoroutineScope, _: Context) context(_: CoroutineScope, _: Context)
@@ -207,7 +197,6 @@ fun GalleryList(
} }
} }
} }
is LoadState.Error -> { is LoadState.Error -> {
Surface { Surface {
ErrorTip( ErrorTip(
@@ -216,7 +205,6 @@ fun GalleryList(
) )
} }
} }
is LoadState.NotLoading -> if (data.itemCount == 0) { is LoadState.NotLoading -> if (data.itemCount == 0) {
// Only for local favorites as empty gallery lists from network are treated as error // Only for local favorites as empty gallery lists from network are treated as error
ErrorTip(modifier = Modifier.widthIn(max = 228.dp), text = stringResource(id = R.string.gallery_list_empty_hit)) ErrorTip(modifier = Modifier.widthIn(max = 228.dp), text = stringResource(id = R.string.gallery_list_empty_hit))
@@ -23,10 +23,10 @@ import androidx.compose.ui.unit.IntSize
import coil3.BitmapImage import coil3.BitmapImage
import coil3.compose.AsyncImagePainter.State import coil3.compose.AsyncImagePainter.State
import coil3.compose.rememberAsyncImagePainter import coil3.compose.rememberAsyncImagePainter
import com.hippo.ehviewer.client.data.GalleryPreview import com.ehviewer.core.model.GalleryPreview
import com.hippo.ehviewer.client.data.V2GalleryPreview import com.ehviewer.core.model.V2GalleryPreview
import com.ehviewer.core.ui.component.CrystalCard
import com.hippo.ehviewer.ktbuilder.imageRequest import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.shouldCrop import com.hippo.ehviewer.ui.tools.shouldCrop
@Composable @Composable
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -19,14 +20,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.ehviewer.core.model.GalleryTagGroup
import com.ehviewer.core.model.PowerStatus
import com.ehviewer.core.model.TagNamespace
import com.ehviewer.core.model.VoteStatus
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhTagDatabase import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.hippo.ehviewer.client.data.PowerStatus
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.client.data.VoteStatus
import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.tools.includeFontPadding import com.hippo.ehviewer.ui.tools.includeFontPadding
@@ -59,6 +61,7 @@ fun GalleryTags(
BaseRoundText( BaseRoundText(
text = translation, text = translation,
weak = power == PowerStatus.Weak, weak = power == PowerStatus.Weak,
solid = power == PowerStatus.Solid && showVote,
modifier = Modifier.combinedClickable( modifier = Modifier.combinedClickable(
onClick = { onTagClick(tag) }, onClick = { onTagClick(tag) },
onLongClick = { onLongClick = {
@@ -88,6 +91,7 @@ private fun BaseRoundText(
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
weak: Boolean = false, weak: Boolean = false,
solid: Boolean = false,
isGroup: Boolean = false, isGroup: Boolean = false,
) { ) {
val bgColor = if (isGroup) { val bgColor = if (isGroup) {
@@ -103,8 +107,9 @@ private fun BaseRoundText(
Text( Text(
text = text, text = text,
modifier = modifier.padding(horizontal = 12.dp, vertical = 4.dp).width(IntrinsicSize.Max), modifier = modifier.padding(horizontal = 12.dp, vertical = 4.dp).width(IntrinsicSize.Max),
color = MaterialTheme.colorScheme.onSurface.let { if (weak) it.copy(0.5F) else it }, color = LocalContentColor.current.let { if (weak) it.copy(0.5F) else it },
style = MaterialTheme.typography.labelLarge.includeFontPadding, style = MaterialTheme.typography.labelLarge.includeFontPadding,
textDecoration = if (solid) TextDecoration.Underline else null,
) )
} }
} }
@@ -22,7 +22,6 @@ fun LoadStateIndicator(
is LoadState.Loading -> { is LoadState.Loading -> {
LinearWavyProgressIndicator(modifier = modifier) LinearWavyProgressIndicator(modifier = modifier)
} }
is LoadState.Error -> { is LoadState.Error -> {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = state.error.displayString()) Text(text = state.error.displayString())
@@ -31,6 +30,5 @@ fun LoadStateIndicator(
} }
} }
} }
is LoadState.NotLoading -> Unit is LoadState.NotLoading -> Unit
} }
@@ -34,20 +34,20 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import arrow.core.raise.ensure import arrow.core.raise.ensure
import com.ehviewer.core.i18n.R import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.GalleryInfo.Companion.S_LANG_TAGS
import com.ehviewer.core.ui.component.DropdownFilterChip
import com.ehviewer.core.ui.util.thenIf
import com.ehviewer.core.util.launch
import com.ehviewer.core.util.toIntOrDefault
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhTagDatabase import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANG_TAGS
import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.ui.tools.DialogState import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.ui.tools.DropdownFilterChip
import com.hippo.ehviewer.ui.tools.awaitResult import com.hippo.ehviewer.ui.tools.awaitResult
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.toIntOrDefault
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import moe.tarsin.launch
private val categoryTable = arrayOf( private val categoryTable = [
EhUtils.DOUJINSHI to R.string.doujinshi, EhUtils.DOUJINSHI to R.string.doujinshi,
EhUtils.MANGA to R.string.manga, EhUtils.MANGA to R.string.manga,
EhUtils.ARTIST_CG to R.string.artist_cg, EhUtils.ARTIST_CG to R.string.artist_cg,
@@ -58,7 +58,7 @@ private val categoryTable = arrayOf(
EhUtils.COSPLAY to R.string.cosplay, EhUtils.COSPLAY to R.string.cosplay,
EhUtils.ASIAN_PORN to R.string.asian_porn, EhUtils.ASIAN_PORN to R.string.asian_porn,
EhUtils.MISC to R.string.misc, EhUtils.MISC to R.string.misc,
) ]
@Composable @Composable
context(_: DialogState, _: CoroutineScope, _: Context) context(_: DialogState, _: CoroutineScope, _: Context)
@@ -193,9 +193,6 @@ fun SearchFilter(
} }
if (error != null) { if (error != null) {
ListItem( ListItem(
headlineContent = {
Text(text = error, style = MaterialTheme.typography.bodySmall)
},
leadingContent = { leadingContent = {
Icon(imageVector = Icons.Default.Info, contentDescription = null) Icon(imageVector = Icons.Default.Info, contentDescription = null)
}, },
@@ -204,6 +201,9 @@ fun SearchFilter(
headlineColor = MaterialTheme.colorScheme.error, headlineColor = MaterialTheme.colorScheme.error,
leadingIconColor = MaterialTheme.colorScheme.error, leadingIconColor = MaterialTheme.colorScheme.error,
), ),
content = {
Text(text = error, style = MaterialTheme.typography.bodySmall)
},
) )
} }
} }
Loaded 100 of 368 files, more files were not shown because too many files have changed in this diff. Show more