Compare commits

...
638 Commits
Author SHA1 Message Date
FooIbar 70082090a1 Prepare for release 1.10.1
Releases / Build (push) Has been cancelled
2023-12-19 18:34:50 +08:00
FooIbar dbc1a32496 Update changelog for 1.10.1 (#526)
[no ci]
2023-12-19 18:34:04 +08:00
FooIbar e8c9ee5dd0 Cleanup obsolete logs 2023-12-19 16:41:33 +08:00
FooIbar 5c3657736b Dump heap on OOM 2023-12-19 16:41:33 +08:00
FooIbar 5191408d06 Provide dismiss action for snackbars with actions 2023-12-19 14:09:33 +08:00
FooIbar 347ce6854d Fix missing content warning cookie in guest mode 2023-12-19 11:17:58 +08:00
FooIbar f86dec9b2e Workaround unable to exit search field on API < 28 2023-12-19 09:46:24 +08:00
FooIbar c74b128b1a Debounce slider value changes 2023-12-19 09:46:13 +08:00
FooIbar 8ee28a4b4f Update history regardless of availability
In case the gallery is taken down by DMCA.
2023-12-18 13:56:01 +08:00
renovate[bot] 3b07b6997d Update dependency com.squareup.okhttp3:okhttp-bom to v5.0.0-alpha.12 2023-12-17 16:15:10 +00:00
FooIbar f59023ed03 Disable some unnecessary logging 2023-12-17 23:05:47 +08:00
FooIbar 3fc13e61c3 Refine download delay 2023-12-17 22:52:04 +08:00
FooIbar ae22f448a8 Revert to use OkHttp as fallback engine
This partially reverts commit 7b67210876

Reason for revert: `setApplicationProtocols` used by Apache 5 HC is not
available until API 29.
2023-12-17 19:12:56 +08:00
renovate[bot] 450c15d51e Update dependency com.squareup.okio:okio-jvm to v3.7.0 2023-12-17 01:17:50 +00:00
FooIbar d9e5bb4399 Fix unreleased lock 2023-12-17 01:03:57 +08:00
FooIbar 369904f659 Don't update page state on force retry 2023-12-16 16:35:49 +08:00
FooIbar 96302b2344 Log before making requests instead of after 2023-12-16 16:26:13 +08:00
FooIbar eeffb12e3b Cleanup code 2023-12-15 15:42:03 +08:00
FooIbar 57c12f7df0 Prepare for next iteration 2023-12-14 22:54:08 +08:00
FooIbar 37eb2f1aed Prepare for release 1.10.0
Releases / Build (push) Has been cancelled
2023-12-14 22:50:41 +08:00
renovate[bot] c70ec5f354 Update dependency dev.chrisbanes.compose:compose-bom to v2023.12.00-alpha04 2023-12-14 14:42:27 +00:00
FooIbar 7139af4629 Update renovate.json
[no ci]
2023-12-14 22:25:27 +08:00
FooIbar a2bac029f5 Update renovate.json
[no ci]
2023-12-14 22:21:04 +08:00
FooIbar 6c68074e8b Update CHANGELOG.zh-CN.MD (#503)
[no ci]
2023-12-14 22:12:55 +08:00
FooIbar 991caf73f4 Fix ReaderPageSheet padding and ripple 2023-12-14 20:18:40 +08:00
FooIbar 0ba11641fa Update compose compiler 2023-12-14 19:45:32 +08:00
FooIbar b51120e914 Drop deprecated Accompanist libraries (#500)
* Migrate XML themes to Compose

* Vendor Accompanist WebView
2023-12-14 18:04:51 +08:00
GG Bond b02fb2d1d7 PagingSourceFactory capture mutableState only, Resolve #491 2023-12-14 12:46:42 +08:00
renovate[bot] c6247230da Update androidx.navigation to v2.7.6 2023-12-14 04:43:19 +00:00
renovate[bot] d64be78b4e Update dependency androidx.activity:activity-compose to v1.8.2 2023-12-14 04:19:48 +00:00
renovate[bot] e75f633165 Update androidx.lifecycle to v2.7.0-rc02 2023-12-14 04:18:36 +00:00
GG Bond 27c5598133 Fix build 2023-12-14 12:00:02 +08:00
renovate[bot] a9449bfbc7 Update dependency sh.calvin.reorderable:reorderable to v1.2.0 2023-12-13 07:32:39 +00:00
GG Bond 4fecbe259b Update compose snapshot, drop outdated workaround 2023-12-13 11:51:12 +08:00
GG Bond 81d72edbf0 Update compose snapshot 2023-12-12 16:04:32 +08:00
GG Bond ea7e6e4624 Support OutgoingContent.WriteChannelContent in CronetEngine 2023-12-10 21:40:27 +08:00
GG Bond 6335d5bca4 Fix search image 2023-12-10 21:40:27 +08:00
GG Bond 227b430dbb Animate comments update, Resolve #450 2023-12-10 11:36:14 +08:00
GG Bond 0bba56a29f Let GalleryList aware of favSlot update 2023-12-10 11:21:16 +08:00
FooIbar c07d9da7b0 Reduce image target size since we are using fill 2023-12-09 17:04:54 +08:00
Vladimir DostoyevskiyandGG Bond 900c5ed007 Remove unnecessary Image -> Drawable (#484)
* Remove unnecessary Image -> Drawable

* Refine

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-09 08:11:22 +00:00
Vladimir DostoyevskiyandGG Bond ba0788f6df Move to Coil 3 (#482)
* Move to Coil 3

* Fix R8

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-09 07:37:44 +00:00
Vladimir DostoyevskiyandGG Bond c517856e89 Convert ReaderPageSheet to Compose (#481)
* Convert ReaderPageSheet to Compose

* Cleanup unused

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-09 06:39:46 +00:00
Vladimir DostoyevskiyandGG Bond 791ee0ddbf Drop icon recompose fix (#480)
* Update snapshot and drop unused workaround

* Drop unnecessary fix

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-09 02:27:28 +00:00
Vladimir DostoyevskiyandGG Bond 0746938cb9 Compose bottom reader menu (#479)
* Migrate bottom reader menu to Compose

* Use Compose to animate bottom reader menu bars

* Cleanup unused

* Refine

* Cleanup to use DropdownMenu

* Cleanup unused

* Refine BottomReaderBar layout

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-08 17:23:51 +00:00
FooIbar bbae24aba9 Follow API rate limit to avoid IP ban (#478)
* Follow API rate limit to avoid IP ban

* Delay for bulk favorite adding to prevent flooding

* Revert "Delay for bulk favorite adding to prevent flooding"

This reverts commit 0c175453101aab2d6d200a661259906291ceac36.

* Make requests in parallel
2023-12-08 16:09:40 +00:00
FooIbar 677edb0e02 Make blur radius discrete to reduce overhead (#476)
* Make blur radius discrete to reduce overhead

* Refine
2023-12-08 14:22:51 +00:00
GG Bond a4328c5111 Workaround rating icon 2023-12-08 21:46:06 +08:00
GG Bond 27cf341fef Drop paging runtime 2023-12-08 21:46:06 +08:00
GG Bond bc654e98b8 Drop explicitly coordinatorlayout usage 2023-12-08 21:46:06 +08:00
FooIbar 588f78e598 Fix search bar padding in landscape mode 2023-12-08 16:40:42 +08:00
FooIbar 70f952285a Don't scroll to top on refresh in local favorites 2023-12-08 16:08:55 +08:00
GG Bond a6f5e4f98d Make sure computeIfAbsent inlined 2023-12-08 13:27:01 +08:00
FooIbar 6fe5c1d666 Use commit author name instead of login name
Fix parse error caused by invalid GitHub account
2023-12-08 13:08:31 +08:00
GG Bond 1dba12ec94 Cache VectorPainter for MetaRatingWidget 2023-12-08 12:50:19 +08:00
GG Bond aeb3f689d1 Fix to provide it with setMD3Content 2023-12-08 12:50:19 +08:00
GG Bond 0a2268d4de Allow VectorPainter to be cached 2023-12-08 12:50:19 +08:00
GG Bond 9a32b020cf Let GalleryTags prefer next column not multiple lines 2023-12-08 00:35:20 +08:00
GG Bond c5b01a7089 Update compose snapshot for lazygrid performance 2023-12-08 00:00:28 +08:00
GG Bond 0634fbb724 Optimize to use constant ConstraintSet 2023-12-07 22:48:06 +08:00
FooIbar 445e9511a0 Flatten layout 2023-12-07 20:20:10 +08:00
renovate[bot] ac532acb5b Update Rust crate once_cell to 1.19 2023-12-07 12:03:34 +00:00
FooIbar 7a8e21fbef Keep search bar state across configuration changes 2023-12-07 15:09:59 +08:00
GG Bond 58b0f3a41e Avoid graphicsLayer overhead 2023-12-07 13:24:31 +08:00
GG Bond ab373990cd Following tachiyomi 4146c4c31d069ce9fb7ab36d72f1f8fa2be1e050 2023-12-07 12:48:50 +08:00
GG Bond 272c157fe0 Compose PageIndicatorText, revert 3744018af8 2023-12-07 12:48:50 +08:00
GG Bond e5e047ee10 Move to Compose ReaderContentOverlay 2023-12-07 12:48:50 +08:00
renovate[bot] bf0b529018 Update dependency com.google.android.material:material to v1.12.0-alpha02 2023-12-06 19:19:11 +00:00
FooIbar acf2392406 Avoid progress indicator recomposing in background 2023-12-07 00:05:50 +08:00
FooIbar cecd6fb359 [no ci] Remove deprecated JCenter repository 2023-12-06 13:31:00 +08:00
Vladimir DostoyevskiyandGG Bond 7b67210876 Fallback engine should be Apache5 HC (#456)
* Drop explicitly okhttp dependency

* Move to Apache5 HC instead

* Fix build

* Credit to Ktor instead

* Refactor Cronet Preference

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-06 04:42:17 +00:00
Vladimir DostoyevskiyandGG Bond e40c4f2c71 Save body for ByteBuffer as well (#455)
* Save parseErrorBody for ByteBuffer as well

* Fix

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-05 13:52:08 +00:00
GG Bond b0717b38b1 Cleanup okhttp urlbuilder usage 2023-12-05 18:17:33 +08:00
GG Bond 18c9b95b9b Convert FavListUrlBuilder to use ktor UrlBuilder 2023-12-05 18:17:33 +08:00
GG Bond fc63c41ab0 Cleanup okhttp usage 2023-12-04 20:30:24 +08:00
GG Bond d1c7434993 Drop CloudflareInterceptor to synchronize behaviour 2023-12-04 20:30:24 +08:00
GG Bond da15ba8b9a Always use KtorHttpUriFetcher 2023-12-04 20:30:24 +08:00
GG Bond 1d2a2a3608 Fix to copy to FileChannel directly 2023-12-04 20:30:24 +08:00
renovate[bot] 145cd7a7c0 Update plugin spotless to v6.23.3 2023-12-04 08:06:29 +00:00
FooIbar 048415f3e0 Readd QUIC hints as cronet engine is now preferred 2023-12-04 12:41:39 +08:00
GG Bond 95ccabc756 Fix download origin image 2023-12-04 01:18:25 +08:00
FooIbar 1f08c0e08c Fix ProgressScreen layout 2023-12-04 01:09:49 +08:00
GG Bond b036aeef23 Refine 2023-12-04 00:46:58 +08:00
GG Bond d469a8404c Refine 2023-12-04 00:46:58 +08:00
GG Bond 5b16b6aed9 Hint the scheduler that we are mostly non-blocking 2023-12-04 00:46:58 +08:00
GG Bond 151949aecf Move SpiderDen to use ktor directly
Should have no performance regression, though benchmark is unavailable

Cache should be managed on ktor layer anyway
2023-12-04 00:46:58 +08:00
FooIbar de2c4ca3f5 Move category filters below search bar 2023-12-03 23:03:52 +08:00
FooIbar e5bebcfe80 [no ci] Update renovate.json 2023-12-03 21:50:00 +08:00
FooIbar 0080a482e4 [no ci] Don't update to SNAPSHOTs 2023-12-03 21:48:41 +08:00
FooIbar 756867416b Fix crash on clicking gallery link with page number 2023-12-03 21:43:05 +08:00
FooIbar 124ff2f190 Fix intent emitted before collection being missed 2023-12-03 21:16:16 +08:00
FooIbar 828a3d09b6 Move advanced options below search bar 2023-12-03 19:39:58 +08:00
FooIbar fc3b9d73ec Add language filter for search (#428)
* Add language filter for search

* Refine

* Refine
2023-12-03 06:44:00 +00:00
FooIbar ba943a31d4 [no ci] Update ci.yml 2023-12-03 13:35:29 +08:00
renovate[bot] 8ee1ac7617 Update dependency sh.calvin.reorderable:reorderable to v1.1.0 2023-12-03 05:30:30 +00:00
FooIbar 113f2a1794 Fix NPE (#433)
* Fix NPE

* Enable strict mode in debug environment
2023-12-03 05:14:09 +00:00
GG Bond f6028813e2 Limit CronetEngine to use 1 thread since we are totally async & non-blocking 2023-12-03 11:57:40 +08:00
GG Bond b094a32d12 Convert to use async Channel for reuse of CancellableContinuation 2023-12-03 11:57:40 +08:00
FooIbar fd3e4bdc60 Fix search bar offset on label change 2023-12-02 22:10:06 +08:00
FooIbarandVladimir Dostoyevskiy 142092d930 Add missing inline modifier (#426)
* Add missing inline modifier

* Update EhRequestBuilder.kt

* Update EhRequestBuilder.kt

---------

Co-authored-by: Vladimir Dostoyevskiy <99822064+VDostoyevskiy@users.noreply.github.com>
2023-12-02 12:15:30 +00:00
FooIbar 75e3e78ecb Update history when viewing downloaded galleries 2023-12-02 17:11:16 +08:00
FooIbar 053832681f Add search suggestion item placement animation 2023-12-02 15:54:20 +08:00
Vladimir DostoyevskiyandGG Bond 29e33d6e69 Initial CronetEngine (#421)
* Initial CronetEngine without UploadDataProvider support

* Support ByteArrayContent

* Cleanup

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-02 07:50:47 +00:00
FooIbar 2d878e2f1c Migrate to kotlinx datetime [1/2] 2023-12-02 15:34:51 +08:00
Vladimir DostoyevskiyandGG Bond 0fdbfbb1ae Move to Ktor Cookie handle, Part 2 (#419)
* Migration

* Migration

* Migration

* Migration

* Make EhCookieStore a CookiesStorage

* Drop CookieDB migration

* Drop one Okhttp cookie usage

* Drop one Okhttp cookie usage

* Remove typealias

* Move EhTagDatabase to use ktor

* Make cronet just use Dispatchers.IO

* Make all okhttpClient internally used

* Cleanup

* Refine

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-02 05:42:02 +00:00
Vladimir DostoyevskiyandGG Bond 52f314f6b0 Move to Ktor, Part 1 (#415)
* Ktor 3.0.0-beta-1

* Drop chunker since we no more se okhttp

* Move getGalleryDetail to ktor

* Migration

* Migration

* Migration

* Migration

* Migration

* Migration

* Migration

* Migration

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-02 09:33:46 +08:00
renovate[bot] ff8d8916aa Update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.5.0 2023-12-01 19:02:36 +00:00
FooIbar b2368b312f Prepare for next iteration 2023-12-01 22:41:12 +08:00
FooIbar 3cade906d6 Prepare for release 1.9.0 (#406)
* Prepare for release 1.9.0

* Add known issues

* Bump version

* [no ci] Rename CHANGELOG.MD
2023-12-01 22:35:47 +08:00
FooIbar e7cb33ef6f Fix calling Activity.recreate from IO thread 2023-12-01 22:25:12 +08:00
Vladimir DostoyevskiyandGG Bond 8c331c9577 Fix BottomSheet (#414)
* Fix BottomSheet

* Predictive Back for ModelBottomSheet

* Naming

* Naming

* Naming

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-12-01 22:13:03 +08:00
FooIbar 8560a050ba Make CheckBox/RadioButton text clickable and refine layout (#416)
* Make CheckBox/RadioButton text clickable

* Introduce LabeledCheckbox and refine layout
2023-12-01 13:59:28 +00:00
FooIbar 583637b692 Fix IME not hiding after performing search (#412)
* Fix IME not hiding after performing search

Regression of https://android.googlesource.com/platform/frameworks/support/+/8f81d46e2c3f37564d0b7515ec9411314645e187

* Add comment
2023-12-01 06:31:02 +00:00
FooIbar 791359e949 Fix last item placement animation 2023-12-01 13:30:17 +08:00
FooIbar e238b6aec0 Fix touch slop 2023-12-01 12:27:16 +08:00
renovate[bot] 17a143c4de Update dependency com.android.application to v8.2.0 2023-11-30 19:36:10 +00:00
FooIbar dfb0aa823e Fix crash when deleting downloads 2023-12-01 01:57:18 +08:00
renovate[bot] b5cad36967 Update kotlinx.serialization to v1.6.2 2023-11-30 15:53:13 +00:00
tehcneko 86378630bb Fix searchLayout close icon (#404)
* Fix searchLayout close icon

* Update GalleryListScreen.kt
2023-11-30 12:35:43 +00:00
Vladimir DostoyevskiyandGG Bond 730035aff7 Refine Fablayout (#403)
* Fix read page

* Refine FabLayout

* Let Fablayout have consistent animation state, walk consistently

* Let Fablayout collapse when secondaryFab updated

* Move animation delay out of FabLayout, should not handle that within FabLayout

* Refine predictive back

* Refine predictive back

* Introduce waitCollapse

* Refine

* Simplify

* Simplify

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-11-30 11:02:58 +00:00
renovate[bot] 64ac770c56 Update dependency dev.chrisbanes.compose:compose-bom to v2023.12.00-alpha03 2023-11-30 09:04:05 +00:00
FooIbar 557a5599e2 Make release build profileable 2023-11-30 16:47:41 +08:00
FooIbar ffd24015ac Update renovate.json
skip-checks: true
2023-11-30 13:19:25 +08:00
renovate[bot] 5eb1f12113 Update dependency androidx.work:work-runtime-ktx to v2.9.0 2023-11-30 04:48:06 +00:00
renovate[bot] 8425fed5a7 Update plugin spotless to v6.23.2 2023-11-30 03:01:33 +00:00
GG Bond 1e34aa4d9a Enable picker mode
Seems fixed by https://github.com/androidx/androidx/commit/4cf31c1a2c2a1f65fcb842490391c8a91fe44a84
2023-11-30 11:01:11 +08:00
GG Bond f383594050 Update snapshot 2023-11-30 11:01:11 +08:00
FooIbar 086db7572d Fix FAB not collapsing with no selected items 2023-11-30 09:28:26 +08:00
FooIbar 082e05584a Fix FAB animation 2023-11-30 09:28:14 +08:00
renovate[bot] fb20f899e2 Update dependency androidx.compose.ui:ui-text-android to v1.6.0-beta02 2023-11-29 22:58:17 +00:00
renovate[bot] c0c07ff2cf Update androidx.room to v2.6.1 2023-11-29 18:15:46 +00:00
renovate[bot] 2f67b741ff Update actions/setup-java action to v4 2023-11-29 16:26:11 +00:00
renovate[bot] 5a96048051 Update dependency gradle to v8.5 2023-11-29 14:58:30 +00:00
FooIbar 5d8b38ecf9 Fix HistoryScreen tip padding 2023-11-29 22:56:38 +08:00
FooIbar 2941ac8285 Add error tip for local favorites 2023-11-29 22:48:55 +08:00
FooIbar 07e316d9a9 Refine DownloadsScreen (#392)
* Fix downloads reorder and delete

* Fix download label edit and delete

* Fix changing download label
2023-11-29 14:15:37 +00:00
GG Bond 5bd9fa3a62 Drop compose viewbinding dependency 2023-11-29 18:25:23 +08:00
GG Bond 0b5da2b2dd Drop explicitly fragment dependency 2023-11-29 18:25:23 +08:00
GG Bond 714a8ccf65 Drop swiperefreshlayout dependency 2023-11-29 18:25:23 +08:00
GG Bond 5c731cc6b2 Drop rv selection dependency 2023-11-29 18:25:23 +08:00
GG Bond eda8115fe5 Revert "Drop fixed workaround"
Revert reason: BTF2 still not fixed
This reverts commit 3c988c63f244f0ec1bf027cc84d49c3e1c404aba.
2023-11-29 18:25:23 +08:00
GG Bond cf0a408b90 Drop fixed workaround 2023-11-29 18:25:23 +08:00
GG Bond f9786377a5 Update compose snapshot, new PullToRefreshContainer style 2023-11-29 18:25:23 +08:00
renovate[bot] 93f234b16d Update dependency com.google.devtools.ksp to v2.0.0-Beta1-1.0.15 2023-11-29 03:39:16 +00:00
GG Bond afb986937b Defer searchBarOffsetY read to minimize recompose scope 2023-11-29 11:38:54 +08:00
Vladimir DostoyevskiyandGG Bond 61fecaffe1 Workaround Infinity recompose (#387)
* Workaround Infinity recompose

* Observe all Snapshot apply that may trigger recompose when debug mode

* Continue to fix things up

* Fix things up

* Fix things up

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-11-28 17:03:37 +00:00
FooIbar 6f87afc1bf Fix navigation drawer padding 2023-11-28 23:35:43 +08:00
FooIbar 2e5950a1a8 Disable haptic feedback if ticks are too dense 2023-11-28 18:01:44 +08:00
FooIbar 4bf0c4ecec Minimize unnecessary network requests 2023-11-28 17:41:45 +08:00
FooIbar 3a8dbcdb32 Disallow empty gallery list result
It has to be a parse error
2023-11-28 16:10:22 +08:00
GG Bond 1f3546f3cc Fix remove fav 2023-11-28 15:12:17 +08:00
GG Bond f864d65d98 Don't collect If isInFavScene 2023-11-28 15:12:17 +08:00
GG Bond 3479498c6c optimize detailScreen state collect 2023-11-28 14:53:26 +08:00
GG Bond ac319330be Reduce allocation 2023-11-28 14:53:26 +08:00
GG Bond 9b3595e45f Reduce allocation 2023-11-28 14:53:26 +08:00
GG Bond d855858543 Refactor DownloadManager NotifyFlow 2023-11-28 14:53:26 +08:00
GG Bond ada52f1687 Reduce allocation 2023-11-28 13:42:59 +08:00
GG Bond ef68374f52 Reduce allocation on anim path 2023-11-28 13:42:59 +08:00
Vladimir DostoyevskiyandGG Bond 0bf8049021 Reduce allocation for getCategoryColor (#379)
* Reduce allocation for getCategoryColor

* refine

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-11-28 05:09:12 +00:00
GG Bond 3cff3f837a Reduce allocation in NavDrawer 2023-11-28 12:25:22 +08:00
GG Bond ca94a63762 Drop unused SimpleRatingView 2023-11-28 12:25:22 +08:00
GG Bond 81b8bc9029 Fixup 2023-11-28 12:25:22 +08:00
GG Bond 9d38520b85 Rename to *Screen 2023-11-28 12:25:22 +08:00
FooIbar 800e09adad Use relative links in READMEs 2023-11-28 11:40:30 +08:00
FooIbar 30d6c11220 Move showDatePicker to CommonOperations.kt 2023-11-28 01:36:55 +08:00
FooIbar e465356df8 Use large heap on API < 26 2023-11-28 00:52:18 +08:00
FooIbar ff8e1b7d28 Fix downloads and favorites selection 2023-11-27 23:34:01 +08:00
GG Bond 175e1d7df9 Convert TimePicker to use a dialogState 2023-11-27 23:05:00 +08:00
FooIbar 271c1b1390 Fix downloads filter and search 2023-11-27 23:02:24 +08:00
GG Bond a128356fc4 Cleanup unused 2023-11-27 22:33:16 +08:00
FooIbar 2ed957b5f2 Compose DatePicker 2023-11-27 22:12:01 +08:00
FooIbar 85bb9b9f97 Fix crash when TextField with focus is scaled to 0 2023-11-27 21:18:03 +08:00
GG Bond 050b5c0451 Kill EditTextDialogBuilder 2023-11-27 21:10:41 +08:00
GG Bond a14df3fc0c Fix bigicon tint in nightmode 2023-11-27 21:01:17 +08:00
GG Bond 939136476e Fix tip 2023-11-27 20:19:59 +08:00
GG Bond 7e2af653ee Cleanup unused 2023-11-27 20:19:59 +08:00
GG Bond d357a55307 Cleanup unused 2023-11-27 20:19:59 +08:00
GG Bond 23509ac2c9 Fix activity transition style 2023-11-27 20:19:59 +08:00
FooIbar 118a4f75c0 Fix crash info collector 2023-11-27 19:56:47 +08:00
renovate[bot] 2b8a7fafe6 Update plugin spotless to v6.23.0 2023-11-27 08:09:14 +00:00
Vladimir DostoyevskiyandGG Bond 10eda7e5f3 Introduce LockDrawerHandle (#360)
* Introduce LockDrawerHandle

* refine

* Ensure SearchBar PredictiveBackHandler is latest when enabled

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-11-27 08:08:54 +00:00
FooIbar c23cadec8a Cleanup snackbar usage
`SnackbarDuration.Short` is already longer than `Snackbar.LENGTH_LONG`
2023-11-27 15:16:11 +08:00
FooIbar 0114081a2b Fix search bar scroll-away offset 2023-11-27 14:14:59 +08:00
GG Bond 2611601fb9 Optional Predictive NavAnim 2023-11-27 13:56:36 +08:00
FooIbar 4194087f0b Fix GalleryInfoBottomSheet content padding 2023-11-27 13:27:42 +08:00
renovate[bot] 673adc41ba Update dependency org.jsoup:jsoup to v1.17.1 2023-11-27 03:09:59 +00:00
FooIbar b195ae25a8 Fix LazyList items jumping during scroll
https://issuetracker.google.com/issues/312025159
2023-11-27 02:19:22 +08:00
GG Bond 556c49fbfd Check meteredNetworkWaring 2023-11-27 00:37:19 +08:00
GG Bond 358a5249a4 Cleanup 2023-11-27 00:37:19 +08:00
GG Bond d5b837e6af CheckClipboard within Composables 2023-11-27 00:37:19 +08:00
GG Bond 61100d0c83 Upgrade lifecycle for new compose utils 2023-11-27 00:37:19 +08:00
GG Bond 8473ca9fff Observe Intent within Composables 2023-11-27 00:37:19 +08:00
FooIbar 37a2227c5c Fix OOB in DownloadsScreen 2023-11-27 00:16:05 +08:00
Vladimir DostoyevskiyandGG Bond d6a4ab7471 CompositionLocal Sheet State (#351)
* CompositionLocal Sheet State

* Fix typo

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-11-26 15:43:30 +00:00
GG Bond 6291288c42 Lowering default navAnim duration to 500ms 2023-11-26 23:25:22 +08:00
Vladimir DostoyevskiyandGG Bond 6c926419e9 Compose Era!!! (#346)
* Initial Kill Fragment!

* Cleanup

* Cleanup

* Convert to use Compose ModalBottomSheet

* Ignore insets

* Navigate

* Fixes

* Cleanup

* Fix navToWebview

* Fix navToWebview

* showtip

* Refine

* Fixes

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-11-26 15:02:03 +00:00
FooIbar f292f31ac1 Refine DownloadsScreen icons (#349)
* Convert big download icon to ImageVector

* Tweak download label icons
2023-11-26 14:54:29 +00:00
FooIbar 86a49f764e Add haptic feedback for drag and drop in Downloads 2023-11-26 22:32:04 +08:00
FooIbar 5dad21f4ea Fix download label drag and drop 2023-11-26 21:57:21 +08:00
FooIbar a4e9291bf9 Implement LoadStateIndicator 2023-11-26 21:08:18 +08:00
Vladimir DostoyevskiyandGG Bond 2a667e62f4 Compose DownloadScreen (#343)
* Initial Compose DownloadScreen

* Cleanup namespace

* Reorderable

* Reorderable

* Fixes

* Fix layout

* Fixes

* State labelList

* Fixes

* SwipeToDismiss

* RenameLabel

* Rewrite renameLabel to notify

* Fix name shadow

* Fixes

* Fixes

* SelectMode

* Drop unused

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-11-26 20:12:01 +08:00
FooIbar 6bd8063976 Replace thumb size setting with thumb column count 2023-11-26 15:09:43 +08:00
FooIbar 0aba5907b0 Fix title overflow 2023-11-26 01:05:57 +08:00
FooIbar 32182a7791 Fix keyword and title not updating 2023-11-26 01:05:57 +08:00
Vladimir DostoyevskiyandGG Bond 198960f93c Compose DownloadCard (#339)
* Cleanup res

* Compose DownloadCard

* Refine

* Fix CI

* Fixes

* Fix recompose

* Fix textStyle

* Fixes

* Fixes

* Restore design specs and clean up

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-11-25 16:46:07 +00:00
FooIbar 618f14a732 Reset search bar offset on loading 2023-11-26 00:09:44 +08:00
FooIbar fec2c39364 Extract common part of GalleryList and Favorites (#338)
* Extract common part of GalleryList and Favorites

* Scroll to top after refreshing
2023-11-25 15:15:28 +00:00
FooIbar dd06d82cd9 Refine side sheet layout 2023-11-25 16:38:30 +08:00
GG Bond 4c98ee5350 Refine CheckBox dialog design 2023-11-25 15:53:36 +08:00
GG Bond 2800a7bc15 Kill CheckBoxDialogBuilder 2023-11-25 15:53:36 +08:00
FooIbar 04818e5afb Fix IME won't pop up after dismissing dialog (#334)
* Fix IME won't pop up after dismissing dialog

* Add keep rule for reflection
2023-11-25 14:17:21 +08:00
Vladimir DostoyevskiyandGG Bond 122089eb1e Predictive Back for NavHost (#335)
* Grab navigation-compose

* Initial SeekableTransitions

* Refine

* Fixes

* Fix anim again

* Easeout Predictive Back

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
2023-11-25 14:11:00 +08:00
GG Bond b991e5d70a Cleanup ActionBuilder 2023-11-24 22:21:03 +08:00
GG Bond 0d00ae207a Drawer blurs behind 2023-11-24 21:51:22 +08:00
GG Bond 9e451c18d9 Simplify GalleryInfoListItem placeholder 2023-11-24 21:32:36 +08:00
GG Bond 71ebd99115 Stateful listThumbSize 2023-11-24 21:22:08 +08:00
GG Bond 116dbead4b Stateful thumbSizeDp 2023-11-24 21:22:08 +08:00
FooIbar ef8adb00f4 Fix invisible FAB being clickable 2023-11-24 20:47:20 +08:00
FooIbar 7d74781f96 Collect debug info when dumping logs 2023-11-24 20:17:12 +08:00
Vladimir Dostoyevskiyandskymkmk b7627dc576 Fix GalleryList to use FastScrollLazyVerticalGrid when listMode (#324)
* Fix GalleryList to use FastScrollLazyVerticalGrid when listMode

* Fix recompose

* Initial PrefDelegate -> State<T>, MutableState<T>

* Migrate

* Optimize

---------

Co-authored-by: skymkmk <whatever@tagkong.com>
2023-11-24 10:44:38 +00:00
FooIbar cf42acd65c Lock side drawer as well 2023-11-24 18:31:34 +08:00
skymkmk 4163c895df Tweak SearchTab Design 2023-11-24 17:44:06 +08:00
FooIbar 63ddd5ea89 Fix side sheet padding 2023-11-24 17:17:33 +08:00
FooIbar 216dfded4b Fix downloads FAB animation 2023-11-24 16:50:29 +08:00
FooIbar 08b5eac6bd Restore history item delete animation 2023-11-24 16:14:53 +08:00
FooIbar 6e541be6ca Restore quick search item drag and drop animation 2023-11-24 15:48:47 +08:00
FooIbar 17a9c02f96 [Marshmallow] Fix OOM when decoding images 2023-11-24 13:22:23 +08:00
Vladimir Dostoyevskiyandskymkmk ed640ab00e Enhanced swipe-edge-oriented PredictiveBack (#317)
* Enhanced swipe-edge-oriented PredictiveBack

Following MDC NavigationDrawer design specs

* Cleanups

---------

Co-authored-by: skymkmk <whatever@tagkong.com>
2023-11-23 18:01:58 +00:00
skymkmk d69df5f7ab PredictiveBack for ModalNavigationDrawer, SideDrawer 2023-11-24 00:47:36 +08:00
FooIbar 6f6461e855 Fix disowned gallery uploader parsing 2023-11-24 00:06:59 +08:00
skymkmk 682d959688 All Easing for animateFloatMergePredictiveBackAsState 2023-11-23 23:31:02 +08:00
skymkmk 179686de16 Reimplement animateFloatMergePredictiveBackAsState 2023-11-23 23:31:02 +08:00
FooIbar 32e35622bd Kotlin 2.0.0-Beta1 (#312)
* Kotlin 2.0.0-Beta1

* Cleanup
2023-11-23 14:21:43 +00:00
c9c06202bb Compose GalleryListScene (#301)
* Initial Migrate

* Cleanup

* Search

* Refine

* Initial searchLayout

* Cleanup

* Margins

* Animated State

* Animated State

* Fixes

* ShowSearchFab

* Refine

* Log

* Refine naming

* Update

* Scroll to hide FAB

* Cleanups

* Initial side sheet

* Refine side sheet

* Refine layout

* Refine transition

* Refine padding

* Refine ProvideSideSheetContent to handle multiple screen on composition

* Let sidesheet container padding vec insets

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
Co-authored-by: skymkmk <admin@skymkmk.com>
2023-11-23 14:14:02 +00:00
FooIbar 42d17621f9 Fix advanced search not working 2023-11-23 21:42:55 +08:00
renovate[bot] 629044c241 Update dependency dev.chrisbanes.compose:compose-bom to v2023.12.00-alpha02 2023-11-23 12:48:58 +00:00
FooIbar 9be19489a4 Fix crash when visiting newer version galleries 2023-11-23 20:48:32 +08:00
FooIbar cde4f9e405 Fix refresh state 2023-11-23 20:48:21 +08:00
FooIbar 16fddcb377 Refine 2023-11-23 20:48:21 +08:00
FooIbar 35cff4647a Fix favorites refresh 2023-11-23 20:48:21 +08:00
renovate[bot] 80ad749fc2 Update compose.destinations to v1.9.55 2023-11-21 14:00:36 +00:00
FooIbar 1a1d444b20 Restore common actions in comment field 2023-11-21 22:00:18 +08:00
FooIbar bb2d500067 Refine FavouritesScreen layout 2023-11-21 21:53:44 +08:00
FooIbar c3c7e88c15 Fix pull request builds being pushed to users 2023-11-21 17:17:07 +08:00
FooIbar 8dc3b1f376 Restore secondary FAB fade 2023-11-21 16:53:49 +08:00
Lokesh Gidra b49a80bb35 Cleanup unused res 2023-11-20 21:45:40 +08:00
Lokesh Gidra 21bfd4dd2d Cleanup unused res 2023-11-20 21:45:40 +08:00
Lokesh Gidra 3551ed7c7a Refine ActionBuilder dsl 2023-11-20 21:45:40 +08:00
FooIbar 5f8ab98e9e Tweak settings transition animations 2023-11-20 17:48:56 +08:00
FooIbar 8c6b2f5366 Fix side sheet padding 2023-11-20 16:10:57 +08:00
Vladimir DostoyevskiyandLokesh Gidra bac60fbec8 Compose FavouriteScreen (#295)
* Restore FavouriteScene SideSheet dragToOpen

* Initial move to Compose FavScene

* Refine state

* Refine state

* Introduce ProvideSideSheetContent with extra checkers

* Make sideSheet activity private

* Dont allow provide a null AssistContent

* Restore trailingIcon to openSideSheet

* Refine

* Cleanup

* Implement searchBarConnection

* Reduce allocation for animation

* Initial compose FabLayout API

* Restore showGoToDialog

* Refine

* Refine

* Refine

* PullTORefresh

* PullTORefresh

* Refine Dsl

* Insetless placeholder

* let pullToRefresh use insetless padding as well

* Support drawer gestures

* Fix tip color and ripple

* Make pullToRefresh optional

* Composite container color

* Fix padding

* Fix misadd

* Fix gesture

* Fixes

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-20 07:09:35 +00:00
FooIbar 4eed03ebed Add configuration for lock bot 2023-11-20 11:37:38 +08:00
Lokesh Gidra e33295d0a6 Don't allow RecyclerView intercept ACTION_CANCEL TouchEvent as well 2023-11-19 16:22:21 +08:00
Vladimir DostoyevskiyandLokesh Gidra 07a495c499 Reimplement ModalNavigationDrawer to allow HistoryScreen gesture (#293)
* Reimplement ModalNavigationDrawer to allow HistoryScreen gesture

* Fix RTL, fix gesture disabled

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-19 07:53:54 +00:00
e69eba6023 Compose FabLayout (#290)
* ComposeFabLayout with Predictive Back

* Refine

* Fix padding

* Fix anim

* Fix scrim

* Cleanup

* Convert FavoritesScene to use ComposeFabLayout

* Convert DownloadsScene to use ComposeFabLayout

* Cleanup unused code

* Rename ComposeFabLayout to FabLayout

* Refine

* Refine

* Intercept MotionEvent.ACTION_CANCEL for rv have SelectionTracker attached

* Revert to old icons

* Revert FAB orders

* Cleanup resources

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
2023-11-19 09:21:52 +08:00
Lokesh Gidra 8be6ad84fc Get rid of IntList, LongList 2023-11-17 23:54:19 +08:00
Lokesh Gidra 59e46aeeea Navigate with GalleryDetailScreenArgs directly 2023-11-17 23:54:19 +08:00
renovate[bot] 4404531171 Update dependency com.android.application to v8.1.4 2023-11-16 19:51:03 +00:00
FooIbar 99db84fdd0 Cleanup code 2023-11-17 00:10:44 +08:00
FooIbar aa50b69754 Add rotate inertia for favSelectDialog 2023-11-16 23:15:02 +08:00
FooIbar fa2933df7c Workaround for BTF2 cursor not showing 2023-11-16 23:07:29 +08:00
FooIbar c51a26b9b7 Close drawer before navigating to another activity
Fix crash caused by activity recreate when it's invisible
2023-11-16 19:00:58 +08:00
FooIbar 9859a1eaac Fix amoled dark mode 2023-11-16 19:00:43 +08:00
Lokesh Gidra cd3858ace8 Rotatable fav select dialog 2023-11-16 14:27:51 +08:00
FooIbar 0c54e8b0b3 Allow setting search bar title 2023-11-16 12:47:33 +08:00
Lokesh Gidra e32a7b61b6 Workaround IME won't show again once hidden 2023-11-16 11:58:23 +08:00
FooIbar ea2977268b Follow upstream API changes 2023-11-16 11:21:20 +08:00
Vladimir Dostoyevskiy 6b529bcb18 Upgrade compose bom (#277)
* Upgrade compose bom

* Update settings.gradle.kts
2023-11-16 02:32:14 +00:00
FooIbar 941e7d18f3 Implement pull to refresh in GalleryCommentsScreen (#272)
* Use AndroidX snapshot builds

* Implement pull to refresh in GalleryCommentsScreen

* Don't use snapshot builds for other libraries
2023-11-16 09:34:29 +08:00
FooIbar d0fe9fac8c Fix crash when applying style to comment 2023-11-16 09:33:58 +08:00
FooIbar 8f37c5ba25 Fix favButton text not updating in gallery detail 2023-11-16 09:33:24 +08:00
renovate[bot] b63b36e6d2 Update dependency androidx.browser:browser to v1.7.0 2023-11-15 21:52:38 +00:00
renovate[bot] 081357cbb4 Update dependency androidx.activity:activity-compose to v1.8.1 2023-11-15 18:15:44 +00:00
renovate[bot] 0f7aa5f9ea Update kotlinx.serialization to v1.6.1 2023-11-15 16:07:25 +00:00
Lokesh Gidra a565b428e8 Apply blur to Compose Dialog 2023-11-15 21:05:45 +08:00
FooIbar 71f4df6f05 Fix download dialog checkbox ripple 2023-11-15 20:41:56 +08:00
FooIbar 5da2f91e6a Fix comment EditText height 2023-11-15 20:32:52 +08:00
FooIbar a6e9867596 Clear previous comment if it was in edit mode 2023-11-15 20:32:52 +08:00
FooIbar 9e1cff014d Minor cleanup 2023-11-15 20:32:52 +08:00
Lokesh Gidra 9c06f10d43 Drop explicitly androidx collection dependency
Since compose runtime depends 1.4.0a1 now
2023-11-15 20:11:56 +08:00
Lokesh Gidra 3f11df1dc6 Cleanup unused 2023-11-15 20:11:56 +08:00
Lokesh Gidra 6dd4505cec Convert download dialog to coroutine dialog 2023-11-15 20:11:56 +08:00
Lokesh Gidra 8faabd7759 Cleanup unused res 2023-11-15 19:03:40 +08:00
FooIbar 288e56d7e0 Fix search bar width 2023-11-15 15:23:15 +08:00
Vladimir DostoyevskiyandLokesh Gidra 7bcddb5f51 Initial limited support for BBCode editor (#260)
* Initial support edit

* Override LocalTextToolbar to reimpl BBCode editor: groundwork

* Impl

* Hacky

* Keep spans when editing

* Fix bold

* Fixes

* Initial AnnoStr to BBCode

* Implement edit with annotatedString

* Cleanup

* Cleanup

* Hack

* Some necessary comment

* Make url span a noop now

* Cleanup

* Cleanup

* Fix annotatedStr concat

* Fix toBBCode

* Clear focus as well

* Support Overlapped SpanStyle to BBCode

* Refine

* Cleanup code

* Fix algo

* Fix conflict

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-15 13:03:18 +08:00
FooIbar 7b0bd309e4 Fix light splash screen in dark theme on API >= 31 2023-11-15 01:19:53 +08:00
FooIbar 237938b1f0 Check temporary IP ban and refine error translation (#262)
* Check temporary IP ban and refine error translation

* Fix lint
2023-11-14 10:27:45 +00:00
FooIbar 07009f8bd6 Expose searchFieldState as workaround
Until we migrate GalleryListScene to compose
2023-11-14 16:09:23 +08:00
FooIbar f7ef768c8f Set default filter mode 2023-11-14 00:37:15 +08:00
FooIbar d1c3c9a37a Refine modifyFavorites 2023-11-13 18:23:58 +08:00
Lokesh Gidra 78f15860e0 Check sadpanda in cronet path 2023-11-13 18:23:58 +08:00
Lokesh Gidra 6b11773da9 ModifyFavoritesRange concurrently
Order?
2023-11-13 18:23:58 +08:00
Lokesh Gidra ed26cb8f35 Move modifyFavorites to use cronet 2023-11-13 18:23:58 +08:00
Lokesh Gidra 337f27dc38 Convert ghRequest to use stdlib Base64 impl 2023-11-13 18:23:58 +08:00
FooIbar 97fab45877 Avoid unnecessary overhead 2023-11-13 16:49:01 +08:00
FooIbar 8e8749ed6e Fix crash when initial composition hasn't been performed 2023-11-13 15:04:02 +08:00
Lokesh Gidra a0e999576e Fix comment textfield cursor color 2023-11-13 12:07:44 +08:00
Lokesh Gidra b29802c260 Keep GalleryDetail comments in sync 2023-11-13 11:32:24 +08:00
Lokesh Gidra 665e5a3d39 Immutable GalleryComment 2023-11-13 11:32:24 +08:00
Lokesh Gidra c2a2dafb47 Resolve #251 2023-11-13 11:32:24 +08:00
Lokesh Gidra 1d12a16e3d Resolve #250 2023-11-13 11:32:24 +08:00
Vladimir DostoyevskiyandLokesh Gidra f3fe5f0a75 Support cronetRequest with body (#248)
* Initial Add RequestBody support for cronetRequest

* Convert modifyFavorites to use cronet

* Code refactor

* Avoid some unnecessary copy

* Suppress NewApi for all managed usage

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-12 15:45:02 +00:00
Vladimir DostoyevskiyandLokesh Gidra 3bad1d06f4 Migrate more to cronet directly (#247)
* Convert GalleryListParser to parse a DirectByteBuffer

* Convert FavoritesParser to parse a DirectByteBuffer

* Limit buffer size

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-12 19:29:31 +08:00
FooIbar 70f2bfb760 Fix fab animation 2023-11-12 16:06:36 +08:00
Lokesh Gidra 9e86d42972 Predictive Back for Compose SearchBar
https://android-review.googlesource.com/c/platform/frameworks/support/+/2721593
2023-11-12 15:41:41 +08:00
FooIbar c8ade4d750 Fix recyclerview item disappearing 2023-11-12 01:39:16 +08:00
FooIbar 11b7bbd7ef Fix search bar text missing 2023-11-11 23:51:48 +08:00
Lokesh Gidra 513bce51ea Avoid unnecessary recompose in ReaderProgressIndicator 2023-11-11 22:57:04 +08:00
FooIbar f2c8c5d66d Mitigate OOM for API < 26 2023-11-11 22:44:55 +08:00
4e08f4f962 Compose Searchbar (#237)
* Migrate to Compose SearchBar

* Fix searchbar padding

* Fix content padding

* Implement scroll away behaviour

* Reset searchBarOffsetY when expand

* Fix multiple listener

* Fix cursor position

* Fix spotless

---------

Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-11 13:03:11 +00:00
FooIbar a9bd55dff5 Restore fab rotation when showing and hiding (#235)
* Restore fab rotation when showing and hiding

* Make it rotate in the same direction
2023-11-10 12:29:01 +00:00
FooIbar 5cec825dbe Clean up duplicate imports 2023-11-10 18:05:20 +08:00
FooIbar 14c5b28cdc Refactor visibility animation for FabLayout 2023-11-10 16:41:42 +08:00
nullArrayListandLokesh Gidra 7693b11e0a Fix wrong SideSheetDialog usage (#231)
* Fix wrong SideSheetDialog usage

* Do not assume fragmentView first composition earlier than drawerView's

Use MainActivity composition instead

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-10 05:47:14 +00:00
FooIbar f9cb49e4d3 Only show commit subjects in changelog 2023-11-10 02:14:56 +08:00
Lokesh Gidra 892a614246 Drop overloadaggressively
https://r8.googlesource.com/r8/+/04d49b152b624a437db46e17410bb1cb1e4dd809
2023-11-09 22:34:31 +08:00
FooIbar 4a482d996e Reduce category min width for small screen devices
Tested-on: 4.65" 720x1280
2023-11-09 20:19:41 +08:00
Lokesh Gidra af9baa8ce4 Convert to use enableEdgeToEdge 2023-11-09 19:39:48 +08:00
Lokesh Gidra 39a11525a9 Rename EMPTY_GALLERY_COMMENT_LIST 2023-11-09 18:20:00 +08:00
Lokesh Gidra 2337dda4eb Remove EMPTY_GALLERY_TAG_GROUP_ARRAY
There is no reason to keep it
emptyList() always return a singleton
2023-11-09 18:20:00 +08:00
Lokesh Gidra c0a610a02b Remove GalleryApiInfo
No user now
2023-11-09 18:20:00 +08:00
Lokesh Gidra 85f7b54719 Convert GalleryComment to data class
Generate equals method for compose
2023-11-09 18:20:00 +08:00
Lokesh Gidra e0aea2fedf Convert GalleryTagGroup to data class
Generate equals method for compose
2023-11-09 18:20:00 +08:00
Lokesh Gidra ee83dbb877 Convert GalleryDetail to data class
Generate equals method for compose
2023-11-09 18:20:00 +08:00
Lokesh Gidra 1de4bc11eb Keep primary constructor only 2023-11-09 18:20:00 +08:00
FooIbar 04a7656afa Fix gallery detail refresh 2023-11-09 16:36:14 +08:00
nullArrayListandLokesh Gidra 77ec7e76e9 Refactor out animateFloatMergePredictiveBackAsState (#221)
* Refactor out animateFloatMergePredictiveBackAsState

* Revert EditTextHeight to 80dp

* Fix

* Fix animation when content too long

* Fix double navbar padding

* Fix lazycolumn jumpping

* Fix padding

* Fix padding again

* Refine

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-09 05:24:42 +00:00
FooIbar 9ca7be081a Built-in proguard rules are obsolete, use our own
Fix regression of #216
2023-11-09 10:30:27 +08:00
FooIbar fb36346f0b Fix variable shadowing (regression of #218) 2023-11-09 09:35:46 +08:00
FooIbar 95679f3fb9 Append skip H@H key to original image url on retry
Since original images may be served by H@H now
2023-11-09 01:04:49 +08:00
nullArrayListandLokesh Gidra addd4124c1 Initial Predictive back animation support for commentFragment Edittext (#217)
* Initial Predictive back animation support for commentFragment Edittext

* Fix

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-08 14:47:24 +00:00
FooIbar 74b79d9faa Use built-in proguard rules and @Keep annotation
`-keepattributes LineNumberTable,SourceFile` is not needed since R8 v8.2
APK size is increased by 1.7% though
2023-11-08 22:45:24 +08:00
nullArrayListandLokesh Gidra 9daa05ef02 Enable Predictive Back (#214)
* Enable Predictive Back

Tested with UP1A.231005.007, no bugs

* Fix Wrong SDK behaviour

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-08 19:06:41 +08:00
FooIbar 5ee884e0a2 Update thanks and Traditional Chinese README (#213)
* Update thanks and Traditional Chinese README

* Credit to Compose Destinations
2023-11-08 10:12:33 +00:00
FooIbar dba7f4e6c0 Fix inconsistent refresh state
Don't save it as `onRestoreInstanceState` will be called after loading
state has settled
2023-11-08 16:55:30 +08:00
FooIbar 8221514805 Update README.md 2023-11-08 16:54:00 +08:00
FooIbar 2ae3effc0e Move updates check to MainActivity 2023-11-08 16:09:45 +08:00
Lokesh Gidra 5b450ff18f Fix not filter all comments 2023-11-08 12:33:26 +08:00
Lokesh Gidra 92be6e9340 Explicitly reallocate lambda by key, fix download not work due to composer skip local fun reallocation 2023-11-08 12:33:26 +08:00
Lokesh Gidra 8884aeec11 Fix filter comment 2023-11-08 12:33:26 +08:00
Lokesh Gidra 8e15eda4e3 Fix navigate on IO thread crash 2023-11-08 12:33:26 +08:00
Lokesh Gidra 0dbf220655 Enable some compose compiler optimization 2023-11-08 12:33:26 +08:00
renovate[bot] fe69d59ef1 Update dependency com.android.application to v8.1.3 2023-11-07 23:30:33 +00:00
renovate[bot] 1e1f8350b5 Update dependency androidx.compose.compiler:compiler to v1.5.4 2023-11-07 19:07:38 +00:00
FooIbar afa2d03b56 Fix local favorites list jumping (#206)
* Revert "Fix local favorites scroll state loss"

This reverts commit c4be269f

* Enable placeholders as workaround

* Remove TODO

* Bump minimum fast scroller handler height to 48 dp

Make it consistent with `VerticalFastScroller`
2023-11-07 14:56:46 +00:00
FooIbar 1293d7fe28 Fix history not updated 2023-11-07 18:22:31 +08:00
renovate[bot] 62e879f2de Update dependency io.ktor:ktor-io-jvm to v2.3.6 2023-11-07 09:13:33 +00:00
Lokesh Gidra a771994f6f Introduce ActionBuilder 2023-11-07 17:13:15 +08:00
Lokesh Gidra 8c2ba051a0 Various cleanup for GalleryDetailScene 2023-11-07 17:13:15 +08:00
FooIbar ae2f289122 Make TorrentResult immutable and do some cleanup 2023-11-07 16:35:05 +08:00
nullArrayListandLokesh Gidra 703d63d106 Fragmentless GalleryDetailScene (#202)
* Initial fragmentless port

* Compile it

* Refine

* Refine

* All

* Fix

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-07 15:39:59 +08:00
FooIbar 2d7b024293 Fix unused resources inspection not working in AS (#200)
* Fix unused resources inspection not working in AS

* Remove unused resources

* Update CI

* Fix CI

* Add baseline for lint

* Test CI
2023-11-06 22:18:56 +08:00
Lokesh Gidra 26906f6709 Fix GalleryDetailScene comment ellipsize 2023-11-06 18:29:48 +08:00
Lokesh Gidra e9ec97403f Fix with missing ComposeWithMD3 2023-11-06 18:29:48 +08:00
Lokesh Gidra 34fb72707b Drop BaseToolbarScene since no user now 2023-11-06 18:29:48 +08:00
Lokesh Gidra fb7eb64c3a Drop unused xml layout 2023-11-06 18:29:48 +08:00
Lokesh Gidra ef65720999 Drop leakcanary since we will totally migrate to compose 2023-11-06 18:29:48 +08:00
Lokesh Gidra 3bc71d6798 Fix comment display limits 2023-11-06 18:29:48 +08:00
FooIbar c4be269fb7 Fix local favorites scroll state loss 2023-11-06 16:18:25 +08:00
nullArrayListandLokesh Gidra 1238edfe2b Compose GalleryCommentsScreen (#187)
* Initial Stub Compose GalleryCommentsScreen

* Initial Fab and edittext

* Refine comment edittext

* Support load more comments

* Animated loading state

* Refine, cleanup migrated things

* Initial comment dialog

* Refine

* Introduce buildAction dsl

* Impl voteComment

* Refine and cleanup migrated things

* Impl check vote status

* Refine

* Refine

* Padding a ime

* Support send commnet

* Cleanup all the code

* Let edittext and sendButton padding a navbar

* Let LazyColumn padding a ime

* Let LazyColumn contentPadding a edittext and fab

* Handle unresolved span with onCardClick

* Modify how LinkifyTextView works

* Adjust padding

* Adjust comment textstyle

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-06 03:19:59 +00:00
FooIbar 747fd23b96 Add experimental option to tweak swipe sensitivity 2023-11-05 10:57:20 +08:00
FooIbar 022f7a3ef1 Fix crash when clicking retry button in favorites 2023-11-04 23:59:40 +08:00
FooIbar e0cad8600c Fix crash caused by drawer state deserialization 2023-11-04 18:50:52 +08:00
renovate[bot] 9510ebbc31 Update dependency com.mikepenz.aboutlibraries.plugin to v10.9.2 2023-11-04 09:31:51 +00:00
FooIbar b63f2fd393 Limit request concurrency 2023-11-04 15:32:08 +08:00
FooIbar 95a0e9f03b Cleanup crash info collector 2023-11-04 14:25:01 +08:00
renovate[bot] 8265dd7f02 Update dependency com.google.devtools.ksp to v1.9.20-1.0.14 2023-11-03 03:58:14 +00:00
FooIbar 67feb19cab Fix unable to open drawer after double clicked 2023-11-03 00:46:10 +08:00
FooIbar 614e847de0 Fix incorrect favorite states in search results 2023-11-02 23:29:41 +08:00
renovate[bot] c2ff4b6a0b Update dependency com.android.tools:desugar_jdk_libs_nio to v2.0.4 2023-11-02 10:08:47 +00:00
FooIbar b273f1039c [Reader] Hide slider ticks if they are too dense 2023-11-02 18:08:31 +08:00
Lokesh Gidra bea0c6a1f0 Simplify with Fragment.ComposeWithMD3 2023-11-02 16:46:35 +08:00
Lokesh Gidra dcfaa361ec Extract a fragmentless ProgressScreen
ProgressScene.KEY_ACTION is always ProgressScene.ACTION_GALLERY_TOKEN, thus remove it
2023-11-02 16:03:03 +08:00
Lokesh Gidra 9904e166d2 Extract a fragmentless HistoryScreen 2023-11-02 15:13:52 +08:00
Lokesh Gidra 5f02769012 Extract a fragmentless GalleryPreviewScreen 2023-11-02 13:42:21 +08:00
nullArrayListandLokesh Gidra 19e5636dd2 Migrate ConfigureActivity to compose-destinations (#178)
* Add compose-destinations lib, prepare to drop fragment

* Move ConfigureActivity to compose destinations

* Restore navAnims

---------

Co-authored-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-02 11:40:13 +08:00
renovate[bot] 9a220f89d9 Update androidx.navigation to v2.7.5 2023-11-01 18:01:33 +00:00
FooIbar 1c76ec813c Fix crash when clicking download notification 2023-11-02 02:01:16 +08:00
FooIbar d36c189430 Update xz to v5.4.5 2023-11-01 23:29:48 +08:00
FooIbar 61e2ace5c8 Fix insets on API < 29 2023-11-01 23:17:46 +08:00
Lokesh Gidra d6a3f21754 Fix a weird NPE 2023-11-01 22:12:07 +08:00
Lokesh Gidra 4e54d7f66f Refine FavouriteStatusRouter observable stateFlow 2023-11-01 22:12:07 +08:00
Lokesh Gidra f2dff038cd Global, CompositionLocal DialogState
Signed-off-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-01 19:37:22 +08:00
Lokesh Gidra a7b30640e0 Drop DrawerView
Signed-off-by: Lokesh Gidra <lokeshgidra@google.com>
2023-11-01 19:37:22 +08:00
FooIbar 45048b1f46 [Marshmallow] Fix crash when saving images 2023-11-01 16:26:06 +08:00
FooIbar 498129314a Refine storage permission request 2023-11-01 15:42:55 +08:00
Officer 3ee4b4a61e Cleanup ReaderActivity & Fix activity not finish when no archive passwd available 2023-11-01 15:41:23 +08:00
FooIbar ee59c0f22e Convert ReaderActivity dialog to use a DialogState 2023-11-01 12:30:36 +08:00
FooIbar c56134cd92 Fix API results parsing error on certain galleries 2023-11-01 12:17:48 +08:00
renovate[bot] b3d1b66a06 Update dependency com.google.devtools.ksp to v1.9.20-1.0.13 2023-11-01 03:49:48 +00:00
renovate[bot] c9bb81bf63 Update kotlin to v1.9.20 2023-11-01 02:41:28 +00:00
FooIbar d176810dae Update compose compiler for kotlin 1.9.20 2023-11-01 10:38:36 +08:00
FooIbar 17bdca71b1 Reduce drawer and history item gesture sensitivity 2023-11-01 09:50:25 +08:00
FooIbar 22fd3730df Sync with upstream changes and cleanup code 2023-11-01 02:37:34 +08:00
FooIbar 23d1271949 Prepare for next iteration 2023-10-31 13:30:29 +08:00
FooIbar bf6aa3e8ae Release 1.8.11.0
Releases / Build (push) Has been cancelled
2023-10-31 12:49:53 +08:00
FooIbar 75fcfde892 Always enable gestures if drawer is open 2023-10-31 11:58:13 +08:00
FooIbar 89be27995e Fix #154 regression (#158)
* Revert "Fix crash when deleting filters"

This reverts commit 717d3717ba.

* Revert "Update dependency dev.chrisbanes.compose:compose-bom to v2023.11.00-alpha02"

This reverts commit 38c18149

Reason for revert: https://issuetracker.google.com/issues/295745063#comment57
2023-10-31 03:45:25 +00:00
FooIbar 6738dc6cdc Partially enable drawer gestures for now 2023-10-31 11:34:14 +08:00
renovate[bot] 9ff0decb98 Update dependency io.coil-kt:coil-bom to v2.5.0 2023-10-30 23:02:14 +00:00
FooIbar 717d3717ba Fix crash when deleting filters 2023-10-31 03:25:25 +08:00
FooIbar fdbca869a8 Allow hiding comments below custom threshold 2023-10-31 00:44:32 +08:00
Astesia aafa750ca3 change runnable to coroutine job, and fix fragment compose memory leak (#152) 2023-10-30 22:39:29 +08:00
Astesia afe2bac672 Fix fragment compose view memory leak (#151)
* fix fragment compose view memory leak

* chang wildcard import
2023-10-30 20:07:37 +08:00
FooIbar b6f5d72db5 Fix NPE when evicting cache but Image was recycled
But why was it recycled?
2023-10-30 18:00:37 +08:00
FooIbar f0021a7601 Fix tag filters not working in thumbnail mode 2023-10-30 17:22:31 +08:00
FooIbar ea5bfbb346 Fix ViewModel memory leak 2023-10-30 12:32:11 +08:00
FooIbar 38fbcf4457 Update issue template 2023-10-30 00:48:24 +08:00
FooIbar 7c43e8f349 Avoid uncaught exceptions crashing entire app 2023-10-30 00:30:01 +08:00
FooIbar 93df4e3dd9 Revert "Scroll to top after refreshing" (#139)
* Revert "Fix progress not showing after clicking error tip"

This reverts commit bc914566dd.

* Revert "Try to fix recyclerview disappearing after refresh (#61)"

This reverts commit f9a3c3d266.

* Revert "Scroll to top after refreshing"

This reverts commit b0896ea465.
2023-10-29 14:44:42 +00:00
fricyo f7d45a52a7 Fix a strange word 2023-10-29 21:01:21 +08:00
FooIbar 328f96d115 Fix GalleryPreviewScreen insets 2023-10-29 20:57:01 +08:00
fricyo 22338a72d8 Complete the missing translations (#131)
* Complete the missing translations

* Remove the duplicate

* Update ""settings_eh_force_eh_thumb_summary""
2023-10-29 17:18:03 +08:00
FooIbar 256dedcb7c Add note for forceEhThumb 2023-10-29 16:36:49 +08:00
FooIbar 95d2438b84 Fix state recalculation after recompose 2023-10-29 16:19:59 +08:00
FooIbar f8d27e165e Update UI state on main thread and fix weight 2023-10-29 16:19:59 +08:00
FooIbar e2f024ae29 Fix reader not loading under certain circumstances (#128)
* Revert "Fix reader bug on certain orientation settings"

This reverts commit 6e59583dea.

* Revert "Skip `buildProvider` if the activity will restart"

This reverts commit a880f801fb.

* Preserve gallery provider in view model
2023-10-29 03:24:24 +08:00
FooIbar 9c28fbeaf8 Allow custom user agent string 2023-10-28 22:34:00 +08:00
FooIbar 6458eb090a Add missing color filter modes 2023-10-28 20:39:16 +08:00
FooIbar f1ca2b6958 Add animations for category chips 2023-10-28 19:34:32 +08:00
FooIbar 60f031e7e3 Fix crash when navController is not initialized 2023-10-28 13:43:32 +08:00
FooIbar e6a44ee0e1 Clarify system requirement for Marshmallow flavor 2023-10-28 02:49:27 +08:00
FooIbar 7a3a641a07 Fix crash when copying cookies 2023-10-28 02:10:13 +08:00
FooIbar 1021efb239 Fix NPE caused by coroutine cancellation 2023-10-27 23:35:23 +08:00
FooIbar edbe6509e1 Remember last viewed toplist 2023-10-27 20:37:20 +08:00
FooIbar 44a6253bfc Sync with upstream API changes 2023-10-27 19:40:41 +08:00
FooIbar 3d6e5df71e Leave 56 dp minimum edge gap for navigation drawer 2023-10-27 13:09:11 +08:00
FooIbar 2733d2e58c Convert more icons to ImageVector 2023-10-27 11:43:47 +08:00
FooIbar 0c78673870 Fix unexpected touch scale behavior in PageViewer 2023-10-27 03:11:00 +08:00
FooIbar d618948429 Set min width and max items in each row (#99)
* Set category chip to fixed width

* Set min width and max items in each row
2023-10-26 17:54:56 +00:00
FooIbar 7584704d22 Avoid race condition when updating tag database
`updateInternal` and `issueUpdateInMemoryData` will try to create tag
database directory at the same time on first launch and one of them will
fail, causing app crash
2023-10-27 00:10:59 +08:00
FooIbar 4a4658cc54 Dispatch and apply system bars insets separately 2023-10-26 22:43:43 +08:00
FooIbar e9875f1a24 Use lazy initialization instead 2023-10-26 21:01:01 +08:00
FooIbar cc11257d80 Suppress lint 2023-10-26 20:17:28 +08:00
FooIbar 4fd64601ef Use ImageVector icons 2023-10-26 19:45:29 +08:00
FooIbar 7eea490adf Drop RikkaX 2023-10-26 19:29:57 +08:00
FooIbar b2b12f7b93 Migrate from RikkaX Insets to Insetter 2023-10-26 13:22:23 +08:00
renovate[bot] 66d026cffa Update kotlin to v1.9.20-RC2 2023-10-26 04:12:31 +00:00
FooIbar 5a15a20dd6 Update compose compiler for kotlin 1.9.20-RC2 2023-10-26 12:09:43 +08:00
renovate[bot] 70450a9725 Update dependency com.google.devtools.ksp to v1.9.20-RC2-1.0.13 2023-10-26 00:43:46 +00:00
FooIbar 470a3c6a09 Move search fab to GalleryListScene 2023-10-26 00:03:04 +08:00
FooIbar 15f8b2f15d Cleanup code 2023-10-26 00:02:20 +08:00
FooIbar 717cbe7630 Request recomposition after view gets created 2023-10-25 23:51:09 +08:00
FooIbar d34075d29d Apply ime padding to suggestion list 2023-10-25 23:51:09 +08:00
FooIbar 80d4d66fb5 Dispatch insets from compose to android view 2023-10-25 23:51:09 +08:00
FooIbar 6b20b46761 Revert "Set bar style correctly"
This reverts 216a043094.

Reason for revert: Incorrect status bar color when app's night mode
setting is different from system's
2023-10-25 21:29:26 +08:00
FooIbar cd764a4a3c Fix spotless 2023-10-25 18:29:44 +08:00
FooIbar 5422b3d8bd Disable edge to edge temporarily 2023-10-25 17:42:32 +08:00
FooIbar 0273390d79 Flatten layout 2023-10-25 15:36:30 +08:00
FooIbar 8f7242b939 Bring search fab in front of search layout 2023-10-25 15:26:43 +08:00
FooIbar 246dee63b4 Move Context.findActivity to separated file 2023-10-25 13:24:28 +08:00
FooIbar 697918a73a Move navItems to top level 2023-10-25 13:11:21 +08:00
FooIbar 67e480220e Refine navigation drawer 2023-10-25 12:58:44 +08:00
FooIbar 5784eb008e Migrate navigation drawer to compose (#80)
* Drop AboveSnackerLayout

* Migrate navigation drawer to compose

* Suspend until logged in before loading

* Use AndroidViewBinding

* fixup! Migrate navigation drawer to compose

* Cleanup AboveSnackView usage

* Cleanup res

* Cleanup dependencies
2023-10-25 01:53:44 +08:00
FooIbar 82d66c4d57 Prepare for next iteration 2023-10-25 01:53:13 +08:00
FooIbar ab8671cc3b Revert "Exclude some libraries from aboutLibraries"
This reverts commit bc34f25a3c.

Reason for revert: Licenses should be included as per Apache 2.0
2023-10-24 23:53:25 +08:00
FooIbar 2e52dda208 Downgrade AGP to stable version but use newer lint (#81)
* Downgrade AGP to stable version but use newer lint

* Downgrade Java to 17 and use newer r8
2023-10-24 13:08:23 +00:00
renovate[bot] 1ab08d0ae1 Update Rust crate quick-xml to 0.31 2023-10-23 20:22:16 +00:00
FooIbar 702be0ed43 Use eh API as well if forceEhThumb is enabled 2023-10-23 22:37:55 +08:00
FooIbar 4067543701 Cleanup code 2023-10-23 16:03:01 +08:00
FooIbar 074689c60e Fall back if failed to retrieve data from API 2023-10-23 15:10:18 +08:00
FooIbar 2beb715df3 Cleanup code 2023-10-23 14:36:28 +08:00
FooIbar 7ea2597899 Fix large torrent list parsing 2023-10-23 02:25:17 +08:00
FooIbar 34a8f92865 Show meaningful error message if it's available 2023-10-23 02:25:17 +08:00
FooIbar 6e2a2de13f Reformat imports alphabetically
https://developer.android.com/kotlin/style-guide#import_statements
2023-10-22 22:13:45 +08:00
FooIbar e073232352 Fix cronet support detection 2023-10-22 17:52:16 +08:00
FooIbar 6f49b4d23e Fix deprecated 2023-10-21 18:22:45 +08:00
FooIbar 97cad20ab1 It's affected by AGP and NDK versions as well 2023-10-21 17:38:29 +08:00
FooIbar 9300a3d9ce Don't write cmake cache every time 2023-10-21 16:26:14 +08:00
FooIbar 28b7f80a8f Fix content behind navigation bar 2023-10-21 16:13:27 +08:00
FooIbar a06d23753a Lock navi drawer on where doesn't have navi icon 2023-10-21 13:10:46 +08:00
FooIbar 98d66b3e6c Release 1.8.10.1
Releases / Build (push) Has been cancelled
2023-10-21 00:14:03 +08:00
FooIbar 6e59583dea Fix reader bug on certain orientation settings
Resolve #68
2023-10-21 00:12:52 +08:00
FooIbar 2f0571e384 Implement splash screen 2023-10-20 17:36:59 +08:00
renovate[bot] 64a6074e02 Update dependency org.jsoup:jsoup to v1.16.2 2023-10-20 06:11:31 +00:00
renovate[bot] 0b7cb4cf41 Update dependency com.android.application to v8.3.0-alpha09 2023-10-20 01:48:36 +00:00
renovate[bot] e620832e33 Update dependency com.google.android.material:material to v1.11.0-beta01 2023-10-19 23:03:41 +00:00
renovate[bot] f82e4af0e3 Update dependency com.android.application to v8.3.0-alpha08 2023-10-19 20:36:34 +00:00
FooIbar 02baa793f8 Release 1.8.10.0
Releases / Build (push) Has been cancelled
2023-10-19 21:31:08 +08:00
FooIbar 9311a9e7f9 Implement download timeout 2023-10-19 21:15:55 +08:00
renovate[bot] 00a60ad6ab Update actions/checkout digest to b4ffde6 2023-10-19 11:51:36 +00:00
FooIbar e740c25211 Implement LoadStateAdapter for append/prepend load 2023-10-19 19:51:19 +08:00
FooIbar 79c6b63c56 Refactor image download and improve retry strategy 2023-10-19 14:50:22 +08:00
FooIbar bc914566dd Fix progress not showing after clicking error tip 2023-10-19 14:50:22 +08:00
FooIbar 2c6ccfb23d Enable showGalleryPages by default 2023-10-19 14:50:22 +08:00
FooIbar 646767e712 Add back cronet option for debugging 2023-10-19 14:50:22 +08:00
FooIbar 754b14d349 Simplify Java toolchain configuration 2023-10-19 14:50:22 +08:00
renovate[bot] 38c18149aa Update dependency dev.chrisbanes.compose:compose-bom to v2023.11.00-alpha02 2023-10-19 00:42:42 +00:00
renovate[bot] 35b448a274 Update dependency androidx.recyclerview:recyclerview to v1.3.2 2023-10-18 22:25:52 +00:00
renovate[bot] c1e63b5a5d Update androidx.room to v2.6.0 2023-10-18 18:31:20 +00:00
FooIbar 37f984829a Update docs to explain flavors (#64)
* Update README.md

* Update zh-cn.md
2023-10-18 09:45:33 +00:00
FooIbar 65942021d0 Kotlin 1.9.20-RC and Java 21 2023-10-18 02:06:09 +08:00
FooIbar ecef20d52b Use stable version of ktlint since 1.0.1 is out 2023-10-18 00:31:19 +08:00
FooIbar 1a092bb200 Fix total pages not updating for new archives 2023-10-18 00:22:37 +08:00
FooIbar a880f801fb Skip buildProvider if the activity will restart 2023-10-18 00:22:37 +08:00
FooIbar f00360e81c Fix stable channel update check 2023-10-18 00:06:10 +08:00
FooIbar 1737163364 Use product flavors for OSS and GMS variants (#62)
* Use product flavors for OSS and GMS variants

* Move code to kotlin directory

* Temporarily disable lint

* Fix CI

* Fix release

* Tweak release asset names
2023-10-17 10:11:59 +00:00
FooIbar f9a3c3d266 Try to fix recyclerview disappearing after refresh (#61)
* Try to fix recyclerview disappearing after refresh

* Another attempt

* Workaround: Use RefreshLayout's progress indicator
2023-10-17 04:49:20 +00:00
FooIbar 7db04e4359 Fix unable to show content in cutout area 2023-10-16 00:28:24 +08:00
FooIbar 934629d829 Add lint check for marshmallow (#58)
* Add lint check for marshmallow

* Treat some warnings as errors

* Fix attribute unused on older versions
2023-10-15 09:16:50 +00:00
FooIbar cd65bb057d Fix inlined constants on older versions 2023-10-15 15:45:00 +08:00
FooIbar f2d94f1ed6 Use product flavors to provide marshmallow support (#57)
* Use product flavors to provide marshmallow support

Fix crashing when opening archives

* Fix artifacts upload

* Change artifact names

* Fix app updater
2023-10-14 19:14:19 +00:00
FooIbar e29281ce39 Generate and upload native debug symbols 2023-10-15 01:09:44 +08:00
FooIbar 1e719a8738 Use local variable instead 2023-10-15 00:05:43 +08:00
FooIbar 92926487b6 Check if the image is GIF before doing mmap 2023-10-14 21:30:10 +08:00
FooIbar 69f744131a Use custom decoder factory instead
`ImageDecoderDecoder.Factory` is only applicable for animated images.
2023-10-14 19:59:17 +08:00
FooIbar 83cd9bdd00 Revert "Remove coil-gif library"
This reverts commit 1130f4f114.
2023-10-14 19:59:17 +08:00
FooIbar 1130f4f114 Remove coil-gif library
We only use coil for thumbnails, which are all static, and
`ImageDecoderDecoder` is only applicable for animated images.
2023-10-14 15:25:33 +08:00
FooIbar d99d9008dd Do not modify the original file when rewriting GIF
Make it a decode-time thing.
2023-10-14 15:25:33 +08:00
FooIbar 78f61b2396 Tweak the error message of getting a blank page 2023-10-14 14:48:47 +08:00
FooIbar 32829bbf15 Implement image subsampling on API < 28 2023-10-13 15:36:28 +08:00
FooIbar a2d3046070 Fix unable to hide hentaiverse notification
Resolve #51
2023-10-13 00:10:29 +08:00
FooIbar 95641e398a Refactor download delay to mitigate request flood 2023-10-12 22:30:53 +08:00
FooIbar 97c39d07e8 Update e-hentai / exhentai API URLs 2023-10-12 21:16:10 +08:00
FooIbar 14fa2fab71 Remove GIF support on API < 28 2023-10-12 19:29:57 +08:00
FooIbar ef8c31ae52 Upgrade ndk to 26.1.10909125
Fix ndk-stack
2023-10-12 15:04:29 +08:00
FooIbar 353080867a Build x86_64 apk for emulator 2023-10-12 14:29:39 +08:00
FooIbar 4d09b895d4 Refactor areContentsTheSame to compare entire objects 2023-10-11 01:08:46 +08:00
FooIbar 6c00d66334 Cleanup sad panda check 2023-10-10 22:43:35 +08:00
FooIbar 77de187207 Refine GalleryPageApiParser exception handling 2023-10-10 22:38:35 +08:00
FooIbar 79eec13856 Wrap exceptions thrown by native methods 2023-10-10 21:59:37 +08:00
FooIbar 562c6fa9a4 Fix tag database download on API < 24 2023-10-10 19:06:58 +08:00
FooIbar 9fa3ddd254 Downgrade minSdk to 23 (#48)
* Revert "Drop webp icon since minSdk is 28"

This reverts commit 9f77a4ce0a.

* Upgrade AGP and use K2 lint

* Enable core library desugaring and minSdk 23

* Fix some new API usage

* Add fallback image decoder for API < 28
2023-10-08 10:04:40 +00:00
FooIbar d11a0b9b8a Move AGP version to version section 2023-10-06 23:15:47 +08:00
FooIbar 41cf3fbe89 Cleanup version catalog 2023-10-06 15:54:43 +08:00
FooIbar ac12ab93e5 Add compose compiler to version catalog for renovate 2023-10-06 15:02:20 +08:00
FooIbar 8b4e978cf4 Migrate to version catalog 2023-10-06 14:39:47 +08:00
FooIbar c15f22400e Migrate to auto-mirrored icons 2023-10-05 23:36:07 +08:00
FooIbar 1773116313 Fix spotless not working 2023-10-05 23:23:39 +08:00
renovate[bot] 62424fbad1 Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.0-alpha13 2023-10-05 11:39:49 +00:00
renovate[bot] d07bd1aa29 Update ktor to v2.3.5 2023-10-05 10:25:27 +00:00
FooIbar 8433df09e3 Update compose-bom to v2023.11.00-alpha01 2023-10-05 15:11:35 +08:00
FooIbar 0634898cb6 Update gradle wrapper 2023-10-05 15:11:01 +08:00
renovate[bot] 2ebafc3eeb Update dependency gradle to v8.4 2023-10-05 05:59:33 +00:00
FooIbar 8288559e44 Drop GIF workaround on API 34 and above
https://android.googlesource.com/platform/frameworks/base/+/2be87bb707e2c6d75f668c4aff6697b85fbf5b15
2023-10-05 13:30:33 +08:00
renovate[bot] 2caf3f0174 Update navigation to v2.7.4 2023-10-05 04:36:23 +00:00
renovate[bot] e88e48906d Update dependency androidx.fragment:fragment-ktx to v1.7.0-alpha06 2023-10-05 00:53:42 +00:00
renovate[bot] 8fb5153983 Update dependency androidx.collection:collection-ktx to v1.3.0 2023-10-04 23:43:31 +00:00
renovate[bot] 03725b0ab4 Update dependency androidx.activity:activity-compose to v1.8.0 2023-10-04 19:07:04 +00:00
FooIbar b0896ea465 Scroll to top after refreshing 2023-10-04 21:24:28 +08:00
FooIbar 654e9a79c5 Don't append when all results were filtered out 2023-10-04 21:04:20 +08:00
FooIbar 7f541e2623 Fix side sheet not showing after swiped off 2023-10-04 15:43:26 +08:00
FooIbar 198e905050 Change block uploader icon 2023-10-03 22:42:31 +08:00
FooIbar 3744018af8 Revert "Move PageIndicatorText to Compose"
This reverts commit 7c987ec8
2023-10-03 18:41:16 +08:00
FooIbar a21ba04e95 Initialize Settings as early as possible 2023-10-02 19:50:13 +08:00
FooIbar bf4d39421b Cleanup code 2023-10-02 16:29:04 +08:00
renovate[bot] 2e4935715f Update dependency com.squareup.okio:okio-jvm to v3.6.0 2023-10-02 03:15:22 +00:00
FooIbar cf8ce9642a Make changelog scrollable 2023-10-02 00:02:48 +08:00
FooIbar cac65d9030 Fix insets for pre API 30 2023-09-30 23:19:52 +08:00
FooIbar 4a83829dfd Fix padding for scrollable 2023-09-30 23:19:52 +08:00
FooIbar c0348484fe Fix padding for the IME 2023-09-30 23:19:52 +08:00
FooIbar 213133ab45 Fix SignInScreen drawing behind the system bars 2023-09-30 23:19:52 +08:00
renovate[bot] 8469eac18c Update plugin com.diffplug.spotless to v6.22.0 (#33)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-29 07:35:40 +00:00
FooIbar 5aaa1bb5b3 Fix spotless 2023-09-29 15:20:43 +08:00
renovate[bot] 9f500b85e6 Update plugin com.android.application to v8.1.2 2023-09-28 18:40:08 +00:00
FooIbar 0d6fd8d7ad Workaround for SideSheetDialog (#31) 2023-09-27 14:05:21 +00:00
FooIbar 7e4b6dbf31 Fix memory leak 2023-09-27 19:47:57 +08:00
renovate[bot] 8bf3f0f094 Update plugin com.mikepenz.aboutlibraries.plugin to v10.9.1 2023-09-27 01:41:01 +00:00
renovate[bot] 198acef666 Update dependency com.mikepenz:aboutlibraries-core to v10.9.1 2023-09-26 21:03:06 +00:00
FooIbar 1d0c76b2b2 Fix OOB access when no results found 2023-09-26 19:26:32 +08:00
FooIbar 567875cf00 Apply quick search in place if not in Popular (#30) 2023-09-26 08:50:07 +00:00
FooIbar 58069e8925 Fix cmake cache for CI 2023-09-26 01:35:37 +08:00
FooIbar a90cf3c72b Update issue template 2023-09-26 01:28:01 +08:00
FooIbar 2fbf226ccd Update author summary in about 2023-09-26 01:23:11 +08:00
FooIbar d674672ce2 Update libarchive to v3.7.2 2023-09-26 00:28:54 +08:00
FooIbar dab7968ce3 Update liblzma to v5.4.4 2023-09-26 00:28:54 +08:00
FooIbar 510719f890 Convert libnettle build to FetchContent 2023-09-26 00:27:48 +08:00
renovate[bot] 96b92239b0 Update plugin com.mikepenz.aboutlibraries.plugin to v10.9.1-debug01 2023-09-25 08:06:51 +00:00
FooIbar 363029cbdd Upgrade r8 (#27) 2023-09-24 15:55:51 +00:00
FooIbar 7783a8de15 Cleanup build cache for CI 2023-09-24 17:52:06 +08:00
FooIbar 26a589fc1d Add native build cache for CI (#26)
* Add rust cache for CI

* Add cmake cache for CI

* Use cache for release as well

* Add build cache for CI
2023-09-24 08:10:17 +00:00
FooIbar 4c467c304f NDK will be installed automatically 2023-09-24 03:17:12 +08:00
FooIbar 9496b9396a Upgrade ndk to 26.0.10792818 (#24) 2023-09-23 18:52:46 +00:00
FooIbar 669a9ee1e3 Use spotless for ktlint integration 2023-09-24 02:12:39 +08:00
FooIbar d08b7c2d94 Update README 2023-09-23 22:32:54 +08:00
FooIbar 534f6aa22c Merge pull request workflow to CI 2023-09-23 19:13:43 +08:00
FooIbar 8ea731159f Prevent unnecessary regeneration of BuildConfig 2023-09-23 18:59:29 +08:00
renovate[bot] 7704fa9318 Update plugin com.mikepenz.aboutlibraries.plugin to v10.9.0 2023-09-23 09:34:12 +00:00
renovate[bot] d0e3de7121 Update dependency com.mikepenz:aboutlibraries-core to v10.9.0 2023-09-23 07:37:51 +00:00
renovate[bot] 40af1862a0 Update actions/checkout digest to 8ade135 2023-09-23 05:50:16 +00:00
FooIbar 33e24aa46c Fix CI for renovate 2023-09-23 13:49:34 +08:00
FooIbar 5a97561389 Upgrade gradle wrapper 2023-09-22 16:24:48 +08:00
FooIbar 26701d84f7 Cleanup redundant .gitignore 2023-09-22 15:56:27 +08:00
FooIbar d13b09aaf3 Fix swipe-to-refresh 2023-09-22 15:46:57 +08:00
FooIbar 6a65b61785 Store SearchLayout state in ViewModel 2023-09-22 15:02:18 +08:00
FooIbar 2b8bfab44b Fix minRating advanced search 2023-09-22 13:41:22 +08:00
FooIbar b9e1d79be6 Switch to branch automerging 2023-09-21 21:33:21 +08:00
renovate[bot] 0f9e1819c1 Update room_version to v2.6.0-rc01 2023-09-21 10:23:31 +00:00
renovate[bot] eda97347fc Update nav_version to v2.7.3 2023-09-21 08:09:21 +00:00
renovate[bot] 2bd24b6a8b Update dependency com.google.android.material:material to v1.11.0-alpha03 2023-09-21 04:56:54 +00:00
renovate[bot] 8306342ab6 Update dependency androidx.fragment:fragment-ktx to v1.7.0-alpha05 2023-09-21 00:45:03 +00:00
renovate[bot] 6b6ba91721 Update dependency androidx.activity:activity-compose to v1.8.0-rc01 2023-09-20 22:32:00 +00:00
FooIbar 68155e6a6b Refine modify favorites dialog layout 2023-09-16 18:36:44 +08:00
FooIbar bfbceefef2 Fix respectDefaultWidth not working 2023-09-16 17:08:13 +08:00
FooIbar 21b39135fe Tweak modify favorites dialog layout 2023-09-15 21:10:51 +08:00
FooIbar 6adc7f7221 Add option to disable category color harmonization 2023-09-13 20:21:06 +08:00
FooIbar 6bfa8d1ea4 Fix crashing when reordering downloads 2023-09-10 21:18:23 +08:00
FooIbar 6f22269d93 Disable swiping and dragging when in selection mode 2023-09-10 21:18:05 +08:00
FooIbar b68dc98ae0 Fix drawer lock states saving 2023-09-10 19:58:12 +08:00
FooIbar f9ad1f724b Don't show search fab when search view is expanded 2023-09-10 18:30:46 +08:00
FooIbar afd22635bd Update releases.yml 2023-09-10 16:41:46 +08:00
FooIbar a9f9706be3 Migrate from intFromStrPref to intPref 2023-09-09 17:39:29 +08:00
FooIbar 41f35498fe Update syn to v2.0 2023-09-09 16:06:46 +08:00
FooIbar 58628b6556 Update jni_fn to v0.1.2 2023-09-09 16:05:17 +08:00
FooIbar 875858618e Maintenance mode 2023-09-08 21:39:56 +08:00
FooIbar 70b7bf390a Sync with 1.8.9.x versionCode 2023-09-08 18:24:00 +08:00
FooIbar de3d6de4cf Revert "Sync rootProjName with repo name"
This reverts commit de9fa5be5a.
2023-09-08 18:01:58 +08:00
FooIbar e3b4caa37d Ignore expunged torrents 2023-09-08 17:15:46 +08:00
renovate[bot] f8e19f7772 Update paging to v3.2.1 2023-09-08 16:52:03 +08:00
renovate[bot] ea5dd8b1bf Update nav_version to v2.7.2 2023-09-08 16:51:29 +08:00
renovate[bot] bd7db1a016 Update dependency io.arrow-kt:arrow-stack to v1.2.1 2023-09-08 15:30:07 +08:00
renovate[bot] 02ac048d74 Update dependency androidx.lifecycle:lifecycle-process to v2.6.2 2023-09-08 15:26:41 +08:00
renovate[bot] 9a3c9b9cbd Update dependency androidx.fragment:fragment-ktx to v1.7.0-alpha04 2023-09-08 14:55:29 +08:00
renovate[bot] 80c77c740e Update dependency androidx.core:core-ktx to v1.12.0 2023-09-08 14:55:05 +08:00
renovate[bot] 0f62662963 Update dependency androidx.collection:collection-ktx to v1.3.0-rc01 2023-09-08 14:54:23 +08:00
renovate[bot] 576d6515db Update dependency androidx.activity:activity-compose to v1.8.0-beta01 2023-09-08 14:53:55 +08:00
FooIbar 30d29bfa80 Fix torrent parsing 2023-09-08 14:52:04 +08:00
FooIbar c7c0446f23 Tweak ParseException 2023-09-08 14:49:28 +08:00
FooIbar 134777584d Merge branch 'Ehviewer-Overhauled:1.8.x.x' into 1.8.x.x 2023-09-06 16:52:10 +08:00
renovate[bot] 0b64eadb0c Update actions/checkout action to v4 2023-09-04 16:52:00 +00:00
FooIbar ea93960bf4 Fix pages fields in AdvancedSearch 2023-09-01 19:30:21 +08:00
renovate[bot] 71032e3010 Update ktor to v2.3.4 2023-08-31 21:07:43 +00:00
FooIbar d6a3a351f5 Fix UI not updating after download state changed 2023-08-30 16:37:37 +08:00
FooIbar a574bca48e Kotlin 1.9.10 2023-08-30 16:22:18 +08:00
FooIbar 785faf1bf4 Upgrade ndk to r26 RC 1 2023-08-30 16:20:32 +08:00
FooIbar fbcc32b3ee Merge remote-tracking branch 'upstream/1.8.x.x' into 1.8.x.x
# Conflicts:
#	app/build.gradle.kts
2023-08-25 13:28:07 +08:00
renovate[bot] 46b19a9de5 Update room_version to v2.6.0-beta01 2023-08-24 15:49:05 +00:00
renovate[bot] d2f47d51f9 Update plugin com.google.devtools.ksp to v1.9.10-1.0.13 2023-08-24 10:15:46 +00:00
renovate[bot] db380507f5 Update nav_version to v2.7.1 2023-08-24 07:03:27 +00:00
renovate[bot] 84cf67d21a Update dependency composeOptions to v1.5.2 2023-08-24 03:49:47 +00:00
renovate[bot] 8f53591800 Update dependency androidx.fragment:fragment-ktx to v1.7.0-alpha03 2023-08-24 01:47:24 +00:00
renovate[bot] 65ffd78844 Update dependency androidx.activity:activity-compose to v1.8.0-alpha07 2023-08-23 23:18:25 +00:00
renovate[bot] c00b5e6635 Update serialization to v1.6.0 2023-08-22 23:06:02 +00:00
FooIbar 8160991086 Don't respond to touch events when scrolling 2023-08-22 19:27:44 +08:00
FooIbar f0c933d97a Replace deprecated method 2023-08-22 18:55:39 +08:00
renovate[bot] 3fb8d3e73b Update plugin com.android.application to v8.1.1 2023-08-22 06:12:17 +00:00
renovate[bot] d8efff9921 Update accompanist_version to v0.32.0 2023-08-21 14:07:10 +00:00
FooIbar 32170688b6 Support clearing image cache for specific gallery 2023-08-21 18:39:35 +08:00
580 changed files with 18265 additions and 20988 deletions

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
[*.{kt,kts}]
ij_kotlin_imports_layout = *
ktlint_code_style = intellij_idea
ktlint_function_naming_ignore_when_annotated_with = Composable
+14 -8
View File
@@ -6,10 +6,10 @@ body:
- type: markdown
attributes:
value: |
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
感谢您愿意为 EhViewer 做出贡献!
提交问题报告前,还请首先完成文末的自查步骤
Thanks for your contribution on Ehviewer-Overhauled Project!
Thanks for your contribution on EhViewer Project!
Please finish verify steps which list in the end first before create bug report
- type: textarea
@@ -64,7 +64,7 @@ body:
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
placeholder: 06-15 17:44:53.704 23382 23382 E ...
validations:
required: false
required: true
- type: textarea
id: more
@@ -79,6 +79,15 @@ body:
validations:
required: false
- type: input
id: site
attributes:
label: 浏览站点 / Browsing site
description: E-Hentai / ExHentai
placeholder: E-Hentai
validations:
required: true
- type: input
id: version
attributes:
@@ -135,14 +144,11 @@ body:
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
required: true
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
- label: 您已搜索过 [Issue Tracker](https://github.com/FooIbar/EhViewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/FooIbar/EhViewer/issues), No duplicate or related open issue has been found
required: true
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
required: true
- label: 您确保已使用最新 CI 版本测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest CI version
- label: 您确保已使用最新 [CI 版本](https://github.com/FooIbar/EhViewer/actions)测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest [CI version](https://github.com/FooIbar/EhViewer/actions)
required: true
+1 -5
View File
@@ -1,5 +1 @@
blank_issues_enabled: false
contact_links:
- name: 所有其他问题 / All other questions
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
about: 转到 Discussions / Turn to discussions
blank_issues_enabled: false
@@ -1,94 +0,0 @@
name: 功能请求 / Feature request
description: 提出一个功能建议 / Suggest an idea
labels:
- "enhancement"
body:
- type: markdown
attributes:
value: |
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
提交功能建议前,还请首先完成文末的自查步骤
Thanks for your contribution on Ehviewer-Overhauled Project!
Please finish verify steps which list in the end first before suggest an idea
- type: textarea
id: request
attributes:
label: 需求 / Requirement
description: |
在此处描述您的需求。这通常会是一个您想要的功能
Ddescribe what you need here.
placeholder: |
我需要 XXX 功能 ...
I want ABC feature ...
validations:
required: true
- type: textarea
id: impl
attributes:
label: 建议实现 / Suggested implements
description: |
在此处表述您建议的实现方式。如有可能,UI 类功能请求还请尽量附上图示
Describe your suggested implements here, It's recommend to add a photo if you are making a UI feature request.
placeholder: |
建议在 XX 处添加 XX ...
I recommend add ABC feature to DEF...
图片(如果有)/ Photos (if exists)
validations:
required: true
- type: textarea
id: more
attributes:
label: 备注 / Addition details
description: |
在此处写下其他您想说的内容
Describe addition details here
placeholder: |
其他有用的信息与附件
Additional details and attachments
validations:
required: false
- type: input
id: version
attributes:
label: EhViewer 版本号 / EhViewer version code
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
placeholder: 1.6.9.13
validations:
required: true
- type: input
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 位于设置 - 关于中的版本号后 / Next to the version code in Settings - About
placeholder: 26f05a0
validations:
required: true
- type: checkboxes
id: check
attributes:
label: 自查步骤 / Verify steps
description: |
请确认您已经遵守所有必选项
Please ensure you have obtained all needed options
options:
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
required: false
- label: 您已仔细查看并知情 [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/Ehviewer-Overhauled/Ehviewer/issues/61) carefully
required: true
- label: 您已搜索过 [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/Ehviewer-Overhauled/Ehviewer/issues), No duplicate or related open issue has been found
required: true
- label: 您确保这个 Issue 只提及一个功能。如果您有多个功能请求,烦请发起多个 Issue / Ensure there is only one feature request in this issue. Please make mutiply issue for mutiply feature request
required: true
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
required: true
+163 -29
View File
@@ -4,8 +4,8 @@ on:
push:
branches:
- '*'
tags-ignore:
- "*"
pull_request:
workflow_dispatch:
jobs:
linux:
@@ -13,23 +13,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
submodules: 'recursive'
- name: Setup Java 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: Rustfmt Check
working-directory: ./app/src/main/rust
run: cargo fmt --all -- --check
@@ -38,42 +39,175 @@ jobs:
working-directory: ./app/src/main/rust
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Ktlint Check
run: ./gradlew ktlintCheck
- name: Gradle Build
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
gradle-home-cache-cleanup: true
arguments: assembleRelease
- name: Update ARM64
- name: Spotless Check
run: ./gradlew spotlessCheck
- name: CMake Cache
uses: actions/cache@v3
with:
path: |
app/.cxx
app/src/main/cpp/nettle/nettle
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cmake-
- name: Build OSS
run: ./gradlew assembleDefaultOssRelease
- name: Upload OSS ARM64
uses: actions/upload-artifact@v3
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
name: default-oss-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk
- name: Update x86_64
- name: Upload OSS x86_64
uses: actions/upload-artifact@v3
with:
name: x86_64-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86_64-release.apk
name: default-oss-x86_64-${{ github.sha }}
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk
- name: Update ARM32
- name: Upload OSS ARM32
uses: actions/upload-artifact@v3
with:
name: armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
name: default-oss-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk
- name: Update x86
- name: Upload OSS x86
uses: actions/upload-artifact@v3
with:
name: x86-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86-release.apk
name: default-oss-x86-${{ github.sha }}
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk
- name: Update mapping
- name: Upload OSS mapping
uses: actions/upload-artifact@v3
with:
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release/mapping.txt
retention-days: 5
name: default-oss-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultOssRelease/mapping.txt
- name: Upload OSS native debug symbols
uses: actions/upload-artifact@v3
with:
name: default-oss-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip
- name: Build GMS
run: ./gradlew assembleDefaultGmsRelease
- name: Upload GMS ARM64
uses: actions/upload-artifact@v3
with:
name: default-gms-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk
- name: Upload GMS x86_64
uses: actions/upload-artifact@v3
with:
name: default-gms-x86_64-${{ github.sha }}
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk
- name: Upload GMS ARM32
uses: actions/upload-artifact@v3
with:
name: default-gms-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk
- name: Upload GMS x86
uses: actions/upload-artifact@v3
with:
name: default-gms-x86-${{ github.sha }}
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk
- name: Upload GMS mapping
uses: actions/upload-artifact@v3
with:
name: default-gms-mapping-${{ github.sha }}
path: app/build/outputs/mapping/defaultGmsRelease/mapping.txt
- name: Upload GMS native debug symbols
uses: actions/upload-artifact@v3
with:
name: default-gms-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip
- name: Build Marshmallow OSS
run: ./gradlew assembleMarshmallowOssRelease
- name: Upload Marshmallow OSS ARM64
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk
- name: Upload Marshmallow OSS x86_64
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-x86_64-${{ github.sha }}
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk
- name: Upload Marshmallow OSS ARM32
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk
- name: Upload Marshmallow OSS x86
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-x86-${{ github.sha }}
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk
- name: Upload Marshmallow OSS mapping
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-mapping-${{ github.sha }}
path: app/build/outputs/mapping/marshmallowOssRelease/mapping.txt
- name: Upload Marshmallow OSS native debug symbols
uses: actions/upload-artifact@v3
with:
name: marshmallow-oss-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip
- name: Build Marshmallow GMS
run: ./gradlew assembleMarshmallowGmsRelease
- name: Upload Marshmallow GMS ARM64
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-arm64-v8a-release.apk
- name: Upload Marshmallow GMS x86_64
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-x86_64-${{ github.sha }}
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86_64-release.apk
- name: Upload Marshmallow GMS ARM32
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-armeabi-v7a-release.apk
- name: Upload Marshmallow GMS x86
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-x86-${{ github.sha }}
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86-release.apk
- name: Upload Marshmallow GMS mapping
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-mapping-${{ github.sha }}
path: app/build/outputs/mapping/marshmallowGmsRelease/mapping.txt
- name: Upload Marshmallow GMS native debug symbols
uses: actions/upload-artifact@v3
with:
name: marshmallow-gms-native-debug-symbols-${{ github.sha }}
path: app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip
+22
View File
@@ -0,0 +1,22 @@
name: 'Lock Threads'
on:
schedule:
- cron: '0 0 * * *'
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'
include-any-issue-labels: 'duplicate, wontfix'
process-only: 'issues'
-66
View File
@@ -1,66 +0,0 @@
name: Pull Request Check
on: [ pull_request ]
jobs:
linux:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
- 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 --all-targets --all-features -- -D warnings
- name: Ktlint Check
run: ./gradlew ktlintCheck
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
arguments: assembleRelease
- name: Update ARM64
uses: actions/upload-artifact@v3
with:
name: arm64-v8a-${{ github.sha }}
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
- name: Update x86_64
uses: actions/upload-artifact@v3
with:
name: x86_64-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86_64-release.apk
- name: Update ARM32
uses: actions/upload-artifact@v3
with:
name: armeabi-v7a-${{ github.sha }}
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
- name: Update x86
uses: actions/upload-artifact@v3
with:
name: x86-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86-release.apk
+76 -37
View File
@@ -11,23 +11,33 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
submodules: 'recursive'
- name: Setup Java 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;26.0.10404224" --channel=1 --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Install Rust Toolchain
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "./app/src/main/rust"
- name: CMake Cache
uses: actions/cache@v3
with:
path: |
app/.cxx
app/src/main/cpp/nettle/nettle
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cmake-
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
@@ -35,42 +45,71 @@ jobs:
- name: Rename Apks
run: |
cp app/build/outputs/apk/release/app-universal-release.apk app/build/outputs/apk/release/EhViewer-universal.apk
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64.apk
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
mv app/build/outputs/apk/defaultOss/release/app-default-oss-universal-release.apk EhViewer-${{ github.ref_name }}-default-oss-universal.apk
mv app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86.apk
mv app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
mv app/build/outputs/apk/defaultGms/release/app-default-gms-universal-release.apk EhViewer-${{ github.ref_name }}-default-gms-universal.apk
mv app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86.apk
mv app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
mv app/build/outputs/mapping/defaultGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
mv app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
mv app/build/outputs/mapping/marshmallowOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
mv app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
mv app/build/outputs/mapping/marshmallowGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
mv app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
- name: Releases
uses: softprops/action-gh-release@v1
with:
body: Bump Version
discussion_category_name: Announcements
files: |
app/build/outputs/apk/release/EhViewer-universal.apk
app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
app/build/outputs/apk/release/EhViewer-x86_64.apk
app/build/outputs/apk/release/EhViewer-x86.apk
app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
app/build/outputs/mapping/release/mapping.txt
EhViewer-${{ github.ref_name }}-default-oss-universal.apk
EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
EhViewer-${{ github.ref_name }}-default-oss-x86.apk
EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
- name: Collect Apks
run: |
mkdir app/build/outputs/apk/release/fdroid
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
EhViewer-${{ github.ref_name }}-default-gms-universal.apk
EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
EhViewer-${{ github.ref_name }}-default-gms-x86.apk
EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
- name: Push Apks
uses: Ehviewer-Overhauled/github-action-push-to-another-repository@main
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
source-directory: app/build/outputs/apk/release/fdroid
destination-github-username: Ehviewer-Overhauled
destination-repository-name: fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: deploy
create-target-branch: true
commit-message: Add apks from $GITHUB_REF
target-directory: fdroid/tmp-apks
EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
-1
View File
@@ -6,5 +6,4 @@
*.iml
.idea
/captures
release
google-services.json
-3
View File
@@ -1,3 +0,0 @@
[submodule "app/src/main/cpp/nettle/nettle"]
path = app/src/main/cpp/nettle/nettle
url = https://github.com/Ehviewer-Overhauled/nettle
+77
View File
@@ -0,0 +1,77 @@
# 变更日志
## 未发布
### 重大变更
* 由于 Apache HttpClient 5 在 Android 10 以下无法使用,后备 HTTP 引擎恢复为 OkHttp [#514](https://github.com/FooIbar/EhViewer/issues/514)
### Bug 修复
* 修复获取 pToken 失败导致无限加载 [#508](https://github.com/FooIbar/EhViewer/issues/508)
* 修复阅读页快速滑动滑块导致下载延时过多累计 [#512](https://github.com/FooIbar/EhViewer/issues/512)
* 修复 Android 9 以下的设备上无法退出搜索栏 [#513](https://github.com/FooIbar/EhViewer/issues/513)
## 1.10.0
### 重大变更
* 后备 HTTP 引擎变更为 Apache HttpClient 5
* 类别和高级搜索选项移至搜索栏下方
### 新功能
* 支持在搜索时指定画廊语言
### 改进
* 在下载中阅读画廊时会更新历史记录
* 多处性能改进
### Bug 修复
* 修复无法由外部应用进入画廊页面 [#436](https://github.com/FooIbar/EhViewer/issues/436)
* 修复点击精确到页的画廊链接闪退 [#430](https://github.com/FooIbar/EhViewer/issues/430)
* 修复无法搜索本地收藏 [#491](https://github.com/FooIbar/EhViewer/issues/491)
* 修复 CI 版本不显示变更日志
## 1.9.0
### 重大变更
* 除阅读器外全部迁移至 Compose
### 新功能
* 设置-高级 中增加实验性滑动手势敏感度设置
* 支持通过滑动手势打开右侧抽屉
* 支持预见式返回动画,相关设置位于 设置-高级
### 改进
* \[阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
* 优化原图下载重试策略
* 优化小屏设备上的搜索类别布局
* 优化过渡动画
* 优化历史记录删除动画
* 设置-EH 中的缩略图大小设置改为缩略图列数
### Bug 修复
* 修复点击通知栏的下载通知闪退 [#175](https://github.com/FooIbar/EhViewer/issues/175)
* 修复收藏状态显示错误 [#189](https://github.com/FooIbar/EhViewer/issues/189)
* 修复双击导航按钮后无法打开导航抽屉
* 修复某些情况下闪退 [#142](https://github.com/FooIbar/EhViewer/issues/142) [#208](https://github.com/FooIbar/EhViewer/issues/208)
* 修复收藏界面点击重试崩溃 [#193](https://github.com/FooIbar/EhViewer/issues/193)
* 修复本地收藏滚动位置丢失 [#192](https://github.com/FooIbar/EhViewer/issues/192)
* 修复手速过快页面切换过快使加载圈圈不自动消失 [#209](https://github.com/FooIbar/EhViewer/issues/209)
* 修复打开并关闭右侧抽屉后,状态栏一片白色 [#101](https://github.com/FooIbar/EhViewer/issues/101)
* 修复使用深色主题时,切换界面会闪过亮色界面 [#121](https://github.com/FooIbar/EhViewer/issues/121)
* \[Marshmallow] 修复小内存设备上图片解码失败 [#229](https://github.com/FooIbar/EhViewer/issues/229) [#372](https://github.com/FooIbar/EhViewer/issues/372)
* 修复已舍弃画廊上传者解析
### 已知问题
* 画廊信息对话框无法通过返回关闭 [#413](https://github.com/FooIbar/EhViewer/issues/413)
+40 -29
View File
@@ -1,60 +1,57 @@
<p align="right">
<strong>English</strong>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
<a href="/docs/README/zh-cn.md">
简体中文
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
<a href="/docs/README/zh-tw.md">
正體中文
</a>
</p>
<h1 align="center">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200">
<img src="https://github.com/Ehviewer-Overhauled/Art/blob/master/launcher_icon-web.svg" width="200" alt="EhViewer">
<br>EhViewer<br>
</h1>
<p align="center">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
<a href="/LICENSE">
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
<img src="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer/badge" alt="CodeFactor">
<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/Ehviewer-Overhauled/Ehviewer/releases">
<img src="https://img.shields.io/github/downloads/Ehviewer-Overhauled/EhViewer/total.svg" alt="Downloads">
<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/Ehviewer-Overhauled/Ehviewer/releases">
<img src="https://img.shields.io/github/v/release/Ehviewer-Overhauled/ehviewer?include_prereleases" alt="Release">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/issues">
<img src="https://img.shields.io/github/issues/Ehviewer-Overhauled/ehviewer" alt="Issues">
<a href="https://github.com/FooIbar/EhViewer/issues">
<img src="https://img.shields.io/github/issues/FooIbar/EhViewer" alt="Issues">
</a>
</p>
<div align="center">
<h3>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#description">
<a href="#description">
Description
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#download">
<a href="#download">
Download
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#screenshot">
<a href="#screenshot">
Screenshot
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#thanks">
<a href="#thanks">
Thanks
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#license">
<a href="#license">
License
</a>
</h3>
@@ -69,7 +66,20 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
# Download
<a href="https://github.com/Ehviewer-Overhauled/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> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Get it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
| Flavor | Feature |
|-------------|-----------------------------------|
| Default | Android 9+, full support |
| Marshmallow | Android 6.0-8.1, limited support* |
| OSS | FOSS dependencies** |
| GMS | Proprietary GMS component |
\* Archives and GIFs are not supported
** Fall back to OkHttp on devices that don't support [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine)
<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
@@ -83,14 +93,14 @@ Here is the libraries
- [Arrow](https://arrow-kt.io/)
- [AOSP & AndroidX](http://source.android.com/)
- [Kotlin & KotlinX](https://kotlinlang.org/)
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
- [MCA](https://github.com/material-components/material-components-android)
- [material-design-icons](https://github.com/google/material-design-icons)
- [Okhttp](https://square.github.io/okhttp/)
- [RikkaX](https://github.com/RikkaApps/RikkaX)
- [Libarchive](http://www.libarchive.org/)
- [MDC-Android](https://github.com/material-components/material-components-android)
- [Material Icons](https://github.com/google/material-design-icons)
- [OkHttp](https://square.github.io/okhttp/)
- [Ktor](https://ktor.io/)
- [Coil](https://coil-kt.github.io/coil/)
- [Jsoup](https://jsoup.org/)
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
- [libarchive](http://www.libarchive.org/)
- [jsoup](https://jsoup.org/)
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
@@ -99,6 +109,7 @@ Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementati
Copyright 2014-2019 Hippo Seven
Copyright 2020-2022 NekoInverter
Copyright 2022-2023 Tarsin Norbin
Copyright 2023 Foolbar
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.
+125 -108
View File
@@ -8,20 +8,21 @@ val isRelease: Boolean
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
plugins {
id("com.android.application")
kotlin("android")
id("kotlin-parcelize")
kotlin("plugin.serialization")
id("com.google.devtools.ksp")
id("com.mikepenz.aboutlibraries.plugin")
id("org.mozilla.rust-android-gradle.rust-android")
id("dev.shreyaspatil.compose-compiler-report-generator")
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.spotless)
alias(libs.plugins.aboutlibrariesPlugin)
alias(libs.plugins.rustAndroidPlugin)
alias(libs.plugins.composeCompilerReportGenerator)
}
android {
compileSdk = 34
buildToolsVersion = "34.0.0"
ndkVersion = "26.0.10404224"
ndkVersion = "26.1.10909125"
splits {
abi {
@@ -31,7 +32,7 @@ android {
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
isUniversalApk = true
} else {
include("arm64-v8a")
include("arm64-v8a", "x86_64")
}
}
}
@@ -63,8 +64,8 @@ android {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
targetSdk = 34
versionCode = 180042
versionName = "1.8.9.0-SNAPSHOT"
versionCode = 180048
versionName = "1.10.1"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -82,8 +83,39 @@ android {
),
)
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
ndk {
debugSymbolLevel = "FULL"
}
}
flavorDimensions += listOf("api", "oss")
productFlavors {
create("default") {
dimension = "api"
}
create("marshmallow") {
dimension = "api"
minSdk = 23
applicationIdSuffix = ".m"
versionNameSuffix = "-M"
externalNativeBuild {
cmake {
targets += "noop"
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
create("oss") {
dimension = "oss"
}
create("gms") {
dimension = "oss"
versionNameSuffix = "-gms"
}
}
externalNativeBuild {
@@ -92,21 +124,14 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = listOf(
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
"-Xjvm-default=all",
"-Xlambdas=indy",
"-XXLanguage:+BreakContinueInInlineLambdas",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi",
@@ -120,13 +145,14 @@ android {
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-opt-in=splitties.experimental.ExperimentalSplittiesApi",
"-opt-in=splitties.preferences.DataStorePreferencesPreview",
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
)
}
lint {
abortOnError = true
checkReleaseBuilds = false
disable.add("MissingTranslation")
disable += setOf("MissingTranslation", "MissingQuantity")
fatal += setOf("NewApi", "InlineApi")
}
packaging {
@@ -146,9 +172,11 @@ android {
isShrinkResources = true
proguardFiles("proguard-rules.pro")
signingConfig = signConfig
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
}
debug {
applicationIdSuffix = ".debug"
buildConfigField("String", "BUILD_TIME", "\"\"")
}
}
@@ -159,7 +187,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
}
namespace = "com.hippo.ehviewer"
@@ -167,102 +195,88 @@ android {
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation("androidx.activity:activity-compose:1.8.0-alpha06")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
implementation("androidx.browser:browser:1.6.0")
implementation("androidx.collection:collection-ktx:1.3.0-beta01")
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.biometric)
implementation(libs.androidx.browser)
// https://developer.android.com/jetpack/androidx/releases/compose-material3
// api(platform("androidx.compose:compose-bom:2023.05.00"))
api(platform("dev.chrisbanes.compose:compose-bom:2023.07.00-alpha02"))
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material3:material3-window-size-class")
implementation("androidx.compose.ui:ui-util")
api(platform(libs.compose.bom))
implementation(libs.bundles.compose)
implementation("androidx.core:core-ktx:1.12.0-rc01")
implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.compiler)
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha12")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.fragment:fragment-ktx:1.7.0-alpha02")
implementation(libs.androidx.core)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.constraintlayout.compose)
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
// https://developer.android.com/jetpack/androidx/releases/navigation
val nav_version = "2.7.0"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
implementation("androidx.navigation:navigation-compose:$nav_version")
implementation(libs.bundles.androidx.navigation)
// https://developer.android.com/jetpack/androidx/releases/paging
val paging = "3.2.0"
implementation("androidx.paging:paging-compose:$paging")
implementation("androidx.paging:paging-runtime-ktx:$paging")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.recyclerview:recyclerview-selection:1.2.0-alpha01")
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.recyclerview)
// https://developer.android.com/jetpack/androidx/releases/room
val room_version = "2.6.0-alpha03"
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-paging:$room_version")
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.paging)
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("com.drakeet.drawer:drawer:1.0.3")
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
// https://github.com/google/accompanist/releases
val accompanist_version = "0.31.6-rc"
implementation("com.google.accompanist:accompanist-themeadapter-material3:$accompanist_version")
implementation("com.google.accompanist:accompanist-webview:$accompanist_version")
implementation("com.google.android.material:material:1.11.0-alpha02")
implementation(libs.androidx.work.runtime)
implementation(libs.photoview) // Dead Dependency
implementation(libs.directionalviewpager) // Dead Dependency
implementation(libs.material)
val splitties = "3.0.0"
implementation("com.louiscad.splitties:splitties-appctx:$splitties")
implementation("com.louiscad.splitties:splitties-systemservices:$splitties")
implementation("com.louiscad.splitties:splitties-preferences:$splitties")
implementation("com.louiscad.splitties:splitties-arch-room:$splitties")
implementation(libs.bundles.splitties)
// https://square.github.io/okhttp/changelogs/changelog/
implementation(platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11"))
implementation("com.squareup.okhttp3:okhttp-android")
implementation("com.squareup.okhttp3:okhttp-coroutines")
implementation(platform(libs.okhttp.bom))
implementation(libs.bundles.okhttp)
implementation("com.squareup.okio:okio-jvm:3.5.0")
implementation(libs.okio.jvm)
implementation("com.mikepenz:aboutlibraries-core:10.8.3")
implementation(libs.diff)
implementation("dev.chrisbanes.insetter:insetter:0.6.1") // Dead Dependency
implementation("dev.rikka.rikkax.core:core-ktx:1.4.1")
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
implementation(libs.aboutlibraries.core)
implementation(platform("io.arrow-kt:arrow-stack:1.2.0"))
implementation("io.arrow-kt:arrow-fx-coroutines")
implementation(libs.insetter) // Dead Dependency
implementation(libs.reorderable)
implementation(platform(libs.arrow.stack))
implementation(libs.arrow.fx.coroutines)
// https://coil-kt.github.io/coil/changelog/
implementation(platform("io.coil-kt:coil-bom:2.4.0"))
implementation("io.coil-kt:coil-compose")
implementation("io.coil-kt:coil-gif")
implementation(platform(libs.coil.bom))
implementation(libs.bundles.coil)
val ktor = "2.3.3"
implementation("io.ktor:ktor-io-jvm:$ktor")
implementation("io.ktor:ktor-utils-jvm:$ktor")
implementation(libs.bundles.ktor)
val serialization = "1.5.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:$serialization")
implementation(libs.bundles.kotlinx.serialization)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jsoup:jsoup:1.16.1")
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
implementation(libs.jsoup)
val chunker = "4.0.0"
debugImplementation("com.github.chuckerteam.chucker:library:$chunker")
releaseImplementation("com.github.chuckerteam.chucker:library-no-op:$chunker")
coreLibraryDesugaring(libs.desugar)
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
"gmsImplementation"(libs.bundles.cronet)
}
configurations.all {
resolutionStrategy {
exclude("androidx.navigation", "navigation-compose")
}
}
kotlin {
jvmToolchain(17)
}
ksp {
@@ -273,29 +287,32 @@ ksp {
aboutLibraries {
duplicationMode = MERGE
duplicationRule = GROUP
exclusionPatterns = listOf(
"androidx\\..*".toPattern(),
".*annotations".toPattern(),
"com\\.google\\.code\\..*".toPattern(),
"com\\.google\\.guava:listenablefuture".toPattern(),
"com\\.google\\.protobuf:protobuf-javalite".toPattern(),
"org\\.checkerframework:checker-qual".toPattern(),
"org\\.chromium\\.net:cronet-embedded".toPattern(),
"org\\.slf4j:slf4j-api".toPattern(),
)
}
cargo {
module = "src/main/rust"
libname = "ehviewer_rust"
targets = if (isRelease) listOf("arm", "x86", "arm64", "x86_64") else listOf("arm64")
targets = if (isRelease) listOf("arm", "x86", "arm64", "x86_64") else listOf("arm64", "x86_64")
if (isRelease) profile = "release"
}
val ktlintVersion = libs.versions.ktlint.get()
spotless {
kotlin {
// https://github.com/diffplug/spotless/issues/111
target("src/**/*.kt")
ktlint(ktlintVersion)
}
kotlinGradle {
ktlint(ktlintVersion)
}
}
tasks.configureEach {
if ((name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders")) {
if (name.startsWith("merge") && name.endsWith("JniLibFolders")) {
dependsOn("cargoBuild")
// fix mergeDebugJniLibFolders UP-TO-DATE
inputs.dir(buildDir.resolve("rustJniLibs/android"))
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
}
}
+8 -18
View File
@@ -1,30 +1,20 @@
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
# https://issuetracker.google.com/issues/190382641
-keepclassmembers class kotlin.SafePublicationLazyImpl {
java.lang.Object _value;
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
-keep class com.hippo.ehviewer.client.parser.Torrent { *; }
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
-keep class com.hippo.ehviewer.ui.scene.SearchBarScene { getBinding(); }
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
-dontwarn org.conscrypt.Conscrypt
-repackageclasses
-allowaccessmodification
-overloadaggressively
-repackageclasses
@@ -1,88 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "c801ee62729ba20a203b150211234d5c",
"entities": [
{
"tableName": "OK_HTTP_3_COOKIE",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT, `VALUE` TEXT, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT, `PATH` TEXT, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "value",
"columnName": "VALUE",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "expiresAt",
"columnName": "EXPIRES_AT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "DOMAIN",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "PATH",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "secure",
"columnName": "SECURE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "httpOnly",
"columnName": "HTTP_ONLY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "persistent",
"columnName": "PERSISTENT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hostOnly",
"columnName": "HOST_ONLY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c801ee62729ba20a203b150211234d5c')"
]
}
}
@@ -1,88 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "1b80cd29939b0f43934721f1289ba94d",
"entities": [
{
"tableName": "OK_HTTP_3_COOKIE",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT NOT NULL, `VALUE` TEXT NOT NULL, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT NOT NULL, `PATH` TEXT NOT NULL, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
"fields": [
{
"fieldPath": "name",
"columnName": "NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "VALUE",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "expiresAt",
"columnName": "EXPIRES_AT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "DOMAIN",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "PATH",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "secure",
"columnName": "SECURE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "httpOnly",
"columnName": "HTTP_ONLY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "persistent",
"columnName": "PERSISTENT",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hostOnly",
"columnName": "HOST_ONLY",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b80cd29939b0f43934721f1289ba94d')"
]
}
}
@@ -0,0 +1,20 @@
package com.hippo.ehviewer.cronet
import com.hippo.ehviewer.Settings
import okio.Path.Companion.toOkioPath
import org.chromium.net.CronetEngine
import splitties.init.appCtx
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
enableBrotli(true)
setUserAgent(Settings.userAgent)
// Cache Quic hint only since the real cache mechanism should on Ktor layer
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 4096)
addQuicHint("e-hentai.org", 443, 443)
addQuicHint("forums.e-hentai.org", 443, 443)
addQuicHint("exhentai.org", 443, 443)
}.build()
@@ -0,0 +1,176 @@
package com.hippo.ehviewer.ktor
import com.hippo.ehviewer.cronet.cronetDispatcher
import com.hippo.ehviewer.cronet.cronetHttpClient
import com.hippo.ehviewer.cronet.cronetHttpClientExecutor
import com.hippo.ehviewer.cronet.pool
import io.ktor.client.engine.HttpClientEngineBase
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.engine.callContext
import io.ktor.client.request.HttpRequestData
import io.ktor.client.request.HttpResponseData
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpProtocolVersion
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.OutgoingContent
import io.ktor.http.headers
import io.ktor.util.date.GMTDate
import io.ktor.util.flattenForEach
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.InternalAPI
import io.ktor.utils.io.pool.useInstance
import io.ktor.utils.io.writer
import java.nio.ByteBuffer
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.chromium.net.CronetException
import org.chromium.net.UploadDataProvider
import org.chromium.net.UploadDataSink
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
object CronetEngine : HttpClientEngineBase("Cronet") {
override val config = HttpClientEngineConfig()
override val dispatcher = cronetDispatcher
@InternalAPI
override suspend fun execute(data: HttpRequestData): HttpResponseData {
val callContext = callContext()
return executeHttpRequest(callContext, data)
}
@OptIn(DelicateCoroutinesApi::class)
private suspend fun executeHttpRequest(
callContext: CoroutineContext,
data: HttpRequestData,
): HttpResponseData = suspendCancellableCoroutine { continuation ->
val requestTime = GMTDate()
val callback = object : UrlRequest.Callback() {
lateinit var chunkChan: Channel<ByteBuffer>
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
continuation.resume(
info.toHttpResponseData(
requestTime = requestTime,
callContext = callContext,
),
)
}
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
val channel = GlobalScope.writer(callContext) {
pool.useInstance {
chunkChan = Channel(1)
request.read(it)
chunkChan.consumeEach { buffer ->
buffer.flip()
channel.writeFully(buffer)
buffer.clear()
request.read(buffer)
}
}
}.channel
continuation.resume(
info.toHttpResponseData(
requestTime = requestTime,
callContext = callContext,
responseBody = channel,
),
)
}
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
chunkChan.trySend(byteBuffer).getOrThrow()
}
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
chunkChan.close()
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
if (::chunkChan.isInitialized) {
chunkChan.close(error)
} else {
continuation.resumeWithException(error)
}
}
}
val request = cronetHttpClient.newUrlRequestBuilder(data.url.toString(), callback, cronetHttpClientExecutor).apply {
setHttpMethod(data.method.value)
data.headers.flattenForEach { key, value -> addHeader(key, value) }
data.body.contentType?.let { addHeader(HttpHeaders.ContentType, it.toString()) }
data.body.contentLength?.let { addHeader(HttpHeaders.ContentLength, it.toString()) }
data.body.toUploadDataProvider(callContext)?.let { setUploadDataProvider(it, cronetHttpClientExecutor) }
}.build()
request.start()
callContext[Job]!!.invokeOnCompletion { request.cancel() }
continuation.invokeOnCancellation { request.cancel() }
}
}
private fun UrlResponseInfo.toHttpResponseData(
requestTime: GMTDate,
callContext: CoroutineContext,
responseBody: ByteReadChannel = ByteReadChannel.Empty,
): HttpResponseData {
return HttpResponseData(
statusCode = HttpStatusCode.fromValue(httpStatusCode),
requestTime = requestTime,
headers = headers {
allHeaders.forEach { (key, value) ->
appendAll(key, value)
}
},
version = when (negotiatedProtocol) {
"h2" -> HttpProtocolVersion.HTTP_2_0
"h3" -> HttpProtocolVersion.QUIC
"quic/1+spdy/3" -> HttpProtocolVersion.SPDY_3
else -> HttpProtocolVersion.HTTP_1_1
},
body = responseBody,
callContext = callContext,
)
}
@OptIn(DelicateCoroutinesApi::class)
private fun OutgoingContent.toUploadDataProvider(callContext: CoroutineContext): UploadDataProvider? = when (this) {
is OutgoingContent.NoContent -> null
is OutgoingContent.ByteArrayContent -> object : UploadDataProvider() {
val buffer = ByteBuffer.wrap(bytes()).slice()
override fun getLength() = buffer.limit().toLong()
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
byteBuffer.put(buffer)
uploadDataSink.onReadSucceeded(false)
}
override fun rewind(uploadDataSink: UploadDataSink) {
buffer.position(0)
uploadDataSink.onRewindSucceeded()
}
}
is OutgoingContent.WriteChannelContent -> object : UploadDataProvider() {
fun launch() = GlobalScope.writer(callContext) { writeTo(channel) }.channel
var chan = launch()
override fun getLength() = contentLength!!
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
GlobalScope.launch(callContext) {
chan.readAvailable(byteBuffer)
uploadDataSink.onReadSucceeded(false)
}
}
override fun rewind(uploadDataSink: UploadDataSink) {
chan = launch()
uploadDataSink.onRewindSucceeded()
}
}
else -> error("UnsupportedContentType $this")
}
@@ -0,0 +1,12 @@
package com.hippo.ehviewer.util
import com.google.android.gms.net.CronetProviderInstaller
import org.chromium.net.CronetProvider
import splitties.init.appCtx
val isCronetAvailable: Boolean
get() = CronetProvider.getAllProviders(appCtx).any { it.isEnabled }.also {
if (!it) {
CronetProviderInstaller.installProvider(appCtx)
}
}
+5 -2
View File
@@ -23,16 +23,20 @@
android:allowBackup="true"
android:appCategory="image"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_scheme"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="@bool/large_heap"
android:localeConfig="@xml/locale_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="tiramisu">
<profileable android:shell="true" />
<activity
android:name=".ui.ConfigureActivity"
android:exported="false"
@@ -44,6 +48,7 @@
android:configChanges="screenSize"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/AppTheme.Splash"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -134,8 +139,6 @@
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name=".ui.WebViewActivity" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
@@ -1,34 +0,0 @@
From 2bda36800a5424c44378bbc0cc8bb5d26a0f34fa Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 22 Jul 2023 08:30:20 +0800
Subject: [PATCH] Don't build other binarys
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 55cd358..52b57aa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -962,7 +962,7 @@ check_symbol_exists(getopt_long getopt.h HAVE_GETOPT_LONG)
# xzdec
#############################################################################
-if(HAVE_GETOPT_LONG AND HAVE_DECODERS)
+if(0)
add_executable(xzdec
src/common/sysdefs.h
src/common/tuklib_common.h
@@ -1008,7 +1008,7 @@ endif()
# xz
#############################################################################
-if(NOT MSVC AND HAVE_GETOPT_LONG)
+if(0)
add_executable(xz
src/common/mythread.h
src/common/sysdefs.h
--
2.41.0
-29
View File
@@ -1,29 +0,0 @@
From 57da49f9b0ed8538573f6cfe289f835ee0078eec Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 22 Jul 2023 17:50:08 +0800
Subject: [PATCH] Hide symbols
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 646484b7..67a71272 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2060,10 +2060,10 @@ SET(OLD_CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS}")
SET(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -fvisibility=hidden -Werror")
CHECK_C_SOURCE_COMPILES("void __attribute__((visibility(\"default\"))) foo(void);
int main() { return 0; }" HAVE_VISIBILITY_ATTR)
-IF (HAVE_VISIBILITY_ATTR)
+IF (0)
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
ADD_DEFINITIONS(-D__LIBARCHIVE_ENABLE_VISIBILITY)
-ENDIF(HAVE_VISIBILITY_ATTR)
+ENDIF(0)
SET(CMAKE_REQUIRED_FLAGS "${OLD_CMAKE_REQUIRED_FLAGS}")
# Generate "config.h" from "build/cmake/config.h.in"
--
2.41.0
@@ -1,25 +0,0 @@
From c9298fd6dc0a8b04dd4b2247118b9cb895c46e58 Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 23 Jul 2022 16:05:36 +0800
Subject: [PATCH 2/2] Insert link libs
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 713e3bc5..7247640d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -415,7 +415,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
# # endif
ENDIF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
-SET(ADDITIONAL_LIBS "")
+SET(ADDITIONAL_LIBS ${LIBARCHIVE_CUSTOM_LIBS})
#
# Find ZLIB
#
--
2.39.2
+13 -6
View File
@@ -23,15 +23,21 @@ set(FETCHCONTENT_BASE_DIR ${deps})
set(BUILD_TESTING 0)
FetchContent_Declare(liblzma
GIT_REPOSITORY https://git.tukaani.org/xz.git
GIT_TAG v5.4.3
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binarys.patch || cd .
GIT_REPOSITORY https://github.com/FooIbar/xz.git
GIT_TAG v5.4.5
)
FetchContent_MakeAvailable(liblzma)
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
# Build GNUTLS libnettle
FetchContent_Declare(nettle
URL https://ftp.gnu.org/gnu/nettle/nettle-3.9.1.tar.gz
URL_MD5 29fcd2dec6bf5b48e5e3ffb3cbc4779e
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/nettle/nettle
)
FetchContent_MakeAvailable(nettle)
add_subdirectory(nettle)
# Configure libnettle support for libarchive
@@ -62,9 +68,8 @@ option(ENABLE_TEST OFF)
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
FetchContent_Declare(libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive
GIT_TAG v3.7.0
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Hide-symbols.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || cd .
GIT_REPOSITORY https://github.com/FooIbar/libarchive.git
GIT_TAG v3.7.2
)
FetchContent_MakeAvailable(libarchive)
@@ -73,3 +78,5 @@ include_directories(${libarchive_SOURCE_DIR}/libarchive)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED archive.c gifutils.c natsort/strnatcmp.c)
target_link_libraries(${PROJECT_NAME} archive_static log)
add_custom_target("noop")
+8 -8
View File
@@ -279,7 +279,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
}
JNIEXPORT jint JNICALL
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
jlong size) {
EH_UNUSED(env);
EH_UNUSED(thiz);
@@ -359,7 +359,7 @@ Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_openArchive(JNIEnv *env, jcl
#pragma ide diagnostic ignored "UnreachableCode"
JNIEXPORT jobject JNICALL
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
@@ -389,7 +389,7 @@ Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToByteBuffer(JNIEnv *
#pragma clang diagnostic pop
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_closeArchive(JNIEnv *env, jclass thiz) {
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
for (int i = 0; i < CTX_POOL_SIZE; i++)
@@ -408,14 +408,14 @@ Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_closeArchive(JNIEnv *env, jc
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_needPassword(JNIEnv *env, jclass thiz) {
Java_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
return need_encrypt;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_providePassword(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
jstring str) {
EH_UNUSED(thiz);
struct archive_entry *entry;
@@ -446,7 +446,7 @@ Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_providePassword(JNIEnv *env,
}
JNIEXPORT jstring JNICALL
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_getFilename(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz,
jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
@@ -462,7 +462,7 @@ Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_getFilename(JNIEnv *env, jcl
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToFd(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
jint index, jint fd) {
EH_UNUSED(env);
EH_UNUSED(thiz);
@@ -477,7 +477,7 @@ Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToFd(JNIEnv *env, jcl
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
jobject buffer) {
EH_UNUSED(thiz);
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
+23 -7
View File
@@ -1,6 +1,7 @@
#include <jni.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <android/log.h>
#include <sys/stat.h>
#include <sys/mman.h>
@@ -11,6 +12,7 @@
#define GIF_HEADER_87A "GIF87a"
#define GIF_HEADER_89A "GIF89a"
#define GIF_HEADER_LENGTH 6
static int FRAME_DELAY_START_MARKER = 0x0021F904;
@@ -21,7 +23,8 @@ typedef signed char byte;
#define DEFAULT_FRAME_DELAY 10
static inline bool isGif(void *addr) {
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
return !memcmp(addr, GIF_HEADER_87A, GIF_HEADER_LENGTH) ||
!memcmp(addr, GIF_HEADER_89A, GIF_HEADER_LENGTH);
}
static void doRewrite(byte *addr, size_t size) {
@@ -42,20 +45,33 @@ static void doRewrite(byte *addr, size_t size) {
}
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_isGif(JNIEnv *env, jclass clazz, jint fd) {
byte buffer[GIF_HEADER_LENGTH];
return read(fd, buffer, GIF_HEADER_LENGTH) == GIF_HEADER_LENGTH &&
isGif(buffer);
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_image_ImageKt_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
Java_com_hippo_ehviewer_jni_GifUtilsKt_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
doRewrite(addr, size);
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_image_ImageKt_rewriteGifSource2(JNIEnv *env, jclass clazz, jint fd) {
JNIEXPORT jobject JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_mmap(JNIEnv *env, jclass clazz, jint fd) {
struct stat64 st;
fstat64(fd, &st);
size_t size = st.st_size;
byte *addr = mmap64(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) return;
doRewrite(addr, size);
byte *addr = mmap64(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) return NULL;
return (*env)->NewDirectByteBuffer(env, addr, size);
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_jni_GifUtilsKt_munmap(JNIEnv *env, jclass clazz, jobject buffer) {
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
munmap(addr, size);
}
+1
View File
@@ -0,0 +1 @@
/nettle
+1 -2
View File
@@ -32,7 +32,6 @@ set(LIBNETTLE_SOURCES
nettle/aes256-meta.c
nettle/nist-keywrap.c
nettle/arcfour.c
nettle/arcfour-crypt.c
nettle/arctwo.c
nettle/arctwo-meta.c
nettle/blowfish.c
@@ -177,7 +176,7 @@ set(LIBNETTLE_SOURCES
nettle/sha1-compress.c
nettle/sha1-meta.c
nettle/sha256.c
nettle/sha256-compress.c
nettle/sha256-compress-n.c
nettle/sha224-meta.c
nettle/sha256-meta.c
nettle/sha512.c
+25 -20
View File
@@ -4,13 +4,19 @@
/* Define if building universal (internal helper macro) */
/* #undef AC_APPLE_UNIVERSAL_BUILD */
/* Define to 1 if using 'alloca.c'. */
/* Define to one of `_getb67', `GETB67', `getb67' for Cray-2 and Cray-YMP
systems. This function is required for `alloca.c' support on those systems.
*/
/* #undef CRAY_STACKSEG_END */
/* Define to 1 if using `alloca.c'. */
/* #undef C_ALLOCA */
/* Define to 1 if you have 'alloca', as a function or macro. */
/* Define to 1 if you have `alloca', as a function or macro. */
#define HAVE_ALLOCA 1
/* Define to 1 if <alloca.h> works. */
/* Define to 1 if you have <alloca.h> and it should be used (not on Ultrix).
*/
#define HAVE_ALLOCA_H 1
/* Define if __builtin_bswap64 is available */
@@ -46,6 +52,9 @@
/* Define to 1 if you have the <malloc.h> header file. */
#define HAVE_MALLOC_H 1
/* Define to 1 if you have the <memory.h> header file. */
#define HAVE_MEMORY_H 1
/* Define to 1 each of the following for which a native (ie. CPU specific)
implementation of the corresponding routine exists. */
/* #undef HAVE_NATIVE_memxor3 */
@@ -71,7 +80,7 @@
/* #undef HAVE_NATIVE_cbc_aes256_encrypt */
/* #undef HAVE_NATIVE_chacha_core */
/* #undef HAVE_NATIVE_chacha_2core */
/* #undef HAVE_NATIVE_chacha_3core */
#define HAVE_NATIVE_chacha_3core 1
/* #undef HAVE_NATIVE_chacha_4core */
/* #undef HAVE_NATIVE_fat_chacha_2core */
/* #undef HAVE_NATIVE_fat_chacha_3core */
@@ -88,13 +97,18 @@
/* #undef HAVE_NATIVE_ecc_secp384r1_redc */
/* #undef HAVE_NATIVE_ecc_secp521r1_modp */
/* #undef HAVE_NATIVE_ecc_secp521r1_redc */
/* #undef HAVE_NATIVE_poly1305_set_key */
/* #undef HAVE_NATIVE_poly1305_block */
/* #undef HAVE_NATIVE_poly1305_digest */
/* #undef HAVE_NATIVE_poly1305_blocks */
/* #undef HAVE_NATIVE_fat_poly1305_blocks */
/* #undef HAVE_NATIVE_ghash_set_key */
/* #undef HAVE_NATIVE_ghash_update */
/* #undef HAVE_NATIVE_salsa20_core */
/* #undef HAVE_NATIVE_salsa20_2core */
#define HAVE_NATIVE_salsa20_2core 1
/* #undef HAVE_NATIVE_fat_salsa20_2core */
/* #undef HAVE_NATIVE_sha1_compress */
/* #undef HAVE_NATIVE_sha256_compress */
/* #undef HAVE_NATIVE_sha256_compress_n */
/* #undef HAVE_NATIVE_sha512_compress */
/* #undef HAVE_NATIVE_sha3_permute */
/* #undef HAVE_NATIVE_umac_nh */
@@ -107,14 +121,11 @@
/* #undef HAVE_OPENSSL_EVP_H */
/* Define to 1 if you have the `secure_getenv' function. */
/* #undef HAVE_SECURE_GETENV */
#define HAVE_SECURE_GETENV 1
/* Define to 1 if you have the <stdint.h> header file. */
#define HAVE_STDINT_H 1
/* Define to 1 if you have the <stdio.h> header file. */
#define HAVE_STDIO_H 1
/* Define to 1 if you have the <stdlib.h> header file. */
#define HAVE_STDLIB_H 1
@@ -130,9 +141,6 @@
/* Define to 1 if you have the <sys/stat.h> header file. */
#define HAVE_SYS_STAT_H 1
/* Define to 1 if you have the <sys/time.h> header file. */
#define HAVE_SYS_TIME_H 1
/* Define to 1 if you have the <sys/types.h> header file. */
#define HAVE_SYS_TYPES_H 1
@@ -149,7 +157,7 @@
#define PACKAGE_NAME "nettle"
/* Define to the full name and version of this package. */
#define PACKAGE_STRING "nettle 3.8.1"
#define PACKAGE_STRING "nettle 3.9.1"
/* Define to the one symbol short name of this package. */
#define PACKAGE_TARNAME "nettle"
@@ -158,7 +166,7 @@
#define PACKAGE_URL ""
/* Define to the version of this package. */
#define PACKAGE_VERSION "3.8.1"
#define PACKAGE_VERSION "3.9.1"
/* The size of `long', as computed by sizeof. */
#define SIZEOF_LONG __SIZEOF_LONG__
@@ -174,13 +182,10 @@
STACK_DIRECTION = 0 => direction of growth unknown */
/* #undef STACK_DIRECTION */
/* Define to 1 if all of the C90 standard headers exist (not just the ones
required in a freestanding environment). This macro is provided for
backward compatibility; new code need not use it. */
/* Define to 1 if you have the ANSI C header files. */
#define STDC_HEADERS 1
/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. This
macro is obsolete. */
/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. */
#define TIME_WITH_SYS_TIME 1
/* Defined if public key features are enabled */
+1 -1
View File
@@ -41,7 +41,7 @@ extern "C" {
/* Individual version numbers in decimal */
#define NETTLE_VERSION_MAJOR 3
#define NETTLE_VERSION_MINOR 8
#define NETTLE_VERSION_MINOR 9
#define NETTLE_USE_MINI_GMP 0
@@ -1,41 +0,0 @@
package androidx.navigation.ui
import android.annotation.SuppressLint
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.navigation.ActivityNavigator
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavOptions
@SuppressLint("PrivateResource")
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
is ActivityNavigator.Destination
) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(androidx.fragment.R.animator.fragment_open_enter)
.setExitAnim(androidx.fragment.R.animator.fragment_open_exit)
.setPopEnterAnim(androidx.fragment.R.animator.fragment_close_enter)
.setPopExitAnim(androidx.fragment.R.animator.fragment_close_exit)
}
val options = builder.build()
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.itemId, null, options)
// Return true only if the destination we've navigated to matches the MenuItem
navController.currentDestination?.matchDestination(item.itemId) == true
} catch (e: IllegalArgumentException) {
false
}
}
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
@@ -1,25 +0,0 @@
package androidx.swiperefreshlayout.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.ShapeDrawable
import android.util.AttributeSet
import androidx.core.graphics.ColorUtils
import rikka.core.res.resolveColor
@SuppressLint("PrivateResource")
class ThemedSwipeRefreshLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : SwipeRefreshLayout(context, attrs) {
init {
val surfaceColor =
context.theme.resolveColor(com.google.android.material.R.attr.colorSurface)
val overlayColor =
context.getColor(com.google.android.material.R.color.m3_popupmenu_overlay_color)
val backgroundColor = ColorUtils.compositeColors(overlayColor, surfaceColor)
(mCircleView.background as ShapeDrawable).paint.color = backgroundColor
setColorSchemeColors(context.theme.resolveColor(com.google.android.material.R.attr.colorAccent))
}
}
@@ -1,111 +0,0 @@
package com.davemorrissey.labs.subscaleview
import android.graphics.Bitmap
import android.graphics.Rect
import com.davemorrissey.labs.subscaleview.provider.InputProvider
/**
* Helper class used to set the source and additional attributes from a variety of sources. Supports
* use of a bitmap, asset, resource, external file or any other URI.
*
*
* When you are using a preview image, you must set the dimensions of the full size image on the
* ImageSource object for the full size image using the [.dimensions] method.
*/
class ImageSource {
val bitmap: Bitmap?
val provider: InputProvider?
private var sWidth = 0
private var sHeight = 0
var sRegion = Rect(0, 0, 0, 0)
private set
var isCached = false
private set
private constructor(bitmap: Bitmap, cached: Boolean) {
this.bitmap = bitmap
provider = null
sWidth = bitmap.width
sHeight = bitmap.height
isCached = cached
}
private constructor(provider: InputProvider) {
bitmap = null
this.provider = provider
}
/**
* Use a region of the source image. Region must be set independently for the full size image and the preview if
* you are using one.
*
* @param sRegion the region of the source image to be displayed.
* @return this instance for chaining.
*/
fun region(sRegion: Rect): ImageSource {
this.sRegion = sRegion
setInvariants()
return this
}
/**
* Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI
* and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare
* the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset.
*
* @param sWidth width of the source image.
* @param sHeight height of the source image.
* @return this instance for chaining.
*/
fun dimensions(sWidth: Int, sHeight: Int): ImageSource {
if (bitmap == null) {
this.sWidth = sWidth
this.sHeight = sHeight
}
setInvariants()
return this
}
private fun setInvariants() {
if (!sRegion.isEmpty) {
sWidth = sRegion.width()
sHeight = sRegion.height()
}
}
companion object {
/**
* Create an instance from an input provider.
*
* @param provider input stream provider.
* @return an [ImageSource] instance.
*/
fun provider(provider: InputProvider): ImageSource {
return ImageSource(provider)
}
/**
* Provide a loaded bitmap for display.
*
* @param bitmap bitmap to be displayed.
* @return an [ImageSource] instance.
*/
fun bitmap(bitmap: Bitmap): ImageSource {
return ImageSource(bitmap, false)
}
/**
* Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no
* longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso
* or Volley.
*
* @param bitmap bitmap to be displayed.
* @return an [ImageSource] instance.
*/
fun cachedBitmap(bitmap: Bitmap): ImageSource {
return ImageSource(bitmap, true)
}
}
}
@@ -1,15 +0,0 @@
package com.davemorrissey.labs.subscaleview
import android.graphics.PointF
import java.io.Serializable
/**
* Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.
*/
class ImageViewState(val scale: Float, center: PointF) : Serializable {
private val centerX: Float = center.x
private val centerY: Float = center.y
val center: PointF
get() = PointF(centerX, centerY)
}
@@ -1,50 +0,0 @@
package com.davemorrissey.labs.subscaleview.decoder
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import com.davemorrissey.labs.subscaleview.provider.InputProvider
/**
* Interface for image decoding classes.
*/
interface Decoder {
/**
* Initialise the decoder. When possible, perform initial setup work once in this method. The
* dimensions of the image must be returned.
*
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param provider Provider of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
fun init(context: Context, provider: InputProvider): Point
/**
* Decode a region of the image with the given sample size. This method is called off the UI
* thread so it can safely load the image on the current thread. It is called from
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
* implementations must be thread safe. Adding `synchronized` to the method signature
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
* called concurrently.
*
* @param sRect Source image rectangle to decode.
* @param sampleSize Sample size.
* @return The decoded region. It is safe to return null if decoding fails.
*/
fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap
/**
* Status check. Should return false before initialisation and after recycle.
*
* @return true if the decoder is ready to be used.
*/
val isReady: Boolean
/**
* This method will be called when the decoder is no longer required. It should clean up any
* resources still in use.
*/
fun recycle()
}
@@ -1,58 +0,0 @@
package com.davemorrissey.labs.subscaleview.decoder
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.provider.InputProvider
class ImageDecoder(private val cropBorders: Boolean, bitmapConfig: Bitmap.Config? = null) : Decoder {
init {
val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig
}
/**
* Initialise the decoder. When possible, perform initial setup work once in this method. The
* dimensions of the image must be returned.
*
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param provider Provider of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
override fun init(context: Context, provider: InputProvider): Point {
TODO()
}
/**
* Decode a region of the image with the given sample size. This method is called off the UI
* thread so it can safely load the image on the current thread. It is called from
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
* implementations must be thread safe. Adding `synchronized` to the method signature
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
* called concurrently.
*
* @param sRect Source image rectangle to decode.
* @param sampleSize Sample size.
* @return The decoded region. It is safe to return null if decoding fails.
*/
override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap {
TODO()
}
/**
* Status check. Should return false before initialisation and after recycle.
*
* @return true if the decoder is ready to be used.
*/
override val isReady: Boolean = TODO()
/**
* This method will be called when the decoder is no longer required. It should clean up any
* resources still in use.
*/
override fun recycle() {
TODO()
}
}
@@ -1,3 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
interface InputProvider
@@ -1,123 +0,0 @@
@file:Suppress("SameParameterValue")
package com.hippo.ehviewer
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapMerge
import splitties.preferences.DataStorePreferences
import splitties.preferences.edit
import java.util.Locale
import kotlin.reflect.KProperty
object Settings : DataStorePreferences(null) {
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
private val _favFlow = MutableSharedFlow<Unit>()
val favChangesFlow = _favFlow.debounce(1000)
var favCat by stringArrayPref("fav_cat", 10, "Favorites").emitTo(_favFlow)
var favCount by intArrayPref("fav_count", 10).emitTo(_favFlow)
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
var downloadScheme by stringOrNullPref("image_scheme", null)
var downloadAuthority by stringOrNullPref("image_authority", null)
var downloadPath by stringOrNullPref("image_path", null)
var downloadQuery by stringOrNullPref("image_query", null)
var downloadFragment by stringOrNullPref("image_fragment", null)
var archivePasswds by stringSetOrNullPref("archive_passwds")
var downloadDelay by intFromStrPref("download_delay", 0)
var gallerySite by intFromStrPref("gallery_site", 0).observed { updateWhenGallerySiteChanges() }
var multiThreadDownload by intFromStrPref("download_thread", 3)
var preloadImage by intFromStrPref("preload_image", 5)
var theme by intFromStrPref("theme", -1).observed { updateWhenThemeChanges() }
var listMode by intFromStrPref("list_mode", 0)
var detailSize by intFromStrPref("detail_size", 0)
var thumbResolution by intFromStrPref("thumb_resolution", 0)
var readCacheSize by intFromStrPref("read_cache_size", 640)
var launchPage by intFromStrPref("launch_page", 0)
var forceEhThumb by boolPref("force_eh_thumb", false)
var showComments by boolPref("show_gallery_comments", true)
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
var hideHvEvents by boolPref("hide_hv_events", false)
var showJpnTitle by boolPref("show_jpn_title", false)
var showGalleryPages by boolPref("show_gallery_pages", false)
var showTagTranslations by boolPref(KEY_SHOW_TAG_TRANSLATIONS, false).observed { updateWhenTagTranslationChanges() }
var meteredNetworkWarning by boolPref("cellular_network_warning", false)
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
var enabledSecurity by boolPref("enable_secure", false)
var useCIUpdateChannel by boolPref("ci_update_channel", false)
var mediaScan by boolPref("media_scan", false).observed { updateWhenKeepMediaStatusChanges() }
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
var qSSaveProgress by boolPref("qs_save_progress", true)
var saveParseErrorBody by boolPref("save_parse_error_body", true)
var saveCrashLog by boolPref("save_crash_log", true)
var security by boolPref("require_unlock", false)
var removeImageFiles by boolPref("include_pic", true)
var needSignIn by boolPref("need_sign_in", true)
var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
var downloadOriginImage by boolPref("download_origin_image", false)
var thumbSizeDp by intPref("thumb_size_", 120)
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
var defaultFavSlot by intPref("default_favorite_slot", -2)
var securityDelay by intPref("require_unlock_delay", 0)
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
var listThumbSize by intPref("list_tile_size", 40)
var searchCategory by intPref("search_pref", EhUtils.ALL_CATEGORY)
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
var updateIntervalDays by intPref("update_interval_days", 0)
var recentDownloadLabel by stringOrNullPref("recent_download_label", null)
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
var displayName by stringOrNullPref("display_name", null)
var avatar by stringOrNullPref("avatar", null)
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
var lastDawnDay by longPref("last_dawn_day", 0)
var lastUpdateDay by longPref("last_update_day", 0)
init {
if ("CN" == Locale.getDefault().country) {
edit {
if (KEY_SHOW_TAG_TRANSLATIONS !in prefs) showTagTranslations = true
}
}
}
interface Delegate<R> {
val flowGetter: () -> Flow<Unit>
operator fun getValue(thisRef: Any?, prop: KProperty<*>?): R
operator fun setValue(thisRef: Any?, prop: KProperty<*>?, value: R)
}
private fun intFromStrPref(key: String, defValue: Int) = object : Delegate<Int> {
override val flowGetter: () -> Flow<Unit>
private var _value by stringPref(key, defValue.toString()).also { flowGetter = { it.changesFlow() } }
override fun getValue(thisRef: Any?, prop: KProperty<*>?) = _value.toIntOrNull() ?: defValue
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Int) { _value = value.toString() }
}
private fun intArrayPref(key: String, count: Int) = object : Delegate<IntArray> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): IntArray = _value.map { it.value }.toIntArray()
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: IntArray) {
check(value.size == count)
edit { value.zip(_value) { v, d -> d.value = v } }
}
}
private fun stringArrayPref(key: String, count: Int, defMetaValue: String) = object : Delegate<Array<String>> {
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
private var _value = (0 until count).map { stringPref("${key}_$it", "$defMetaValue $it") }.toTypedArray()
override fun getValue(thisRef: Any?, prop: KProperty<*>?): Array<String> = _value.map { it.value }.toTypedArray()
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Array<String>) {
check(value.size == count)
edit { value.zip(_value) { v, d -> d.value = v } }
}
}
}
@@ -1,104 +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
import android.webkit.CookieManager
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
object EhCookieStore : CookieJar {
private val manager = CookieManager.getInstance()
fun signOut() = manager.removeAllCookies(null)
fun contains(url: HttpUrl, name: String) = get(url).any { it.name == name }
fun get(url: HttpUrl): List<Cookie> {
val cookies = manager.getCookie(url.toString())
return if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(url, it) }.filterNot { it.name == KEY_UTMP_NAME }
} else {
emptyList()
}
}
fun hasSignedIn(): Boolean {
val url = EhUrl.HOST_E.toHttpUrl()
return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH)
}
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
const val KEY_IGNEOUS = "igneous"
private const val KEY_STAR = "star"
private const val KEY_CONTENT_WARNING = "nw"
private const val CONTENT_WARNING_NOT_SHOW = "1"
private const val KEY_UTMP_NAME = "__utmp"
private val sTipsCookie = Cookie.Builder().apply {
name(KEY_CONTENT_WARNING)
value(CONTENT_WARNING_NOT_SHOW)
domain(EhUrl.DOMAIN_E)
path("/")
expiresAt(Long.MAX_VALUE)
}.build()
fun copyNecessaryCookies() {
val cookie = get(EhUrl.HOST_E.toHttpUrl()).filter { it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH || it.name == KEY_IGNEOUS }
cookie.forEach { manager.setCookie(EhUrl.HOST_EX, it.toString()) }
flush()
}
fun deleteCookie(url: HttpUrl, name: String) {
manager.setCookie(url.toString(), "$name=;Max-Age=0")
}
fun addCookie(cookie: Cookie) {
if (EhUrl.DOMAIN_E in cookie.domain) manager.setCookie(EhUrl.HOST_E, cookie.toString()) else manager.setCookie(EhUrl.DOMAIN_EX, cookie.toString())
}
fun flush() = manager.flush()
fun getCookieHeader(url: HttpUrl): String {
val cookies = loadForRequest(url)
val cookieHeader = StringBuilder()
var i = 0
val size = cookies.size
while (i < size) {
if (i > 0) {
cookieHeader.append("; ")
}
val cookie = cookies[i]
cookieHeader.append(cookie.name).append('=').append(cookie.value)
i++
}
return cookieHeader.toString()
}
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.filterNot { it.name == KEY_UTMP_NAME }.forEach { manager.setCookie(url.toString(), it.toString()) }.also { flush() }
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val checkTips = EhUrl.DOMAIN_E in url.host
return get(url).run {
if (checkTips) {
filterNot { it.name == KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
} else {
this
}
}
}
}
@@ -1,525 +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
import android.util.Log
import arrow.fx.coroutines.parMap
import arrow.fx.coroutines.parZip
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.InsufficientFundsException
import com.hippo.ehviewer.client.exception.NotLoggedInException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parser.ArchiveParser
import com.hippo.ehviewer.client.parser.EventPaneParser
import com.hippo.ehviewer.client.parser.FavoritesParser
import com.hippo.ehviewer.client.parser.ForumsParser
import com.hippo.ehviewer.client.parser.GalleryApiParser
import com.hippo.ehviewer.client.parser.GalleryDetailParser
import com.hippo.ehviewer.client.parser.GalleryListParser
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
import com.hippo.ehviewer.client.parser.GalleryPageApiParser
import com.hippo.ehviewer.client.parser.GalleryPageParser
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
import com.hippo.ehviewer.client.parser.HomeParser
import com.hippo.ehviewer.client.parser.ProfileParser
import com.hippo.ehviewer.client.parser.RateGalleryResult
import com.hippo.ehviewer.client.parser.SignInParser
import com.hippo.ehviewer.client.parser.TorrentParser
import com.hippo.ehviewer.client.parser.TorrentResult
import com.hippo.ehviewer.client.parser.VoteCommentResult
import com.hippo.ehviewer.client.parser.VoteTagParser
import com.hippo.ehviewer.cronet.awaitBodyFully
import com.hippo.ehviewer.cronet.cronetRequest
import com.hippo.ehviewer.cronet.execute
import com.hippo.ehviewer.dailycheck.showEventNotification
import com.hippo.ehviewer.dailycheck.today
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.StatusCodeException
import com.hippo.ehviewer.util.isCronetSupported
import io.ktor.utils.io.pool.DirectByteBufferPool
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.serialization.json.add
import kotlinx.serialization.json.addJsonArray
import kotlinx.serialization.json.put
import moe.tarsin.coroutines.removeAllSuspend
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.Headers
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import org.jsoup.Jsoup
import splitties.init.appCtx
import java.io.File
import java.nio.ByteBuffer
import kotlin.math.ceil
private const val TAG = "EhEngine"
private const val MAX_REQUEST_SIZE = 25
private const val SAD_PANDA_DISPOSITION = "inline; filename=\"sadpanda.jpg\""
private const val SAD_PANDA_TYPE = "image/gif"
private const val SAD_PANDA_LENGTH = "9615"
private const val KOKOMADE_URL = "https://exhentai.org/img/kokomade.jpg"
private const val U_CONFIG_TEXT = "Selected Profile"
private val MEDIA_TYPE_JPEG: MediaType = "image/jpeg".toMediaType()
private fun rethrowExactly(code: Int, headers: Headers, body: String, e: Throwable): Nothing {
// Don't translate coroutine cancellation
if (e is CancellationException) throw e
// Check sad panda
if (SAD_PANDA_DISPOSITION == headers["Content-Disposition"] && SAD_PANDA_TYPE == headers["Content-Type"] && SAD_PANDA_LENGTH == headers["Content-Length"]) {
throw EhException("Sad Panda")
}
// Check sad panda(without panda)
if ("text/html; charset=UTF-8" == headers["Content-Type"] && "0" == headers["Content-Length"] && EhUtils.isExHentai) {
throw EhException("Sad Panda\n(without panda)")
}
// Check kokomade
if (body.contains(KOKOMADE_URL)) {
throw EhException("今回はここまで ${appCtx.getString(R.string.kokomade_tip)}".trimIndent())
}
// Check Gallery Not Available
if (body.contains("Gallery Not Available - ")) {
val error = GalleryNotAvailableParser.parse(body)
if (!error.isNullOrBlank()) {
throw EhException(error)
}
}
// Check bad response code
if (code >= 400) {
throw StatusCodeException(code)
}
if (e is ParseException) {
if (body.isEmpty()) {
throw EhException(appCtx.getString(R.string.error_empty_html))
} else if ("<" !in body) {
throw EhException(body)
} else {
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e)
throw EhException(appCtx.getString(R.string.error_parse_error))
}
}
// We can't translate it, rethrow it anyway
throw e
}
val httpContentPool = DirectByteBufferPool(16, 16384)
suspend inline fun <T> fetchCompat(url: String, referer: String? = null, crossinline parser: (ByteBuffer) -> T): T {
return if (isCronetSupported) {
cronetRequest(url, referer).execute {
val buffer = httpContentPool.borrow()
try {
awaitBodyFully {
buffer.put(it)
}
buffer.flip()
parser(buffer)
} finally {
httpContentPool.recycle(buffer)
}
}
} else {
ehRequest(url, referer).execute {
val buffer = httpContentPool.borrow()
try {
while (true) {
if (body.source().read(buffer) == -1) break
}
buffer.flip()
parser(buffer)
} finally {
httpContentPool.recycle(buffer)
}
}
}
}
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
private suspend inline fun <T> Request.executeAndParsingWith(block: suspend String.() -> T): T {
Log.d(TAG, url.toString())
return execute {
val body = body.string()
try {
block(body)
} catch (e: Exception) {
rethrowExactly(code, headers, body, e)
}
}
}
object EhEngine {
suspend fun getOriginalImageUrl(url: String, referer: String?): String {
Log.d(TAG, url)
return ehRequest(url, referer).executeNoRedirect {
header("Location")?.apply {
if (contains("bounce_login")) {
throw NotLoggedInException()
}
} ?: throw InsufficientFundsException()
}
}
suspend fun getTorrentList(url: String, gid: Long, token: String?): TorrentResult {
val referer = EhUrl.getGalleryDetailUrl(gid, token)
return fetchCompat(url, referer, TorrentParser::parse)
}
suspend fun getArchiveList(url: String, gid: Long, token: String?) = ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
.executeAndParsingWith(ArchiveParser::parse)
.apply { funds = funds ?: ehRequest(EhUrl.URL_FUNDS).executeAndParsingWith(HomeParser::parseFunds) }
suspend fun getImageLimits() = parZip(
{ fetchCompat(EhUrl.URL_HOME, parser = HomeParser::parse) },
{ ehRequest(EhUrl.URL_FUNDS).executeAndParsingWith(HomeParser::parseFunds) },
{ limits, funds -> HomeParser.Result(limits, funds) },
)
suspend fun getNews(parse: Boolean) = ehRequest(EhUrl.URL_NEWS, EhUrl.REFERER_E)
.executeAndParsingWith { if (parse) EventPaneParser.parse(this) else null }
suspend fun getProfile(): ProfileParser.Result {
val url = ehRequest(EhUrl.URL_FORUMS).executeAndParsingWith(ForumsParser::parse)
return ehRequest(url, EhUrl.URL_FORUMS).executeAndParsingWith(ProfileParser::parse)
}
suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {
runSuspendCatching {
ehRequest(url).executeAndParsingWith { check(contains(U_CONFIG_TEXT)) { "Unable to load config from $url!" } }
}.onFailure {
// It may get redirected when accessing ex for the first time
if (url == EhUrl.URL_UCONFIG_EX) {
it.printStackTrace()
ehRequest(url).executeAndParsingWith { check(contains(U_CONFIG_TEXT)) { "Unable to load config from $url!" } }
} else {
throw it
}
}
}
suspend fun getGalleryPage(
url: String,
gid: Long,
token: String?,
): GalleryPageParser.Result {
val referer = EhUrl.getGalleryDetailUrl(gid, token)
return ehRequest(url, referer).executeAndParsingWith(GalleryPageParser::parse)
}
suspend fun getGalleryList(url: String) = ehRequest(url, EhUrl.referer)
.executeAndParsingWith(GalleryListParser::parse)
.apply { fillGalleryList(galleryInfoList, url, true) }
suspend fun getGalleryDetail(url: String) = ehRequest(url, EhUrl.referer).executeAndParsingWith {
EventPaneParser.parse(this)?.let {
Settings.lastDawnDay = today
showEventNotification(it)
}
GalleryDetailParser.parse(this)
}
suspend fun getPreviewList(url: String) = ehRequest(url, EhUrl.referer).executeAndParsingWith {
GalleryDetailParser.parsePreviewList(this) to GalleryDetailParser.parsePreviewPages(this)
}
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer)
.executeAndParsingWith(FavoritesParser::parse)
.apply { fillGalleryList(galleryInfoList, url, false) }
suspend fun signIn(username: String, password: String): String {
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
val url = EhUrl.API_SIGN_IN
val origin = "https://forums.e-hentai.org"
return ehRequest(url, referer, origin) {
formBody {
add("referer", referer)
add("b", "")
add("bt", "")
add("UserName", username)
add("PassWord", password)
add("CookieDate", "1")
}
}.executeAndParsingWith(SignInParser::parse)
}
suspend fun commentGallery(url: String, comment: String, id: String?) = ehRequest(url, url, EhUrl.origin) {
formBody {
if (id == null) {
add("commenttext_new", comment)
} else {
add("commenttext_edit", comment)
add("edit_comment", id)
}
}
}.executeAndParsingWith {
val document = Jsoup.parse(this)
val elements = document.select("#chd + p")
if (elements.size > 0) {
throw EhException(elements[0].text())
}
GalleryDetailParser.parseComments(document)
}
/**
* @param dstCat -1 for delete, 0 - 9 for cloud favorite, others throw Exception
* @param note max 250 characters
*/
suspend fun modifyFavorites(
gid: Long,
token: String?,
dstCat: Int = -1,
note: String = "",
) {
val catStr: String = when (dstCat) {
-1 -> "favdel"
in 0..9 -> dstCat.toString()
else -> throw EhException("Invalid dstCat: $dstCat")
}
val url = EhUrl.getAddFavorites(gid, token)
return ehRequest(url, url, EhUrl.origin) {
formBody {
add("favcat", catStr)
add("favnote", note)
// apply=Add+to+Favorites is not necessary, just use apply=Apply+Changes all the time
add("apply", "Apply Changes")
add("update", "1")
}
}.executeAndParsingWith { }
}
suspend fun downloadArchive(
gid: Long,
token: String?,
or: String?,
res: String?,
isHAtH: Boolean,
): String? {
if (or.isNullOrEmpty()) {
throw EhException("Invalid form param or: $or")
}
if (res.isNullOrEmpty()) {
throw EhException("Invalid res: $res")
}
val url = EhUrl.getDownloadArchive(gid, token, or)
val referer = EhUrl.getGalleryDetailUrl(gid, token)
val request = ehRequest(url, referer, EhUrl.origin) {
formBody {
if (isHAtH) {
add("hathdl_xres", res)
} else {
add("dltype", res)
if (res == "org") {
add("dlcheck", "Download Original Archive")
} else {
add("dlcheck", "Download Resample Archive")
}
}
}
}
var result = request.executeAndParsingWith(ArchiveParser::parseArchiveUrl)
if (!isHAtH) {
if (result == null) {
// Wait for the server to prepare archives
delay(1000)
result = request.executeAndParsingWith(ArchiveParser::parseArchiveUrl)
if (result == null) {
throw EhException("Archive unavailable")
}
}
return result
}
return null
}
suspend fun resetImageLimits() = ehRequest(EhUrl.URL_HOME) {
formBody {
add("reset_imagelimit", "Reset Limit")
}
}.executeAndParsingWith(HomeParser::parseResetLimits)
suspend fun modifyFavorites(url: String, gidArray: LongArray, dstCat: Int): FavoritesParser.Result {
val catStr: String = when (dstCat) {
-1 -> "delete"
in 0..9 -> "fav$dstCat"
else -> throw EhException("Invalid dstCat: $dstCat")
}
return ehRequest(url, url, EhUrl.origin) {
formBody {
add("ddact", catStr)
gidArray.forEach { add("modifygids[]", it.toString()) }
}
}.executeAndParsingWith(FavoritesParser::parse).apply { fillGalleryList(galleryInfoList, url, false) }
}
suspend fun getGalleryPageApi(
gid: Long,
index: Int,
pToken: String,
showKey: String?,
previousPToken: String?,
): GalleryPageApiParser.Result {
val referer = if (index > 0 && previousPToken != null) EhUrl.getPageUrl(gid, index - 1, previousPToken) else null
return ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
jsonBody {
put("method", "showpage")
put("gid", gid)
put("page", index + 1)
put("imgkey", pToken)
put("showkey", showKey)
}
}.executeAndParsingWith(GalleryPageApiParser::parse)
}
suspend fun rateGallery(
apiUid: Long,
apiKey: String?,
gid: Long,
token: String?,
rating: Float,
): RateGalleryResult = ehRequest(EhUrl.apiUrl, EhUrl.getGalleryDetailUrl(gid, token), EhUrl.origin) {
jsonBody {
put("method", "rategallery")
put("apiuid", apiUid)
put("apikey", apiKey)
put("gid", gid)
put("token", token)
put("rating", ceil((rating * 2).toDouble()).toInt())
}
}.executeAndParsingWith(String::parseAs)
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String) = galleryInfoList.chunked(MAX_REQUEST_SIZE).parMap {
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
jsonBody {
put("method", "gdata")
array("gidlist") {
it.forEach {
addJsonArray {
add(it.gid)
add(it.token)
}
}
}
put("namespace", 1)
}
}.executeAndParsingWith { GalleryApiParser.parse(this, it) }
}
suspend fun voteComment(
apiUid: Long,
apiKey: String?,
gid: Long,
token: String?,
commentId: Long,
commentVote: Int,
): VoteCommentResult = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
jsonBody {
put("method", "votecomment")
put("apiuid", apiUid)
put("apikey", apiKey)
put("gid", gid)
put("token", token)
put("comment_id", commentId)
put("comment_vote", commentVote)
}
}.executeAndParsingWith(String::parseAs)
suspend fun voteTag(
apiUid: Long,
apiKey: String?,
gid: Long,
token: String?,
tags: String?,
vote: Int,
) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
jsonBody {
put("method", "taggallery")
put("apiuid", apiUid)
put("apikey", apiKey)
put("gid", gid)
put("token", token)
put("tags", tags)
put("vote", vote)
}
}.executeAndParsingWith(VoteTagParser::parse)
suspend fun getGalleryToken(gid: Long, gtoken: String?, page: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
jsonBody {
put("method", "gtoken")
array("pagelist") {
addJsonArray {
add(gid)
add(gtoken)
add(page + 1)
}
}
}
}.executeAndParsingWith(GalleryTokenApiParser::parse)
/**
* @param image Must be jpeg
*/
suspend fun imageSearch(image: File, uss: Boolean, osc: Boolean) = ehRequest(EhUrl.imageSearchUrl, EhUrl.referer, EhUrl.origin) {
multipartBody {
setType(MultipartBody.FORM)
addFormDataPart("sfile", "a.jpg", image.asRequestBody(MEDIA_TYPE_JPEG))
if (uss) addFormDataPart("fs_similar", "on")
if (osc) addFormDataPart("fs_covers", "on")
addFormDataPart("f_sfile", "File Search")
}
}.executeAndParsingWith(GalleryListParser::parse).apply { fillGalleryList(galleryInfoList, EhUrl.imageSearchUrl, true) }
private suspend fun fillGalleryList(list: MutableList<BaseGalleryInfo>, url: String, filter: Boolean) {
// Filter title and uploader
if (filter) list.removeAllSuspend { EhFilter.filterTitle(it) || EhFilter.filterUploader(it) }
var hasTags = false
var hasPages = false
var hasRated = false
for (gi in list) {
if (gi.simpleTags != null) {
hasTags = true
}
if (gi.pages != 0) {
hasPages = true
}
if (gi.rated) {
hasRated = true
}
}
val needApi = filter && EhFilter.needTags() && !hasTags || Settings.showGalleryPages && !hasPages || hasRated
if (needApi) fillGalleryListByApi(list, url)
// Filter tag, thumbnail mode need filter uploader again
if (filter) list.removeAllSuspend { EhFilter.filterUploader(it) || EhFilter.filterTag(it) || EhFilter.filterTagNamespace(it) }
}
suspend fun modifyFavoritesRange(galleryList: List<Pair<Long, String>>, dstCat: Int) {
galleryList.forEach { (gid, token) -> modifyFavorites(gid, token, dstCat) }
}
}
@@ -1,128 +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
import android.util.Log
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArrayBuilder
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.okio.decodeFromBufferedSource
import okhttp3.Call
import okhttp3.FormBody
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.executeAsync
import okio.BufferedSource
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
inline fun ehRequest(url: String, referer: String? = null, origin: String? = null, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply {
addHeader("User-Agent", CHROME_USER_AGENT)
addHeader("Accept", CHROME_ACCEPT)
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
referer?.let { addHeader("Referer", it) }
origin?.let { addHeader("Origin", it) }
}.apply(builder).build()
inline fun Request.Builder.formBody(builder: FormBody.Builder.() -> Unit) = post(FormBody.Builder().apply(builder).build())
inline fun Request.Builder.multipartBody(builder: MultipartBody.Builder.() -> Unit) = post(MultipartBody.Builder().apply(builder).build())
val MEDIA_TYPE_JSON: MediaType = "application/json; charset=utf-8".toMediaType()
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
inline fun Request.Builder.jsonBody(builder: JsonObjectBuilder.() -> Unit) = post(buildJsonObject(builder).toString().toRequestBody(MEDIA_TYPE_JSON))
const val TAG_CALL = "CancellableCallReadingScope"
// This scoped response reading scope suspend function aimed at cancelling the call immediately once coroutine is cancelled.
// i.e., bind a call with a suspended coroutine, and launch another coroutine to execute reading
// Once origin coroutine is cancelled, then call cancelled, socket is closed, the reading coroutine will encounter [IOException]
// Since Okio is not suspendable/cancellable, also okhttp, [executeAsync] only make [Request -> Response] cancellable, reading a Response is still not cancellable
// Considering interrupting is not safe and performant, See https://github.com/Kotlin/kotlinx.coroutines/issues/3551#issuecomment-1353245978, we have such [usingCancellable]
// TODO: Remove logging after this function stable
suspend inline fun <R> Call.usingCancellable(crossinline block: suspend Response.() -> R): R = executeAsync().use {
val call = this
Log.d(TAG_CALL, "Got response of call$call")
coroutineScope {
suspendCancellableCoroutine<R> { cont ->
launch {
val r = runCatching {
Log.d(TAG_CALL, "Reading response of call$call")
block(it)
}
if (!isCanceled() && cont.isActive) {
r.exceptionOrNull()?.let {
Log.e(TAG_CALL, "Reading response of call$call failed!")
cont.resumeWithException(it)
}
r.getOrNull()?.let {
Log.d(TAG_CALL, "Reading response of call$call succeed!")
cont.resume(it)
}
} else if (isCanceled() && cont.isActive) {
Log.e(TAG_CALL, "call$call cancelled but coroutine is Active!")
} else if (!isCanceled() && !cont.isActive) {
Log.e(TAG_CALL, "call$call not cancelled but coroutine is Dead!")
} else {
Log.d(TAG_CALL, "Reading response of call$call cancelled")
}
}
cont.invokeOnCancellation {
Log.d(TAG_CALL, "Cancelling call$call")
cancel()
}
}
}.apply {
Log.d(TAG_CALL, "Reading response of call$call finished!")
}
}
suspend inline fun <R> Request.execute(block: Response.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return okHttpClient.newCall(this).executeAsync().use(block)
}
suspend inline fun <R> Request.executeNonCache(crossinline block: suspend Response.() -> R) = baseOkHttpClient.newCall(this).usingCancellable(block)
suspend inline fun <R> Request.executeNoRedirect(block: Response.() -> R) = noRedirectOkHttpClient.newCall(this).executeAsync().use(block)
suspend inline fun <reified T> Request.executeAndParseAs() = execute { parseAs<T>() }
inline fun <reified T> Response.parseAs(): T = body.source().parseAs()
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
val json = Json { ignoreUnknownKeys = true }
const val CHROME_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36"
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.9"
const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.5"
@@ -1,37 +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 android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryComment(
// 0 for uploader comment. can't vote
var id: Long = 0,
var score: Int = 0,
var editable: Boolean = false,
var voteUpAble: Boolean = false,
var voteUpEd: Boolean = false,
var voteDownAble: Boolean = false,
var voteDownEd: Boolean = false,
var uploader: Boolean = false,
var voteState: String? = null,
var time: Long = 0,
var user: String? = null,
var comment: String? = null,
var lastEdited: Long = 0,
) : Parcelable
@@ -1,5 +0,0 @@
package com.hippo.ehviewer.client.exception
import com.hippo.ehviewer.R
class CloudflareBypassException : EhException(R.string.cloudflare_bypass_failed)
@@ -1,38 +0,0 @@
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.BaseGalleryInfo
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.ParseException
object GalleryListParser {
suspend fun parse(body: String) = runCatching {
parseGalleryInfoList(body).apply {
galleryInfoList.onEach {
if (it.favoriteSlot == NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
it.favoriteSlot = LOCAL_FAVORITED
}
it.generateSLang()
}
}
}.getOrElse {
if (body.contains("<p>You do not have any watched tags")) {
throw EhException(R.string.gallery_list_empty_hit_subscription)
}
if (body.contains("No hits found</p>")) {
throw EhException(R.string.gallery_list_empty_hit)
}
throw ParseException("Can't parse gallery list", body, it)
}
}
class GalleryListResult(
val prev: String?,
val next: String?,
val galleryInfoList: ArrayList<BaseGalleryInfo>,
)
private external fun parseGalleryInfoList(e: String): GalleryListResult
@@ -1,59 +0,0 @@
/*
* Copyright 2019 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.parser
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parseAs
import com.hippo.ehviewer.util.unescapeXml
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object GalleryPageApiParser {
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
fun parse(body: String): Result {
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error, body) }
val res = body.parseAs<JsonResult>()
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
groupValues[1].trim().unescapeXml()
}
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKey)?.run {
groupValues[1].trim().unescapeXml()
}
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.originImageUrl)?.run {
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
}
if (!imageUrl.isNullOrEmpty()) {
return Result(imageUrl, skipHathKey, originImageUrl)
} else {
throw ParseException("Parse image url and skip hath key error", body)
}
}
@Serializable
private data class JsonResult(
@SerialName("i3")
val imageUrl: String,
@SerialName("i6")
val skipHathKey: String,
@SerialName("i7")
val originImageUrl: String,
)
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
}
@@ -1,69 +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.parser
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.unescapeXml
import java.util.regex.Pattern
object GalleryPageParser {
private val PATTERN_IMAGE_URL = Pattern.compile("<img[^>]*src=\"([^\"]+)\" style")
private val PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^)]+)'\\)")
private val PATTERN_ORIGIN_IMAGE_URL =
Pattern.compile("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
// TODO Not sure about the size of show keys
private val PATTERN_SHOW_KEY = Pattern.compile("var showkey=\"([0-9a-z]+)\";")
fun parse(body: String): Result {
var m = PATTERN_IMAGE_URL.matcher(body)
val imageUrl = if (m.find()) {
m.group(1)!!.trim().unescapeXml()
} else {
null
}
m = PATTERN_SKIP_HATH_KEY.matcher(body)
val skipHathKey = if (m.find()) {
m.group(1)!!.trim().unescapeXml()
} else {
null
}
m = PATTERN_ORIGIN_IMAGE_URL.matcher(body)
val originImageUrl = if (m.find()) {
m.group(1)!!.unescapeXml() + "fullimg.php" + m.group(2)!!.unescapeXml()
} else {
null
}
m = PATTERN_SHOW_KEY.matcher(body)
val showKey = if (m.find()) {
m.group(1)
} else {
null
}
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {
Result(imageUrl, skipHathKey, originImageUrl, showKey)
} else {
throw ParseException("Parse image url and show error", body)
}
}
class Result(
val imageUrl: String,
val skipHathKey: String?,
val originImageUrl: String?,
val showKey: String,
)
}
@@ -1,124 +0,0 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.cronet
import android.net.http.HttpEngine
import android.net.http.HttpException
import android.net.http.UrlRequest
import android.net.http.UrlResponseInfo
import android.os.Build
import androidx.annotation.RequiresExtension
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.client.CHROME_ACCEPT
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
import com.hippo.ehviewer.client.CHROME_USER_AGENT
import com.hippo.ehviewer.client.EhCookieStore
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.utils.io.pool.DirectByteBufferPool
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
private const val TAG = "CronetRequest"
val pool = DirectByteBufferPool(32)
val cronetHttpClient: HttpEngine = HttpEngine.Builder(appCtx).apply {
setEnableBrotli(true)
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
setStoragePath(cache.absolutePath)
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
setUserAgent(CHROME_USER_AGENT)
}.build()
val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService
class CronetRequest {
lateinit var consumer: (ByteBuffer) -> Unit
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
lateinit var request: UrlRequest
lateinit var onError: (Throwable) -> Unit
lateinit var readerCont: Continuation<Unit>
val callback = object : UrlRequest.Callback {
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
logcat(tag = TAG) { "Redirected to $url" }
req.followRedirect()
}
override fun onResponseStarted(req: UrlRequest, info: UrlResponseInfo) {
onResponse(info)
}
override fun onReadCompleted(req: UrlRequest, info: UrlResponseInfo, data: ByteBuffer) {
data.flip()
consumer(data)
}
override fun onSucceeded(req: UrlRequest, info: UrlResponseInfo) {
readerCont.resume(Unit)
}
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: HttpException) {
onError(e)
}
override fun onCanceled(req: UrlRequest, info: UrlResponseInfo?) {
// No-op
}
}
}
inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
request = cronetHttpClient.newUrlRequestBuilder(url, cronetHttpClientExecutor, callback).apply {
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
addHeader("Accept", CHROME_ACCEPT)
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
referer?.let { addHeader("Referer", it) }
}.apply(conf).build()
}
suspend inline fun CronetRequest.awaitBodyFully(crossinline callback: (ByteBuffer) -> Unit) {
val buffer = pool.borrow()
return try {
suspendCancellableCoroutine { cont ->
consumer = {
callback(it)
buffer.clear()
request.read(buffer)
}
onError = { readerCont.resumeWithException(it) }
readerCont = cont
request.read(buffer)
}
} finally {
pool.recycle(buffer)
}
}
suspend inline fun CronetRequest.copyToChannel(chan: FileChannel, crossinline listener: ((Int) -> Unit) = {}) = awaitBodyFully {
val bytes = chan.write(it)
listener(bytes)
}
suspend inline fun <R> CronetRequest.execute(crossinline callback: suspend CronetRequest.(UrlResponseInfo) -> R): R {
contract {
callsInPlace(callback, InvocationKind.EXACTLY_ONCE)
}
return coroutineScope {
suspendCancellableCoroutine { cont ->
onResponse = { launch { cont.resume(callback(it)) } }
cont.invokeOnCancellation { request.cancel() }
onError = { cont.resumeWithException(it) }
request.start()
}
}
}
@@ -1,30 +0,0 @@
package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "OK_HTTP_3_COOKIE")
data class Cookie(
@ColumnInfo(name = "NAME")
var name: String,
@ColumnInfo(name = "VALUE")
var value: String,
@ColumnInfo(name = "EXPIRES_AT")
var expiresAt: Long,
@ColumnInfo(name = "DOMAIN")
var domain: String,
@ColumnInfo(name = "PATH")
var path: String,
@ColumnInfo(name = "SECURE")
var secure: Boolean,
@ColumnInfo(name = "HTTP_ONLY")
var httpOnly: Boolean,
@ColumnInfo(name = "PERSISTENT")
var persistent: Boolean,
@ColumnInfo(name = "HOST_ONLY")
var hostOnly: Boolean,
@PrimaryKey
@ColumnInfo(name = "_id")
var id: Long?,
)
@@ -1,10 +0,0 @@
package com.hippo.ehviewer.dao
import androidx.room.Dao
import androidx.room.Query
@Dao
interface CookiesDao {
@Query("SELECT * FROM OK_HTTP_3_COOKIE")
suspend fun list(): List<Cookie>
}
@@ -1,105 +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.image
import android.graphics.ColorSpace
import android.graphics.ImageDecoder
import android.graphics.ImageDecoder.ImageInfo
import android.graphics.ImageDecoder.Source
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import com.hippo.ehviewer.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import splitties.init.appCtx
import java.nio.ByteBuffer
import kotlin.math.min
class Image private constructor(private val src: CloseableSource) {
var mObtainedDrawable: Drawable? =
ImageDecoder.decodeDrawable(src.source) { decoder: ImageDecoder, info: ImageInfo, _: Source ->
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
// Allocating hardware bitmap may cause a crash on framework versions prior to Android Q
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
}
decoder.setTargetColorSpace(colorSpace)
decoder.setTargetSampleSize(
calculateSampleSize(info, 2 * screenHeight, 2 * screenWidth),
)
}.also {
if (it !is Animatable) src.close()
}
private set
val size: Int
get() = mObtainedDrawable!!.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 }
@Synchronized
fun recycle() {
(mObtainedDrawable as? Animatable)?.stop()
(mObtainedDrawable as? BitmapDrawable)?.bitmap?.recycle()
mObtainedDrawable?.callback = null
if (mObtainedDrawable is Animatable) src.close()
mObtainedDrawable = null
}
companion object {
fun calculateSampleSize(info: ImageInfo, targetHeight: Int, targetWeight: Int): Int {
return min(
info.size.width / targetWeight,
info.size.height / targetHeight,
).coerceAtLeast(1)
}
private val imageSearchMaxSize = appCtx.resources.getDimensionPixelOffset(R.dimen.image_search_max_size)
val imageSearchDecoderSampleListener =
ImageDecoder.OnHeaderDecodedListener { decoder, info, _ ->
decoder.setTargetSampleSize(
calculateSampleSize(info, imageSearchMaxSize, imageSearchMaxSize),
)
}
val screenWidth = appCtx.resources.displayMetrics.widthPixels
val screenHeight = appCtx.resources.displayMetrics.heightPixels
val isWideColorGamut = appCtx.resources.configuration.isScreenWideColorGamut
var colorSpace = ColorSpace.get(
if (isWideColorGamut && ReaderPreferences.wideColorGamut().get()) {
ColorSpace.Named.DISPLAY_P3
} else {
ColorSpace.Named.SRGB
},
)
fun decode(src: CloseableSource): Image? {
return runCatching {
Image(src)
}.onFailure {
src.close()
it.printStackTrace()
}.getOrNull()
}
}
interface CloseableSource : AutoCloseable {
val source: Source
}
}
external fun rewriteGifSource(buffer: ByteBuffer)
external fun rewriteGifSource2(fd: Int)
@@ -1,13 +0,0 @@
package com.hippo.ehviewer.ktbuilder
import com.chuckerteam.chucker.api.ChuckerInterceptor
import okhttp3.Cache
import okhttp3.OkHttpClient
import okio.FileSystem
import okio.Path
import splitties.init.appCtx
inline fun httpClient(builder: OkHttpClient.Builder.() -> Unit): OkHttpClient = OkHttpClient.Builder().apply(builder).build()
inline fun httpClient(client: OkHttpClient, builder: OkHttpClient.Builder.() -> Unit): OkHttpClient = client.newBuilder().apply(builder).build()
fun OkHttpClient.Builder.cache(directory: Path, maxSize: Long, fileSystem: FileSystem = FileSystem.SYSTEM) = cache(Cache(directory, maxSize, fileSystem))
inline fun OkHttpClient.Builder.chunker(builder: ChuckerInterceptor.Builder.() -> Unit) = addInterceptor(ChuckerInterceptor.Builder(appCtx).apply(builder).build())
@@ -1,38 +0,0 @@
package com.hippo.ehviewer.legacy
import arrow.fx.coroutines.release
import arrow.fx.coroutines.resource
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.dao.Cookie
import com.hippo.ehviewer.dao.CookiesDatabase
import splitties.arch.room.roomDb
import splitties.init.appCtx
private const val legacyCookiesDB = "okhttp3-cookie.db"
private fun Cookie.toOkHttp3Cookie(): okhttp3.Cookie {
return okhttp3.Cookie.Builder().apply {
name(name)
value(value)
expiresAt(expiresAt)
if (hostOnly) hostOnlyDomain(domain) else domain(domain)
path(path)
if (secure) secure()
if (httpOnly) httpOnly()
}.build()
}
suspend fun migrateCookies() {
if (legacyCookiesDB !in appCtx.databaseList()) return
resource {
roomDb<CookiesDatabase>(legacyCookiesDB)
} release {
it.close()
appCtx.deleteDatabase(legacyCookiesDB)
} use { database ->
database.cookiesDao().list().forEach {
EhCookieStore.addCookie(it.toOkHttp3Cookie())
}
EhCookieStore.flush()
}
}
@@ -1,123 +0,0 @@
package com.hippo.ehviewer.ui
import android.os.Bundle
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.core.view.WindowCompat
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.ui.login.CookieSignInScene
import com.hippo.ehviewer.ui.login.SelectSiteScreen
import com.hippo.ehviewer.ui.login.SignInScreen
import com.hippo.ehviewer.ui.login.WebViewSignInScreen
import com.hippo.ehviewer.ui.settings.AboutScreen
import com.hippo.ehviewer.ui.settings.AdvancedScreen
import com.hippo.ehviewer.ui.settings.BaseScreen
import com.hippo.ehviewer.ui.settings.DownloadScreen
import com.hippo.ehviewer.ui.settings.EhScreen
import com.hippo.ehviewer.ui.settings.FilterScreen
import com.hippo.ehviewer.ui.settings.LicenseScreen
import com.hippo.ehviewer.ui.settings.MyTagsScreen
import com.hippo.ehviewer.ui.settings.PrivacyScreen
import com.hippo.ehviewer.ui.settings.UConfigScreen
class ConfigureActivity : EhActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setMD3Content {
val navController = rememberNavController()
val windowSizeClass = calculateWindowSizeClass(this)
CompositionLocalProvider(LocalNavController provides navController) {
NavHost(
navController = navController,
startDestination = if (Settings.needSignIn) {
if (EhCookieStore.hasSignedIn()) SELECT_SITE_ROUTE_NAME else SIGN_IN_ROUTE_NAME
} else {
BASE_SETTINGS_SCREEN
},
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up, tween(200)) },
exitTransition = { fadeOut(tween(200)) },
popEnterTransition = { fadeIn(tween(200)) },
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Down, tween(200)) },
) {
composable(SIGN_IN_ROUTE_NAME) {
SignInScreen(windowSizeClass)
}
composable(WEBVIEW_SIGN_IN_ROUTE_NAME) {
WebViewSignInScreen()
}
composable(COOKIE_SIGN_IN_ROUTE_NAME) {
CookieSignInScene(windowSizeClass)
}
composable(SELECT_SITE_ROUTE_NAME) {
SelectSiteScreen()
}
composable(BASE_SETTINGS_SCREEN) {
BaseScreen()
}
composable(ABOUT_SETTINGS_SCREEN) {
AboutScreen()
}
composable(LICENSE_SCREEN) {
LicenseScreen()
}
composable(PRIVACY_SETTINGS_SCREEN) {
PrivacyScreen()
}
composable(ADVANCED_SETTINGS_SCREEN) {
AdvancedScreen()
}
composable(DOWNLOAD_SETTINGS_SCREEN) {
DownloadScreen()
}
composable(EH_SETTINGS_SCREEN) {
EhScreen()
}
composable(UCONFIG_SCREEN) {
UConfigScreen()
}
composable(MYTAGS_SCREEN) {
MyTagsScreen()
}
composable(FILTER_SCREEN) {
FilterScreen()
}
composable(FINISH_ROUTE_NAME) {
SideEffect {
finish()
}
}
}
}
}
}
}
val LocalNavController = compositionLocalOf<NavController> { error("CompositionLocal LocalNavController not present!") }
const val BASE_SETTINGS_SCREEN = "Base"
const val EH_SETTINGS_SCREEN = "Eh"
const val DOWNLOAD_SETTINGS_SCREEN = "Download"
const val PRIVACY_SETTINGS_SCREEN = "Privacy"
const val ADVANCED_SETTINGS_SCREEN = "Advanced"
const val ABOUT_SETTINGS_SCREEN = "About"
const val LICENSE_SCREEN = "License"
const val UCONFIG_SCREEN = "UConfig"
const val MYTAGS_SCREEN = "Mytags"
const val FILTER_SCREEN = "Filter"
const val SIGN_IN_ROUTE_NAME = "SignIn"
const val WEBVIEW_SIGN_IN_ROUTE_NAME = "WebViewSignIn"
const val COOKIE_SIGN_IN_ROUTE_NAME = "CookieSignIn"
const val SELECT_SITE_ROUTE_NAME = "SelectSite"
const val FINISH_ROUTE_NAME = "Finish"
@@ -1,103 +0,0 @@
package com.hippo.ehviewer.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.thumbUrl
import com.hippo.ehviewer.databinding.InfoSheetBinding
import com.hippo.ehviewer.ui.scene.navWithUrl
import com.hippo.ehviewer.util.tellClipboardWithToast
private const val INDEX_URL = 2
private const val INDEX_PARENT = 9
private fun GalleryDetail.generateContent() = arrayOf(
R.string.key_gid to gid.toString(),
R.string.key_token to token,
R.string.key_url to EhUrl.getGalleryDetailUrl(gid, token),
R.string.key_title to title,
R.string.key_title_jpn to titleJpn,
R.string.key_thumb to thumbUrl,
R.string.key_category to EhUtils.getCategory(category),
R.string.key_uploader to uploader,
R.string.key_posted to posted,
R.string.key_parent to parent,
R.string.key_visible to visible,
R.string.key_language to language,
R.string.key_pages to pages.toString(),
R.string.key_size to size,
R.string.key_favorite_count to favoriteCount.toString(),
R.string.key_favorited to (favoriteSlot > LOCAL_FAVORITED).toString(),
R.string.key_rating_count to ratingCount.toString(),
R.string.key_rating to rating.toString(),
R.string.key_torrents to torrentCount.toString(),
R.string.key_torrent_url to torrentUrl,
R.string.favorite_name to favoriteName,
)
class GalleryInfoBottomSheet(private val detail: GalleryDetail) : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = InfoSheetBinding.inflate(layoutInflater, container, false).apply {
content.setMD3Content {
Column(modifier = Modifier.fillMaxWidth()) {
val context = LocalContext.current
val navController = remember { findNavController() }
Text(
text = stringResource(id = R.string.gallery_info),
modifier = Modifier.align(Alignment.CenterHorizontally),
style = MaterialTheme.typography.titleLarge,
)
val data = remember(detail) { detail.generateContent() }
ProvideTextStyle(MaterialTheme.typography.labelLarge) {
LazyColumn {
itemsIndexed(data) { index, (key, content) ->
Row(
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.keyline_margin)).clickable {
if (index == INDEX_PARENT) {
if (content != null) {
navController.navWithUrl(content)
}
} else {
context tellClipboardWithToast content
if (index == INDEX_URL) {
// Save it to avoid detect the gallery
Settings.clipboardTextHashCode = data[index].hashCode()
}
}
}.fillMaxWidth(),
) {
Text(stringResource(id = key), modifier = Modifier.width(90.dp).padding(8.dp))
Text(content.orEmpty(), modifier = Modifier.padding(8.dp))
}
}
}
}
}
}
}.root
}
@@ -1,447 +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.annotation.SuppressLint
import android.app.assist.AssistContent
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.verify.domain.DomainVerificationManager
import android.content.pm.verify.domain.DomainVerificationUserState
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.customview.widget.Openable
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.onNavDestinationSelected2
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.Settings.launchPage
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.hippo.ehviewer.databinding.ActivityMainBinding
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.image.Image
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
import com.hippo.ehviewer.ui.scene.GalleryListScene
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.ehviewer.ui.scene.ProgressScene
import com.hippo.ehviewer.ui.scene.navAnimated
import com.hippo.ehviewer.ui.scene.navWithUrl
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.getParcelableExtraCompat
import com.hippo.ehviewer.util.getUrlFromClipboard
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import splitties.systemservices.clipboardManager
import splitties.systemservices.connectivityManager
import java.io.File
import java.io.FileOutputStream
class MainActivity : EhActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private fun saveImageToTempFile(uri: Uri): File? {
val src = ImageDecoder.createSource(contentResolver, uri)
val bitmap = runCatching {
ImageDecoder.decodeBitmap(src, Image.imageSearchDecoderSampleListener)
}.getOrNull() ?: return null
val temp = AppConfig.createTempFile() ?: return null
return runCatching {
FileOutputStream(temp).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
temp
}.getOrElse {
it.printStackTrace()
null
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
lifecycleScope.launchUI {
if (!handleIntent(intent)) {
if (intent != null && Intent.ACTION_VIEW == intent.action) {
if (intent.data != null) {
val url = intent.data.toString()
EditTextDialogBuilder(this@MainActivity, url, "")
.setTitle(R.string.error_cannot_parse_the_url)
.setPositiveButton(android.R.string.copy) { _: DialogInterface?, _: Int ->
this@MainActivity.addTextToClipboard(
url,
false,
)
}
.show()
}
}
}
}
}
private fun handleIntent(intent: Intent?): Boolean {
when (intent?.action) {
Intent.ACTION_VIEW -> {
val uri = intent.data ?: return false
return navController.navWithUrl(uri.toString())
}
Intent.ACTION_SEND -> {
val type = intent.type
if ("text/plain" == type) {
val builder = ListUrlBuilder()
builder.keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
navController.navAnimated(
R.id.galleryListScene,
builder.toStartArgs(),
)
return true
} else if (type != null && type.startsWith("image/")) {
val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
if (null != uri) {
val temp = saveImageToTempFile(uri)
if (null != temp) {
val builder = ListUrlBuilder()
builder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
builder.imagePath = temp.path
builder.isUseSimilarityScan = true
navController.navAnimated(
R.id.galleryListScene,
builder.toStartArgs(),
)
return true
}
}
}
}
DownloadService.ACTION_START_DOWNLOADSCENE -> {
val args = intent.getBundleExtra(DownloadService.ACTION_START_DOWNLOADSCENE_ARGS)
navController.navAnimated(R.id.nav_downloads, args)
}
}
return false
}
private fun setNavGraph() {
navController.apply {
graph = navInflater.inflate(R.navigation.nav_graph).apply {
when (
when (val value = launchPage) {
0 -> GalleryListScene.ACTION_HOMEPAGE
1 -> GalleryListScene.ACTION_SUBSCRIPTION
2 -> GalleryListScene.ACTION_WHATS_HOT
3 -> GalleryListScene.ACTION_TOP_LIST
else -> throw IllegalStateException("Unexpected value: $value")
}
) {
GalleryListScene.ACTION_HOMEPAGE -> setStartDestination(R.id.nav_homepage)
GalleryListScene.ACTION_SUBSCRIPTION -> setStartDestination(R.id.nav_subscription)
GalleryListScene.ACTION_WHATS_HOT -> setStartDestination(R.id.nav_whats_hot)
GalleryListScene.ACTION_TOP_LIST -> setStartDestination(R.id.nav_toplist)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navController = navHostFragment.navController
if (!EhUtils.needSignedIn()) setNavGraph()
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
binding.navView.setupWithNavController(navController)
// Trick: Tweak NavigationUI to disable multiple backstack
binding.navView.setNavigationItemSelectedListener {
val navigationView = binding.navView
val handled = navigationView.checkedItem?.itemId == it.itemId || onNavDestinationSelected2(it, navController)
if (handled) {
val parent = navigationView.parent
if (parent is Openable) {
parent.close()
} else {
@SuppressLint("RestrictedApi")
val bottomSheetBehavior = NavigationUI.findBottomSheetBehavior(navigationView)
if (bottomSheetBehavior != null) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
}
}
handled
}
// Trick End
if (savedInstanceState == null) {
if (intent.action != Intent.ACTION_MAIN) {
onNewIntent(intent)
}
checkDownloadLocation()
if (Settings.meteredNetworkWarning) {
checkMeteredNetwork()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!Settings.appLinkVerifyTip) {
try {
checkAppLinkVerify()
} catch (ignored: PackageManager.NameNotFoundException) {
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
private fun checkAppLinkVerify() {
val manager = getSystemService(DomainVerificationManager::class.java)
val userState = manager.getDomainVerificationUserState(packageName) ?: return
var hasUnverified = false
val hostToStateMap = userState.hostToStateMap
for (key in hostToStateMap.keys) {
val stateValue = hostToStateMap[key]
if (stateValue == null || stateValue == DomainVerificationUserState.DOMAIN_STATE_VERIFIED || stateValue == DomainVerificationUserState.DOMAIN_STATE_SELECTED) {
continue
}
hasUnverified = true
break
}
if (hasUnverified) {
BaseDialogBuilder(this)
.setTitle(R.string.app_link_not_verified_title)
.setMessage(R.string.app_link_not_verified_message)
.setPositiveButton(R.string.open_settings) { _: DialogInterface?, _: Int ->
try {
val intent = Intent(
android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:$packageName"),
)
startActivity(intent)
} catch (t: Throwable) {
val intent = Intent(
android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:$packageName"),
)
startActivity(intent)
}
}
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.dont_show_again) { _: DialogInterface?, _: Int ->
Settings.appLinkVerifyTip = true
}
.show()
}
}
private fun checkDownloadLocation() {
val uniFile = downloadLocation
// null == uniFile for first start
if (uniFile.ensureDir()) {
return
}
BaseDialogBuilder(this)
.setTitle(R.string.waring)
.setMessage(R.string.invalid_download_location)
.setPositiveButton(R.string.get_it, null)
.show()
}
private fun checkMeteredNetwork() {
if (connectivityManager.isActiveNetworkMetered) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Snackbar.make(
binding.drawView,
R.string.metered_network_warning,
Snackbar.LENGTH_LONG,
)
.setAction(R.string.settings) {
val panelIntent =
Intent(android.provider.Settings.Panel.ACTION_INTERNET_CONNECTIVITY)
startActivity(panelIntent)
}
.show()
} else {
showTip(R.string.metered_network_warning, BaseScene.LENGTH_LONG)
}
}
}
private val loginLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
setNavGraph()
}
override fun onResume() {
if (EhUtils.needSignedIn()) {
loginLauncher.launch(Intent(this, ConfigureActivity::class.java))
}
super.onResume()
lifecycleScope.launch {
delay(300)
mDrawerOnBackPressedCallback.remove()
onBackPressedDispatcher.addCallback(mDrawerOnBackPressedCallback)
checkClipboardUrl()
}
}
private suspend fun checkClipboardUrl() {
val text = clipboardManager.getUrlFromClipboard(this)
val hashCode = text?.hashCode() ?: 0
if (text != null && hashCode != 0 && Settings.clipboardTextHashCode != hashCode) {
val result1 = GalleryDetailUrlParser.parse(text, false)
var launch: (() -> Unit)? = null
if (result1 != null) {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)
args.putLong(GalleryDetailScene.KEY_GID, result1.gid)
args.putString(GalleryDetailScene.KEY_TOKEN, result1.token)
launch = { navController.navAnimated(R.id.galleryDetailScene, args) }
}
val result2 = GalleryPageUrlParser.parse(text, false)
if (result2 != null) {
val args = Bundle()
args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN)
args.putLong(ProgressScene.KEY_GID, result2.gid)
args.putString(ProgressScene.KEY_PTOKEN, result2.pToken)
args.putInt(ProgressScene.KEY_PAGE, result2.page)
launch = { navController.navAnimated(R.id.progressScene, args) }
}
launch?.let {
withUIContext {
val snackbar = Snackbar.make(
binding.drawView,
R.string.clipboard_gallery_url_snack_message,
Snackbar.LENGTH_SHORT,
)
snackbar.setAction(R.string.clipboard_gallery_url_snack_action) {
it()
}
snackbar.show()
}
}
}
Settings.clipboardTextHashCode = hashCode
}
fun addAboveSnackView(view: View) {
binding.absnacker.addAboveSnackView(view)
}
fun removeAboveSnackView(view: View) {
binding.absnacker.removeAboveSnackView(view)
}
fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {
binding.drawView.setDrawerLockMode(lockMode, edgeGravity)
}
fun getDrawerLockMode(edgeGravity: Int): Int {
return binding.drawView.getDrawerLockMode(edgeGravity)
}
fun isDrawerOpen(drawerGravity: Int) = binding.drawView.isDrawerOpen(drawerGravity)
fun openDrawer(drawerGravity: Int) {
binding.drawView.openDrawer(drawerGravity)
}
fun closeDrawer(drawerGravity: Int) {
binding.drawView.closeDrawer(drawerGravity)
}
fun toggleDrawer(drawerGravity: Int) {
if (isDrawerOpen(drawerGravity)) {
closeDrawer(drawerGravity)
} else {
openDrawer(drawerGravity)
}
}
fun clearNavCheckedItem() {
binding.navView.setCheckedItem(R.id.nav_stub)
}
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
showTip(getString(id), length, useToast)
}
/**
* If activity is running, show snack bar, otherwise show toast
*/
fun showTip(message: CharSequence, length: Int, useToast: Boolean = false) {
findViewById<View>(R.id.snackbar)?.takeUnless { useToast }?.apply {
Snackbar.make(
this,
message,
if (length == BaseScene.LENGTH_LONG) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT,
).show()
} ?: Toast.makeText(
this,
message,
if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT,
).show()
}
var mShareUrl: String? = null
override fun onProvideAssistContent(outContent: AssistContent?) {
super.onProvideAssistContent(outContent)
mShareUrl?.let { outContent?.webUri = Uri.parse(mShareUrl) }
}
private val mDrawerOnBackPressedCallback =
object : OnBackPressedCallback(false), DrawerLayout.DrawerListener {
val slideThreshold = 0.05
override fun handleOnBackPressed() {
binding.drawView.closeDrawers()
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
isEnabled = slideOffset > slideThreshold
}
override fun onDrawerOpened(drawerView: View) {}
override fun onDrawerClosed(drawerView: View) {}
override fun onDrawerStateChanged(newState: Int) {}
}
}
@@ -1,11 +0,0 @@
package com.hippo.ehviewer.ui
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import com.google.accompanist.themeadapter.material3.Mdc3Theme
// To make sure compose theme and legacy view theme are in sync, we only use Mdc3Theme
fun ComposeView.setMD3Content(content: @Composable () -> Unit) = setContent { Mdc3Theme(content = content) }
fun ComponentActivity.setMD3Content(content: @Composable () -> Unit) = setContent { Mdc3Theme(content = content) }
@@ -1,18 +0,0 @@
package com.hippo.ehviewer.ui
import android.os.Bundle
import com.hippo.ehviewer.ui.webview.WebViewScreen
class WebViewActivity : EhActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val url = intent.extras?.getString(KEY_URL) ?: return
setMD3Content {
WebViewScreen(url = url) { finish() }
}
}
companion object {
const val KEY_URL = "url"
}
}
@@ -1,217 +0,0 @@
package com.hippo.ehviewer.ui.legacy
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout.AttachedBehavior
import androidx.core.util.ObjectsCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.drakeet.drawer.FullDraggableHelper
import com.google.android.material.snackbar.Snackbar.SnackbarLayout
import com.hippo.ehviewer.util.dp2px
class AboveSnackerLayout : FrameLayout, AttachedBehavior, FullDraggableHelper.Callback {
private val helper: FullDraggableHelper
private var drawerLayout: DrawerLayout? = null
private var mAboveSnackViewList: MutableList<View>? = null
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {
helper = FullDraggableHelper(context, this)
}
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) :
super(context!!, attrs, defStyle) {
helper = FullDraggableHelper(context, this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
ensureDrawerLayout()
}
private fun ensureDrawerLayout() {
val parent = parent.parent
check(parent is DrawerLayout) { "This $this must be added to a DrawerLayout" }
drawerLayout = parent
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return helper.onInterceptTouchEvent(event)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
return helper.onTouchEvent(event)
}
override fun getDrawerMainContainer(): View {
return this
}
override fun isDrawerOpen(gravity: Int): Boolean {
return drawerLayout!!.isDrawerOpen(gravity)
}
override fun hasEnabledDrawer(gravity: Int): Boolean {
return drawerLayout!!.getDrawerLockMode(gravity) == DrawerLayout.LOCK_MODE_UNLOCKED &&
findDrawerWithGravity(gravity) != null
}
override fun offsetDrawer(gravity: Int, offset: Float) {
setDrawerToOffset(gravity, offset)
drawerLayout!!.invalidate()
}
override fun smoothOpenDrawer(gravity: Int) {
drawerLayout!!.openDrawer(gravity, true)
}
override fun smoothCloseDrawer(gravity: Int) {
drawerLayout!!.closeDrawer(gravity, true)
}
override fun onDrawerDragging() {
val drawerListeners = drawerListeners
if (drawerListeners != null) {
val listenerCount = drawerListeners.size
for (i in listenerCount - 1 downTo 0) {
drawerListeners[i].onDrawerStateChanged(DrawerLayout.STATE_DRAGGING)
}
}
}
private val drawerListeners: List<DrawerListener>?
get() = try {
val listeners = DrawerLayout::class.java.getDeclaredField("mListeners")
listeners.isAccessible = true
listeners[drawerLayout] as List<DrawerListener>
} catch (e: Exception) {
// throw to let developer know the api is changed
throw RuntimeException(e)
}
private fun setDrawerToOffset(gravity: Int, offset: Float) {
val drawerView = findDrawerWithGravity(gravity)
val slideOffsetPercent =
(offset / ObjectsCompat.requireNonNull(drawerView).width).coerceIn(0f, 1f)
try {
val method = DrawerLayout::class.java.getDeclaredMethod(
"moveDrawerToOffset",
View::class.java,
Float::class.javaPrimitiveType,
)
method.isAccessible = true
method.invoke(drawerLayout, drawerView, slideOffsetPercent)
drawerView!!.visibility = VISIBLE
} catch (e: Exception) {
// throw to let developer know the api is changed
throw RuntimeException(e)
}
}
// Copied from DrawerLayout
private fun findDrawerWithGravity(gravity: Int): View? {
val absHorizontalGravity = GravityCompat.getAbsoluteGravity(
gravity,
ViewCompat.getLayoutDirection(drawerLayout!!),
) and Gravity.HORIZONTAL_GRAVITY_MASK
val childCount = drawerLayout!!.childCount
for (i in 0 until childCount) {
val child = drawerLayout!!.getChildAt(i)
val childAbsGravity = getDrawerViewAbsoluteGravity(child)
if (childAbsGravity and Gravity.HORIZONTAL_GRAVITY_MASK == absHorizontalGravity) {
return child
}
}
return null
}
// Copied from DrawerLayout
private fun getDrawerViewAbsoluteGravity(drawerView: View): Int {
val gravity = (drawerView.layoutParams as DrawerLayout.LayoutParams).gravity
return GravityCompat.getAbsoluteGravity(
gravity,
ViewCompat.getLayoutDirection(drawerLayout!!),
)
}
fun addAboveSnackView(view: View) {
if (null == mAboveSnackViewList) {
mAboveSnackViewList = ArrayList()
}
mAboveSnackViewList!!.add(view)
}
fun removeAboveSnackView(view: View) {
if (null == mAboveSnackViewList) {
return
}
mAboveSnackViewList!!.remove(view)
}
val aboveSnackViewCount: Int
get() = if (null == mAboveSnackViewList) 0 else mAboveSnackViewList!!.size
fun getAboveSnackViewAt(index: Int): View? {
return if (null == mAboveSnackViewList || index < 0 || index >= mAboveSnackViewList!!.size) {
null
} else {
mAboveSnackViewList!![index]
}
}
override fun getBehavior(): Behavior {
return Behavior()
}
class Behavior : CoordinatorLayout.Behavior<AboveSnackerLayout>() {
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: AboveSnackerLayout,
dependency: View,
): Boolean {
return dependency is SnackbarLayout
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: AboveSnackerLayout,
dependency: View,
): Boolean {
for (i in 0 until child.aboveSnackViewCount) {
val view = child.getAboveSnackViewAt(i)
if (view != null) {
val translationY =
(dependency.translationY - dependency.height - dp2px(view.context, 8f))
.coerceAtMost(0f)
ViewCompat.animate(view).setInterpolator(FastOutSlowInInterpolator())
.translationY(translationY).setDuration(150).start()
}
}
return false
}
override fun onDependentViewRemoved(
parent: CoordinatorLayout,
child: AboveSnackerLayout,
dependency: View,
) {
for (i in 0 until child.aboveSnackViewCount) {
val view = child.getAboveSnackViewAt(i)
if (view != null) {
ViewCompat.animate(view).setInterpolator(FastOutSlowInInterpolator())
.translationY(0f).setDuration(75).start()
}
}
}
}
}
@@ -1,149 +0,0 @@
/*
* Copyright (C) 2015 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.legacy
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import androidx.annotation.Keep
import com.google.android.material.math.MathUtils
import com.hippo.ehviewer.R
import kotlin.math.roundToInt
/**
* @param context used to get the configuration for the drawable from
*/
class AddDeleteDrawable(context: Context, color: Int) : Drawable() {
private val mPaint = Paint()
private val mPath = Path()
private val mSize: Int
private var mProgress = 0f
private var mAutoUpdateMirror = false
private var mVerticalMirror = false
init {
val resources = context.resources
mSize = resources.getDimensionPixelSize(R.dimen.add_size)
val barThickness = resources.getDimension(R.dimen.add_thickness).roundToInt().toFloat()
mPaint.color = color
mPaint.style = Paint.Style.STROKE
mPaint.strokeJoin = Paint.Join.MITER
mPaint.strokeCap = Paint.Cap.BUTT
mPaint.strokeWidth = barThickness
val halfSize = (mSize / 2).toFloat()
mPath.moveTo(0f, -halfSize)
mPath.lineTo(0f, halfSize)
mPath.moveTo(-halfSize, 0f)
mPath.lineTo(halfSize, 0f)
}
override fun draw(canvas: Canvas) {
val bounds = bounds
val canvasRotate = if (mVerticalMirror) {
MathUtils.lerp(270f, 135f, mProgress)
} else {
MathUtils.lerp(0f, 135f, mProgress)
}
canvas.save()
canvas.translate(bounds.centerX().toFloat(), bounds.centerY().toFloat())
canvas.rotate(canvasRotate)
canvas.drawPath(mPath, mPaint)
canvas.restore()
}
fun setColor(color: Int) {
mPaint.color = color
invalidateSelf()
}
override fun setAlpha(alpha: Int) {
mPaint.alpha = alpha
}
override fun setColorFilter(cf: ColorFilter?) {
mPaint.colorFilter = cf
}
override fun getIntrinsicHeight(): Int {
return mSize * 6 / 5
}
override fun getIntrinsicWidth(): Int {
return mSize * 6 / 5
}
/**
* If set, canvas is flipped when progress reached to end and going back to start.
*/
private fun setVerticalMirror(verticalMirror: Boolean) {
mVerticalMirror = verticalMirror
}
fun setAutoUpdateMirror(autoUpdateMirror: Boolean) {
mAutoUpdateMirror = autoUpdateMirror
}
@get:Keep
@set:Keep
var progress: Float
get() = mProgress
set(progress) {
if (mAutoUpdateMirror) {
if (progress == 1f) {
setVerticalMirror(true)
} else if (progress == 0f) {
setVerticalMirror(false)
}
}
mProgress = progress
invalidateSelf()
}
fun setAdd(duration: Long) {
setShape(false, duration)
}
fun setDelete(duration: Long) {
setShape(true, duration)
}
private fun setShape(delete: Boolean, duration: Long) {
if (!(!delete && mProgress == 0f || delete && mProgress == 1f)) {
val endProgress = if (delete) 1f else 0f
if (duration <= 0) {
progress = endProgress
} else {
val oa = ObjectAnimator.ofFloat(this, "progress", endProgress)
oa.duration = duration
oa.setAutoCancel(true)
oa.start()
}
}
}
@Deprecated(
"Deprecated in Java",
ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat"),
)
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
}
@@ -1,112 +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.legacy
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.hippo.ehviewer.Settings
import kotlin.math.roundToInt
class AutoStaggeredGridLayoutManager(columnSize: Int, orientation: Int) :
StaggeredGridLayoutManager(1, orientation) {
private var mColumnSize = columnSize
private var mColumnSizeChanged = true
private var mStrategy = 0
var supportsPredictiveItemAnimations = true
fun setColumnSize(columnSize: Int) {
if (columnSize == mColumnSize) {
return
}
mColumnSize = columnSize
mColumnSizeChanged = true
}
fun setStrategy(strategy: Int) {
if (strategy == mStrategy) {
return
}
mStrategy = strategy
mColumnSizeChanged = true
}
override fun supportsPredictiveItemAnimations(): Boolean {
return supportsPredictiveItemAnimations && super.supportsPredictiveItemAnimations()
}
override fun onMeasure(
recycler: RecyclerView.Recycler,
state: RecyclerView.State,
widthSpec: Int,
heightSpec: Int,
) {
if (mColumnSizeChanged && mColumnSize > 0) {
val totalSpace = if (orientation == VERTICAL) {
check(
View.MeasureSpec.EXACTLY == View.MeasureSpec.getMode(
widthSpec,
),
) { "RecyclerView need a fixed width for AutoStaggeredGridLayoutManager" }
View.MeasureSpec.getSize(widthSpec) - paddingRight - paddingLeft
} else {
check(
View.MeasureSpec.EXACTLY == View.MeasureSpec.getMode(
heightSpec,
),
) { "RecyclerView need a fixed height for AutoStaggeredGridLayoutManager" }
View.MeasureSpec.getSize(heightSpec) - paddingTop - paddingBottom
}
val spanCount = when (mStrategy) {
STRATEGY_MIN_SIZE -> getSpanCountForMinSize(
totalSpace,
mColumnSize,
)
STRATEGY_SUITABLE_SIZE -> getSpanCountForSuitableSize(
totalSpace,
mColumnSize,
)
else -> getSpanCountForMinSize(
totalSpace,
mColumnSize,
)
}
setSpanCount(spanCount)
mColumnSizeChanged = false
}
super.onMeasure(recycler, state, widthSpec, heightSpec)
}
}
const val STRATEGY_MIN_SIZE = 0
const val STRATEGY_SUITABLE_SIZE = 1
fun getSpanCountForSuitableSize(total: Int, single: Int): Int {
return (total / single.toFloat()).roundToInt().coerceAtLeast(1)
}
@Composable
fun calculateSuitableSpanCount(): Int {
val totalSpace = LocalConfiguration.current.screenWidthDp
return getSpanCountForSuitableSize(totalSpace, Settings.thumbSizeDp)
}
fun getSpanCountForMinSize(total: Int, single: Int): Int {
return (total / single).coerceAtLeast(1)
}
@@ -1,46 +0,0 @@
/*
* Copyright (C) 2015 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.legacy
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.widget.FrameLayout
class BringOutTransition(contentLayout: FrameLayout, mSearchLayout: SearchLayout) :
ViewTransition(contentLayout, mSearchLayout) {
override fun startAnimations(hiddenView: View, shownView: View) {
mAnimator1 = hiddenView.animate().alpha(0f).scaleY(0.7f).scaleX(0.7f).apply {
setDuration(ANIMATE_TIME).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
hiddenView.visibility = View.GONE
mAnimator1 = null
}
}).start()
}
shownView.alpha = 0f
shownView.scaleX = 0.7f
shownView.scaleY = 0.7f
shownView.visibility = View.VISIBLE
mAnimator2 = shownView.animate().alpha(1f).scaleX(1f).scaleY(1f).apply {
setDuration(ANIMATE_TIME).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
mAnimator2 = null
}
}).start()
}
}
}
@@ -1,44 +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.legacy
import android.content.Context
import android.view.LayoutInflater
import android.widget.CheckBox
import com.hippo.ehviewer.R
class CheckBoxDialogBuilder(
context: Context,
message: String?,
checkText: String?,
checked: Boolean,
) : BaseDialogBuilder(
context,
) {
private val mCheckBox: CheckBox
val isChecked: Boolean
get() = mCheckBox.isChecked
init {
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_checkbox_builder, null)
setView(view)
setMessage(message)
mCheckBox = view.findViewById(R.id.checkbox)
mCheckBox.text = checkText
mCheckBox.isChecked = checked
view.setOnClickListener { mCheckBox.toggle() }
}
}
@@ -1,53 +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.legacy
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
class DrawerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : FrameLayout(context, attrs) {
private var mMaxWidth = 0
init {
val a = context.obtainStyledAttributes(attrs, SIZE_ATTRS)
mMaxWidth = a.getDimensionPixelOffset(0, 0)
a.recycle()
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
var widthSpec = widthSpec
when (MeasureSpec.getMode(widthSpec)) {
MeasureSpec.EXACTLY -> {}
MeasureSpec.AT_MOST -> widthSpec = MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(widthSpec).coerceAtMost(mMaxWidth),
MeasureSpec.EXACTLY,
)
MeasureSpec.UNSPECIFIED ->
widthSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY)
}
// Let super sort out the height
super.onMeasure(widthSpec, heightSpec)
}
companion object {
private val SIZE_ATTRS = intArrayOf(android.R.attr.maxWidth)
}
}
@@ -1,72 +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.legacy
import android.content.Context
import android.content.DialogInterface
import android.view.KeyEvent
import android.view.LayoutInflater
import android.widget.EditText
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener
import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputLayout
import com.hippo.ehviewer.R
class EditTextDialogBuilder(
context: Context,
text: String?,
hint: String?,
) : BaseDialogBuilder(
context,
),
OnEditorActionListener {
private val mTextInputLayout: TextInputLayout
val editText: EditText
private var mDialog: AlertDialog? = null
val text: String
get() = editText.text.toString()
fun setError(error: CharSequence?) {
mTextInputLayout.error = error
}
override fun create(): AlertDialog {
mDialog = super.create()
return mDialog as AlertDialog
}
override fun onEditorAction(v: TextView?, p1: Int, event: KeyEvent?): Boolean {
return if (mDialog != null) {
val button = mDialog!!.getButton(DialogInterface.BUTTON_POSITIVE)
button?.performClick()
true
} else {
false
}
}
init {
val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittext_builder, null)
setView(view)
mTextInputLayout = view as TextInputLayout
editText = view.findViewById(R.id.edit_text)
editText.setText(text)
editText.setSelection(editText.text.length)
editText.setOnEditorActionListener(this)
mTextInputLayout.hint = hint
}
}
@@ -1,432 +0,0 @@
/*
* Copyright (C) 2015 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.legacy
import android.animation.Animator
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.view.animation.Interpolator
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.hippo.ehviewer.R
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.hippo.ehviewer.util.getParcelableCompat
class FabLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : ViewGroup(context, attrs, defStyleAttr), View.OnClickListener {
private var mFabSize = 0
private var mFabMiniSize = 0
private var mIntervalPrimary = 0
private var mIntervalSecondary = 0
private var mExpanded = true
private var mAutoCancel = true
private var mHidePrimaryFab = false
private var mMainFabCenterY = -1f
private var mOnExpandListeners = arrayListOf<OnExpandListener>()
private var mOnClickFabListener: OnClickFabListener? = null
private val mOnBackPressedCallback = object : OnBackPressedCallback(false), OnExpandListener {
override fun handleOnBackPressed() {
isExpanded = false
}
override fun onExpand(expanded: Boolean) {
isEnabled = expanded
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
(context as ComponentActivity).onBackPressedDispatcher.addCallback(mOnBackPressedCallback)
mOnExpandListeners.add(mOnBackPressedCallback)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mOnBackPressedCallback.remove()
mOnExpandListeners.remove(mOnBackPressedCallback)
}
init {
isSoundEffectsEnabled = false
clipToPadding = false
mFabSize = context.resources.getDimensionPixelOffset(R.dimen.fab_size)
mFabMiniSize = context.resources.getDimensionPixelOffset(R.dimen.fab_min_size)
mIntervalPrimary =
context.resources.getDimensionPixelOffset(R.dimen.fab_layout_primary_margin)
mIntervalSecondary =
context.resources.getDimensionPixelOffset(R.dimen.fab_layout_secondary_margin)
}
override fun addView(child: View, index: Int, params: LayoutParams) {
check(child is FloatingActionButton) { "FloatingActionBarLayout should only " + "contain FloatingActionButton, but try to add " + child.javaClass.name }
super.addView(child, index, params)
}
val primaryFab: FloatingActionButton?
get() = getChildAt(childCount - 1) as? FloatingActionButton
private val secondaryFabCount: Int
get() = 0.coerceAtLeast(childCount - 1)
private fun getSecondaryFabAt(index: Int): FloatingActionButton? {
return if (index < 0 || index >= secondaryFabCount) {
null
} else {
getChildAt(index) as FloatingActionButton
}
}
fun setSecondaryFabVisibilityAt(index: Int, visible: Boolean) {
getSecondaryFabAt(index)?.run {
if (visible && visibility == GONE) {
animate().cancel()
visibility = if (mExpanded) VISIBLE else INVISIBLE
} else if (!visible && visibility != GONE) {
animate().cancel()
visibility = GONE
}
}
}
private fun getChildMeasureSpec(parentMeasureSpec: Int): Int {
val parentMode = MeasureSpec.getMode(parentMeasureSpec)
val parentSize = MeasureSpec.getSize(parentMeasureSpec)
val childMode: Int
val childSize: Int
when (parentMode) {
MeasureSpec.AT_MOST, MeasureSpec.EXACTLY -> {
childMode = MeasureSpec.AT_MOST
childSize = parentSize
}
MeasureSpec.UNSPECIFIED -> {
childMode = MeasureSpec.UNSPECIFIED
childSize = parentSize
}
else -> {
childMode = MeasureSpec.AT_MOST
childSize = parentSize
}
}
return MeasureSpec.makeMeasureSpec(childSize, childMode)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec)
val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec)
measureChildren(childWidthMeasureSpec, childHeightMeasureSpec)
var maxWidth = 0
var maxHeight = 0
val count = childCount
for (i in 0 until count) {
val child = getChildAt(i)
if (child.visibility == GONE) {
continue
}
maxWidth = maxWidth.coerceAtLeast(child.measuredWidth)
maxHeight += child.measuredHeight
}
maxWidth += paddingLeft + paddingRight
maxHeight += paddingTop + paddingBottom
maxHeight = maxHeight.coerceAtLeast(suggestedMinimumHeight)
maxWidth = maxWidth.coerceAtLeast(suggestedMinimumWidth)
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
resolveSizeAndState(maxHeight, heightMeasureSpec, 0),
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var centerX = 0
var bottom = measuredHeight - paddingBottom
val count = childCount
var i = count
while (--i >= 0) {
val child = getChildAt(i)
if (child.visibility == GONE) {
continue
}
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
var layoutBottom: Int
var layoutRight: Int
if (i == count - 1) {
layoutBottom = bottom + (childHeight - mFabSize) / 2
layoutRight = measuredWidth - paddingRight + (childWidth - mFabSize) / 2
bottom -= mFabSize + mIntervalPrimary
centerX = layoutRight - childWidth / 2
mMainFabCenterY = layoutBottom - childHeight / 2f
} else {
layoutBottom = bottom + (childHeight - mFabMiniSize) / 2
layoutRight = centerX + childWidth / 2
bottom -= mFabMiniSize + mIntervalSecondary
}
child.layout(
layoutRight - childWidth,
layoutBottom - childHeight,
layoutRight,
layoutBottom,
)
}
}
fun addOnExpandListener(listener: OnExpandListener) {
mOnExpandListeners.add(listener)
}
fun setOnClickFabListener(listener: OnClickFabListener?) {
mOnClickFabListener = listener
if (listener != null) {
var i = 0
val n = childCount
while (i < n) {
getChildAt(i).setOnClickListener(this)
i++
}
} else {
var i = 0
val n = childCount
while (i < n) {
getChildAt(i).isClickable = false
i++
}
}
}
fun setHidePrimaryFab(hidePrimaryFab: Boolean) {
if (mHidePrimaryFab != hidePrimaryFab) {
mHidePrimaryFab = hidePrimaryFab
val expanded = mExpanded
val count = childCount
if (!expanded && count > 0) {
getChildAt(count - 1).visibility = if (hidePrimaryFab) INVISIBLE else VISIBLE
}
}
}
fun setAutoCancel(autoCancel: Boolean) {
if (mAutoCancel != autoCancel) {
mAutoCancel = autoCancel
if (mExpanded) {
if (autoCancel) {
setOnClickListener(this)
} else {
isClickable = false
}
}
}
}
fun toggle() {
isExpanded = !mExpanded
}
var isExpanded: Boolean
get() = mExpanded
set(expanded) {
setExpanded(expanded, true)
}
fun setExpanded(expanded: Boolean, animation: Boolean) {
if (mExpanded != expanded) {
mExpanded = expanded
if (mAutoCancel) {
if (expanded) {
setOnClickListener(this)
} else {
isClickable = false
}
}
val count = childCount
if (count > 0) {
if (mMainFabCenterY == -1f || !animation) {
// It is before first onLayout
val checkCount = if (mHidePrimaryFab) count else count - 1
for (i in 0 until checkCount) {
val child = getChildAt(i)
if (child.visibility == GONE) {
continue
}
child.visibility = if (expanded) VISIBLE else INVISIBLE
if (expanded) {
child.alpha = 1f
}
}
} else {
if (mHidePrimaryFab) {
setPrimaryFabAnimation(getChildAt(count - 1), expanded, !expanded)
}
for (i in 0 until count - 1) {
val child = getChildAt(i)
if (child.visibility == GONE) {
continue
}
setSecondaryFabAnimation(child, expanded, expanded)
}
}
}
mOnExpandListeners.forEach { it.onExpand(expanded) }
}
}
private fun setPrimaryFabAnimation(child: View, expanded: Boolean, delay: Boolean) {
val startRotation: Float
val endRotation: Float
val startScale: Float
val endScale: Float
val interpolator: Interpolator
if (expanded) {
startRotation = -45.0f
endRotation = 0.0f
startScale = 0.0f
endScale = 1.0f
interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR
} else {
startRotation = 0.0f
endRotation = 0.0f
startScale = 1.0f
endScale = 0.0f
interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR
}
child.scaleX = startScale
child.scaleY = startScale
child.rotation = startRotation
child.animate()
.scaleX(endScale)
.scaleY(endScale)
.rotation(endRotation)
.setStartDelay(if (delay) ANIMATE_TIME else 0L)
.setDuration(ANIMATE_TIME)
.setInterpolator(interpolator)
.setListener(object : SimpleAnimatorListener() {
override fun onAnimationStart(animation: Animator) {
if (expanded) {
child.visibility = VISIBLE
}
}
override fun onAnimationEnd(animation: Animator) {
if (!expanded) {
child.visibility = INVISIBLE
}
}
}).start()
}
private fun setSecondaryFabAnimation(child: View, expanded: Boolean, delay: Boolean) {
val startTranslationY: Float
val endTranslationY: Float
val startAlpha: Float
val endAlpha: Float
val interpolator: Interpolator
if (expanded) {
startTranslationY = mMainFabCenterY - child.top - child.height / 2
endTranslationY = 0f
startAlpha = 0f
endAlpha = 1f
interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR
} else {
startTranslationY = 0f
endTranslationY = mMainFabCenterY - child.top - child.height / 2
startAlpha = 1f
endAlpha = 0f
interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR
}
child.alpha = startAlpha
child.translationY = startTranslationY
child.animate()
.alpha(endAlpha)
.translationY(endTranslationY)
.setStartDelay(if (delay) ANIMATE_TIME else 0L)
.setDuration(ANIMATE_TIME)
.setInterpolator(interpolator)
.setListener(object : SimpleAnimatorListener() {
override fun onAnimationStart(animation: Animator) {
if (expanded) {
child.visibility = VISIBLE
}
}
override fun onAnimationEnd(animation: Animator) {
if (!expanded) {
child.visibility = INVISIBLE
}
}
}).start()
}
override fun onClick(v: View) {
if (this === v) {
isExpanded = false
} else {
mOnClickFabListener?.let {
val position = indexOfChild(v)
if (position == childCount - 1) {
it.onClickPrimaryFab(this, v as FloatingActionButton)
} else if (position >= 0 && mExpanded) {
it.onClickSecondaryFab(this, v as FloatingActionButton, position)
}
}
}
}
override fun dispatchSetPressed(pressed: Boolean) {
// Don't dispatch it to children
}
override fun onSaveInstanceState(): Parcelable {
val state = Bundle()
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState())
state.putBoolean(STATE_KEY_AUTO_CANCEL, mAutoCancel)
state.putBoolean(STATE_KEY_EXPANDED, mExpanded)
return state
}
override fun onRestoreInstanceState(state: Parcelable) {
if (state is Bundle) {
super.onRestoreInstanceState(state.getParcelableCompat(STATE_KEY_SUPER))
setAutoCancel(state.getBoolean(STATE_KEY_AUTO_CANCEL))
setExpanded(state.getBoolean(STATE_KEY_EXPANDED), false)
}
}
fun interface OnExpandListener {
fun onExpand(expanded: Boolean)
}
interface OnClickFabListener {
fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton)
fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int)
}
companion object {
private const val ANIMATE_TIME = 300L
private const val STATE_KEY_SUPER = "super"
private const val STATE_KEY_AUTO_CANCEL = "auto_cancel"
private const val STATE_KEY_EXPANDED = "expanded"
}
}
@@ -1,336 +0,0 @@
/*
* Copyright 2015 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.legacy
import android.animation.Animator
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import com.google.android.material.math.MathUtils.dist
import com.hippo.ehviewer.R
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.hippo.ehviewer.util.SimpleHandler
import com.hippo.ehviewer.util.dp2px
import kotlin.math.abs
class FastScroller : View {
private var mDraggable = false
private var mMinHandlerHeight = 0
private var mRecyclerView: RecyclerView? = null
private var mOnScrollChangeListener: RecyclerView.OnScrollListener? = null
private var mAdapter: RecyclerView.Adapter<*>? = null
private var mAdapterDataObserver: AdapterDataObserver? = null
private var mHandler: Drawable? = null
private var mHandlerOffset = INVALID
private var mHandlerHeight = INVALID
private var mDownX = INVALID.toFloat()
private var mDownY = INVALID.toFloat()
private var mLastMotionY = INVALID.toFloat()
private var mDragged = false
private var mCantDrag = false
private var mTouchSlop = 0
private var mListener: OnDragHandlerListener? = null
private var mShowAnimator: ObjectAnimator? = null
private var mHideAnimator: ObjectAnimator? = null
private val mHideRunnable = Runnable { mHideAnimator!!.start() }
constructor(context: Context) : super(context) {
init(context, null, 0)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs, 0)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr,
) {
init(context, attrs, defStyleAttr)
}
private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
val a = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, defStyleAttr, 0)
mHandler = a.getDrawable(R.styleable.FastScroller_handler)
mDraggable = a.getBoolean(R.styleable.FastScroller_draggable, true)
a.recycle()
alpha = 0.0f
visibility = INVISIBLE
mMinHandlerHeight = dp2px(context, MIN_HANDLER_HEIGHT_DP.toFloat())
mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
mShowAnimator = ObjectAnimator.ofFloat(this, "alpha", 1.0f).apply {
interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR
duration = SCROLL_BAR_FADE_DURATION.toLong()
}
mHideAnimator = ObjectAnimator.ofFloat(this, "alpha", 0.0f).apply {
interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR
duration = SCROLL_BAR_FADE_DURATION.toLong()
addListener(object : SimpleAnimatorListener() {
private var mCancel = false
override fun onAnimationCancel(animation: Animator) {
mCancel = true
}
override fun onAnimationEnd(animation: Animator) {
if (mCancel) {
mCancel = false
} else {
visibility = INVISIBLE
}
}
})
}
}
fun setOnDragHandlerListener(listener: OnDragHandlerListener?) {
mListener = listener
}
private fun updatePosition(show: Boolean) {
if (mRecyclerView == null) {
return
}
val paddingTop = paddingTop
val paddingBottom = paddingBottom
val height = height - paddingTop - paddingBottom
val offset = mRecyclerView!!.computeVerticalScrollOffset()
val extent = mRecyclerView!!.computeVerticalScrollExtent()
val range = mRecyclerView!!.computeVerticalScrollRange()
if (height <= 0 || extent >= range || extent <= 0) {
return
}
var endOffset = height * offset / range
var endHeight = height * extent / range
endHeight = endHeight.coerceAtLeast(mMinHandlerHeight)
endOffset = endOffset.coerceAtMost(height - endHeight)
mHandlerOffset = endOffset + paddingTop
mHandlerHeight = endHeight
if (show) {
if (mHideAnimator!!.isRunning) {
mHideAnimator!!.cancel()
mShowAnimator!!.start()
} else if (visibility != VISIBLE && !mShowAnimator!!.isRunning) {
visibility = VISIBLE
mShowAnimator!!.start()
}
SimpleHandler.removeCallbacks(mHideRunnable)
if (!mDragged) {
SimpleHandler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY.toLong())
}
}
}
fun setHandlerDrawable(drawable: Drawable?) {
mHandler = drawable
invalidate()
}
var isDraggable: Boolean
get() = mDraggable
set(draggable) {
mDraggable = draggable
if (mDragged) {
mDragged = false
}
SimpleHandler.removeCallbacks(mHideRunnable)
mHideRunnable.run()
}
val isAttached: Boolean
get() = mRecyclerView != null
fun attachToRecyclerView(recyclerView: RecyclerView?) {
if (recyclerView == null) {
return
}
check(mRecyclerView == null) {
"The FastScroller is already attached to a RecyclerView, " +
"call detachedFromRecyclerView first"
}
mRecyclerView = recyclerView
mOnScrollChangeListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
updatePosition(true)
invalidate()
}
}
recyclerView.addOnScrollListener(mOnScrollChangeListener!!)
mAdapter = recyclerView.adapter
if (mAdapter != null) {
mAdapterDataObserver = object : AdapterDataObserver() {
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
super.onItemRangeChanged(positionStart, itemCount)
updatePosition(false)
invalidate()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
super.onItemRangeChanged(positionStart, itemCount, payload)
updatePosition(false)
invalidate()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
updatePosition(false)
invalidate()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
updatePosition(false)
invalidate()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
updatePosition(false)
invalidate()
}
}
mAdapter!!.registerAdapterDataObserver(mAdapterDataObserver!!)
}
}
fun detachedFromRecyclerView() {
if (mRecyclerView != null && mOnScrollChangeListener != null) {
mRecyclerView!!.removeOnScrollListener(mOnScrollChangeListener!!)
}
mRecyclerView = null
mOnScrollChangeListener = null
if (mAdapter != null && mAdapterDataObserver != null) {
mAdapter!!.unregisterAdapterDataObserver(mAdapterDataObserver!!)
}
mAdapter = null
mAdapterDataObserver = null
alpha = 0.0f
visibility = INVISIBLE
}
override fun onDraw(canvas: Canvas) {
if (mRecyclerView == null || mHandler == null) {
return
}
if (mHandlerHeight == INVALID) {
updatePosition(false)
}
if (mHandlerHeight == INVALID) {
return
}
val paddingLeft = paddingLeft
val saved = canvas.save()
canvas.translate(paddingLeft.toFloat(), mHandlerOffset.toFloat())
mHandler!!.setBounds(0, 0, width - paddingLeft - paddingRight, mHandlerHeight)
mHandler!!.draw(canvas)
canvas.restoreToCount(saved)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!mDraggable || visibility != VISIBLE || mRecyclerView == null || mHandlerHeight == INVALID) {
return false
}
val action = event.action
if (action == MotionEvent.ACTION_DOWN) {
mCantDrag = false
}
if (mCantDrag) {
return false
}
when (action) {
MotionEvent.ACTION_DOWN -> {
mDragged = false
mDownX = event.x
mDownY = event.y
if (mDownY < mHandlerOffset || mDownY > mHandlerOffset + mHandlerHeight) {
mCantDrag = true
return false
}
}
MotionEvent.ACTION_MOVE -> {
if (!mDragged) {
val x = event.x
val y = event.y
// Check touch slop
if (dist(x, y, mDownX, mDownY) < mTouchSlop) {
return true
}
if (abs(x - mDownX) > abs(y - mDownY) || y < mHandlerOffset || y > mHandlerOffset + mHandlerHeight) {
mCantDrag = true
return false
} else {
mDragged = true
SimpleHandler.removeCallbacks(mHideRunnable)
// Update mLastMotionY
mLastMotionY =
if (mDownY < mHandlerOffset || mDownY >= mHandlerOffset + mHandlerHeight) {
// the point out of handler, make the point in handler center
(mHandlerOffset + mHandlerHeight / 2).toFloat()
} else {
mDownY
}
// Notify
if (mListener != null) {
mListener!!.onStartDragHandler()
}
}
}
val range = mRecyclerView!!.computeVerticalScrollRange()
if (range <= 0) {
return true
}
val y = event.y
val scroll =
(range * (y - mLastMotionY) / (height - paddingTop - paddingBottom)).toInt()
mRecyclerView!!.scrollBy(0, scroll)
mLastMotionY = y
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
// Notify
if (mDragged && mListener != null) {
mListener!!.onEndDragHandler()
}
mDragged = false
SimpleHandler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY.toLong())
}
}
return true
}
interface OnDragHandlerListener {
fun onStartDragHandler()
fun onEndDragHandler()
}
companion object {
private const val INVALID = -1
private const val SCROLL_BAR_FADE_DURATION = 500
private const val SCROLL_BAR_DELAY = 1000
private const val MIN_HANDLER_HEIGHT_DP = 32
}
}
@@ -1,82 +0,0 @@
/*
* Copyright 2015 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.legacy
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.RectF
import android.graphics.drawable.Drawable
class HandlerDrawable : Drawable() {
private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val mTemp = RectF()
private var mColor = Color.BLACK
init {
mPaint.color = mColor
mPaint.style = Paint.Style.FILL
}
override fun draw(canvas: Canvas) {
val width = bounds.width()
val height = bounds.height()
if (width > height) {
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), mPaint)
} else {
mTemp[0f, 0f, width.toFloat()] = width.toFloat()
canvas.drawArc(mTemp, -180f, 180f, true, mPaint)
mTemp[0f, (height - width).toFloat(), width.toFloat()] = height.toFloat()
canvas.drawArc(mTemp, 0f, 180f, true, mPaint)
val halfWidth = width.toFloat() / 2.0f
canvas.drawRect(0f, halfWidth, width.toFloat(), height - halfWidth, mPaint)
}
}
fun setColor(color: Int) {
if (mColor != color) {
mColor = color
mPaint.color = color
}
}
override fun setAlpha(alpha: Int) {
mPaint.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mPaint.colorFilter = colorFilter
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int {
return when (Color.alpha(mColor)) {
0xff -> {
PixelFormat.OPAQUE
}
0x00 -> {
PixelFormat.TRANSPARENT
}
else -> {
PixelFormat.TRANSLUCENT
}
}
}
}
@@ -1,81 +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.legacy
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet
import android.widget.ListView
import com.hippo.ehviewer.R
@SuppressLint("CustomViewStyleable")
class IndicatingListView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : ListView(context, attrs) {
private val mPaint = Paint()
private val mTemp = Rect()
private var mIndicatorHeight = 0
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.Indicating)
mIndicatorHeight = a.getDimensionPixelOffset(R.styleable.Indicating_indicatorHeight, 1)
mPaint.color = a.getColor(
R.styleable.Indicating_indicatorColor,
Color.BLACK,
)
mPaint.style = Paint.Style.FILL
a.recycle()
}
private fun fillTopIndicatorDrawRect() {
mTemp[0, 0, width] = mIndicatorHeight
}
private fun fillBottomIndicatorDrawRect() {
mTemp[0, height - mIndicatorHeight, width] = height
}
private fun needShowTopIndicator(): Boolean {
return canScrollVertically(-1)
}
private fun needShowBottomIndicator(): Boolean {
return canScrollVertically(1)
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
val restoreCount = canvas.save()
canvas.translate(scrollX.toFloat(), scrollY.toFloat())
// Draw top indicator
if (needShowTopIndicator()) {
fillTopIndicatorDrawRect()
canvas.drawRect(mTemp, mPaint)
}
// Draw bottom indicator
if (needShowBottomIndicator()) {
fillBottomIndicatorDrawRect()
canvas.drawRect(mTemp, mPaint)
}
canvas.restoreToCount(restoreCount)
}
}
@@ -1,153 +0,0 @@
/*
* Copyright 2015 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.legacy
import android.content.Context
import android.graphics.PointF
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.google.android.material.math.MathUtils
import com.hippo.ehviewer.util.SimpleHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import kotlin.math.abs
object LayoutManagerUtils {
private var sCsdfp: Method? = null
init {
try {
sCsdfp = StaggeredGridLayoutManager::class.java.getDeclaredMethod(
"calculateScrollDirectionForPosition",
Int::class.javaPrimitiveType,
).apply { isAccessible = true }
} catch (e: NoSuchMethodException) {
// Ignore
e.printStackTrace()
}
}
fun scrollToPositionWithOffset(
layoutManager: LayoutManager,
position: Int,
offset: Int,
) {
when (layoutManager) {
is LinearLayoutManager -> layoutManager.scrollToPositionWithOffset(position, offset)
is StaggeredGridLayoutManager ->
layoutManager.scrollToPositionWithOffset(position, offset)
else -> throw IllegalStateException("Can't do scrollToPositionWithOffset for " + layoutManager.javaClass.name)
}
}
fun smoothScrollToPosition(
layoutManager: LayoutManager,
context: Context,
position: Int,
millisecondsPerInch: Int = -1,
) {
val smoothScroller: SimpleSmoothScroller
when (layoutManager) {
is LinearLayoutManager -> {
smoothScroller =
object : SimpleSmoothScroller(context, millisecondsPerInch.toFloat()) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
return layoutManager.computeScrollVectorForPosition(targetPosition)
}
}
}
is StaggeredGridLayoutManager -> {
smoothScroller =
object : SimpleSmoothScroller(context, millisecondsPerInch.toFloat()) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
var direction = 0
try {
direction = sCsdfp!!.invoke(layoutManager, targetPosition) as Int
} catch (e: IllegalAccessException) {
e.printStackTrace()
} catch (e: InvocationTargetException) {
e.printStackTrace()
}
if (direction == 0) {
return null
}
return if (layoutManager.orientation == StaggeredGridLayoutManager.HORIZONTAL) {
PointF(direction.toFloat(), 0f)
} else {
PointF(0f, direction.toFloat())
}
}
}
}
else -> throw IllegalStateException("Can't do smoothScrollToPosition for " + layoutManager.javaClass.name)
}
smoothScroller.targetPosition = position
layoutManager.startSmoothScroll(smoothScroller)
}
fun scrollToPositionProperly(
layoutManager: LayoutManager,
context: Context,
position: Int,
listener: OnScrollToPositionListener?,
) {
SimpleHandler.postDelayed({
val first = layoutManager.firstVisibleItemPosition
val last = getLastVisibleItemPosition(layoutManager)
val offset = abs(position - first)
val max = last - first
if (offset < max && max > 0) {
smoothScrollToPosition(
layoutManager,
context,
position,
MathUtils.lerp(100F, 25F, (offset / max).toFloat()).toInt(),
)
} else {
scrollToPositionWithOffset(layoutManager, position, 0)
listener?.onScrollToPosition(position)
}
}, 200)
}
val LayoutManager.firstVisibleItemPosition: Int
get() = when (this) {
is LinearLayoutManager -> findFirstVisibleItemPosition()
is StaggeredGridLayoutManager -> findFirstVisibleItemPositions(null).min()
else -> error("Can't do getFirstVisibleItemPosition for " + javaClass.name)
}
fun getLastVisibleItemPosition(layoutManager: LayoutManager): Int {
return when (layoutManager) {
is LinearLayoutManager -> layoutManager.findLastVisibleItemPosition()
is StaggeredGridLayoutManager -> {
val positions = layoutManager.findLastVisibleItemPositions(null)
positions.max()
}
else -> throw IllegalStateException("Can't do getLastVisibleItemPosition for " + layoutManager.javaClass.name)
}
}
fun interface OnScrollToPositionListener {
fun onScrollToPosition(position: Int)
}
}
@@ -1,56 +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.legacy
import android.content.Context
import android.view.LayoutInflater
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import com.hippo.ehviewer.R
import com.hippo.ehviewer.databinding.DialogListCheckboxBuilderBinding
class ListCheckBoxDialogBuilder(
context: Context,
items: List<CharSequence>,
listener: (ListCheckBoxDialogBuilder, AlertDialog?, Int) -> Unit,
checkText: String?,
checked: Boolean,
) : BaseDialogBuilder(
context,
) {
private val binding =
DialogListCheckboxBuilderBinding.inflate(LayoutInflater.from(getContext()))
private var mDialog: AlertDialog? = null
val isChecked get() = binding.checkbox.isChecked
override fun create(): AlertDialog {
mDialog = super.create()
return mDialog as AlertDialog
}
init {
setView(binding.root)
binding.listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)
binding.listView.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
listener(this@ListCheckBoxDialogBuilder, mDialog, position)
mDialog?.dismiss()
}
binding.checkbox.text = checkText
binding.checkbox.isChecked = checked
}
}
@@ -1,74 +0,0 @@
/*
* Copyright (C) 2015 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.legacy
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
class MarginItemDecoration(
margin: Int,
paddingLeft: Int,
paddingTop: Int,
paddingRight: Int,
paddingBottom: Int,
) : ItemDecoration() {
private var mMargin = 0
private var mPaddingLeft = 0
private var mPaddingTop = 0
private var mPaddingRight = 0
private var mPaddingBottom = 0
init {
setMargin(margin, paddingLeft, paddingTop, paddingRight, paddingBottom)
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State,
) {
outRect[mMargin, mMargin, mMargin] = mMargin
}
/**
* @param margin gap between two item
* @param paddingLeft gap between RecyclerView left and left item left
* @param paddingTop gap between RecyclerView top and top item top
* @param paddingRight gap between RecyclerView right and right item right
* @param paddingBottom gap between RecyclerView bottom and bottom item bottom
*/
fun setMargin(
margin: Int,
paddingLeft: Int,
paddingTop: Int,
paddingRight: Int,
paddingBottom: Int,
) {
val halfMargin = margin / 2
mMargin = halfMargin
mPaddingLeft = paddingLeft - halfMargin
mPaddingTop = paddingTop - halfMargin
mPaddingRight = paddingRight - halfMargin
mPaddingBottom = paddingBottom - halfMargin
}
fun applyPaddings(view: View) {
view.setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom)
}
}
@@ -1,189 +0,0 @@
package com.hippo.ehviewer.ui.legacy
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri
import android.util.AttributeSet
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.image.Image
import com.hippo.ehviewer.ui.main.AdvancedSearchOption
import com.hippo.ehviewer.ui.main.ImageSearch
import com.hippo.ehviewer.ui.main.NormalSearch
import com.hippo.ehviewer.ui.main.SearchAdvanced
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.pickVisualMedia
import com.hippo.unifile.UniFile
import kotlinx.coroutines.launch
class SearchLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : AbstractComposeView(context, attrs, defStyle) {
private var isNormalMode by mutableStateOf(true) // else ImageSearch mode
private var isAdvancedMode by mutableStateOf(false)
private var mCategory by mutableIntStateOf(Settings.searchCategory)
private var mSearchMode by mutableIntStateOf(1)
private var advancedState by mutableStateOf(AdvancedSearchOption())
private var uss by mutableStateOf(false)
private var osc by mutableStateOf(false)
private var path by mutableStateOf("")
@Composable
override fun Content() {
val coroutineScope = rememberCoroutineScope()
fun selectImage() = coroutineScope.launch {
context.pickVisualMedia(ActivityResultContracts.PickVisualMedia.ImageOnly)?.let {
path = it.toString()
}
}
Mdc3Theme {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_layout_margin_h)).verticalScroll(rememberScrollState())) {
AnimatedVisibility(visible = isNormalMode) {
ElevatedCard(modifier = Modifier.fillMaxWidth().padding(vertical = dimensionResource(id = R.dimen.search_layout_margin_v))) {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
Text(text = stringResource(id = R.string.search_normal), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
NormalSearch(
category = mCategory,
onCategoryChanged = {
Settings.searchCategory = it
mCategory = it
},
searchMode = mSearchMode,
onSearchModeChanged = { mSearchMode = it },
isAdvanced = isAdvancedMode,
onAdvancedChanged = { isAdvancedMode = it },
showInfo = { BaseDialogBuilder(context).setMessage(R.string.search_tip).show() },
)
}
}
}
AnimatedVisibility(visible = isNormalMode && isAdvancedMode) {
ElevatedCard(modifier = Modifier.fillMaxWidth().padding(vertical = dimensionResource(id = R.dimen.search_layout_margin_v))) {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
Text(text = stringResource(id = R.string.search_advance), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
SearchAdvanced(
state = advancedState,
onStateChanged = { advancedState = it },
)
}
}
}
AnimatedVisibility(visible = !isNormalMode) {
ElevatedCard(modifier = Modifier.fillMaxWidth().padding(vertical = dimensionResource(id = R.dimen.search_layout_margin_v))) {
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.search_category_padding_h), vertical = dimensionResource(id = R.dimen.search_category_padding_v))) {
Text(text = stringResource(id = R.string.search_image), modifier = Modifier.height(dimensionResource(id = R.dimen.search_category_title_height)), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
ImageSearch(
imagePath = path,
onSelectImage = ::selectImage,
uss = uss,
onUssChecked = { uss = it },
osc = osc,
onOscChecked = { osc = it },
)
}
}
}
TabRow(
selectedTabIndex = if (isNormalMode) 0 else 1,
divider = {},
) {
Tab(
selected = isNormalMode,
onClick = { isNormalMode = true },
text = { Text(text = stringResource(id = R.string.keyword_search)) },
)
Tab(
selected = !isNormalMode,
onClick = { isNormalMode = false },
text = { Text(text = stringResource(id = R.string.search_image)) },
)
}
Spacer(modifier = Modifier.height(WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))
}
}
}
fun setSearchMyTags(isMyTags: Boolean) {
if (isMyTags) mSearchMode = 2
}
fun setCategory(category: Int) {
mCategory = category
}
fun formatListUrlBuilder(urlBuilder: ListUrlBuilder, query: String?) {
urlBuilder.reset()
when (isNormalMode) {
true -> {
when (mSearchMode) {
1 -> urlBuilder.mode = ListUrlBuilder.MODE_NORMAL
2 -> urlBuilder.mode = ListUrlBuilder.MODE_SUBSCRIPTION
3 -> urlBuilder.mode = ListUrlBuilder.MODE_UPLOADER
4 -> urlBuilder.mode = ListUrlBuilder.MODE_TAG
}
urlBuilder.keyword = query
urlBuilder.category = mCategory
if (isAdvancedMode) {
urlBuilder.advanceSearch = advancedState.advanceSearch
urlBuilder.minRating = advancedState.minRating
val pageFrom = advancedState.fromPage
val pageTo = advancedState.toPage
if (pageTo != -1 && pageTo < 10) {
throw EhException(context.getString(R.string.search_sp_err1))
} else if (pageFrom != -1 && pageTo != -1 && pageTo - pageFrom < 20) {
throw EhException(context.getString(R.string.search_sp_err2))
}
urlBuilder.pageFrom = pageFrom
urlBuilder.pageTo = pageTo
}
}
false -> {
urlBuilder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
if (path.isBlank()) throw EhException(context.getString(R.string.select_image_first))
val uri = Uri.parse(path)
val src = UniFile.fromUri(context, uri)?.imageSource ?: return
val temp = AppConfig.createTempFile() ?: return
val bitmap = ImageDecoder.decodeBitmap(src, Image.imageSearchDecoderSampleListener)
temp.outputStream().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
urlBuilder.imagePath = temp.path
urlBuilder.isUseSimilarityScan = uss
urlBuilder.isOnlySearchCovers = osc
}
}
}
}
@@ -1,26 +0,0 @@
package com.hippo.ehviewer.ui.legacy
import android.content.Context
import android.util.AttributeSet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.AbstractComposeView
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.hippo.ehviewer.ui.tools.GalleryListCardRating
class SimpleRatingView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : AbstractComposeView(context, attrs, defStyle) {
var rating by mutableFloatStateOf(0f)
@Composable
override fun Content() {
Mdc3Theme {
GalleryListCardRating(rating)
}
}
}
@@ -1,38 +0,0 @@
/*
* Copyright (C) 2015 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.legacy
import android.content.Context
import androidx.recyclerview.widget.LinearSmoothScroller
import kotlin.math.abs
import kotlin.math.ceil
abstract class SimpleSmoothScroller(context: Context, millisecondsPerInch: Float) :
LinearSmoothScroller(context) {
private val mMillisecondsPerPx: Float
init {
mMillisecondsPerPx = millisecondsPerInch / context.resources.displayMetrics.densityDpi
}
override fun calculateTimeForScrolling(dx: Int): Int {
return if (mMillisecondsPerPx <= 0) {
super.calculateTimeForScrolling(dx)
} else {
ceil((abs(dx) * mMillisecondsPerPx).toDouble()).toInt()
}
}
}
@@ -1,109 +0,0 @@
/*
* Copyright (C) 2015 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.legacy
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.view.ViewPropertyAnimator
open class ViewTransition(vararg views: View) {
private val mViews: Array<View>
protected var mAnimator1: ViewPropertyAnimator? = null
protected var mAnimator2: ViewPropertyAnimator? = null
var shownViewIndex = -1
private set
private var mOnShowViewListener: OnShowViewListener? = null
init {
check(views.size >= 2) { "You must pass view to ViewTransition" }
mViews = arrayOf(*views)
showView(0, false)
}
fun setOnShowViewListener(listener: OnShowViewListener?) {
mOnShowViewListener = listener
}
fun showView(shownView: Int, animation: Boolean = true): Boolean {
val views = mViews
val length = views.size
if (shownView >= length || shownView < 0) {
throw IndexOutOfBoundsException(
"Only " + length + " view(s) in " +
"the ViewTransition, but attempt to show " + shownView,
)
}
return if (shownViewIndex != shownView) {
val oldShownView = shownViewIndex
shownViewIndex = shownView
// Cancel animation
if (mAnimator1 != null) {
mAnimator1!!.cancel()
}
if (mAnimator2 != null) {
mAnimator2!!.cancel()
}
if (animation) {
startAnimations(views[oldShownView], views[shownView])
} else {
for (i in 0 until length) {
val v = views[i]
if (i == shownView) {
v.alpha = 1f
v.visibility = View.VISIBLE
} else {
v.alpha = 0f
v.visibility = View.GONE
}
}
}
if (null != mOnShowViewListener) {
mOnShowViewListener!!.onShowView(views[oldShownView], views[shownView])
}
true
} else {
false
}
}
protected open fun startAnimations(hiddenView: View, shownView: View) {
mAnimator1 = hiddenView.animate().alpha(0f)
mAnimator1!!.setDuration(ANIMATE_TIME).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
hiddenView.visibility = View.GONE
mAnimator1 = null
}
}).start()
shownView.alpha = 0f
shownView.visibility = View.VISIBLE
mAnimator2 = shownView.animate().alpha(1f)
mAnimator2!!.setDuration(ANIMATE_TIME).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
mAnimator2 = null
}
}).start()
}
interface OnShowViewListener {
fun onShowView(hiddenView: View, shownView: View)
}
companion object {
const val ANIMATE_TIME = 300L
}
}
@@ -1,95 +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.legacy
import android.view.View
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsAnimationCompat.BoundsCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.math.MathUtils
class WindowInsetsAnimationHelper(dispatchMode: Int, vararg views: View) :
WindowInsetsAnimationCompat.Callback(dispatchMode) {
private val views: Array<View>
private val startPaddings = HashMap<View, Int>()
private val endPaddings = HashMap<View, Int>()
var animation: WindowInsetsAnimationCompat? = null
init {
this.views = arrayOf(*views)
}
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
super.onPrepare(animation)
this.animation = animation
for (view in views) {
startPaddings[view] = view.paddingBottom
}
}
override fun onStart(
animation: WindowInsetsAnimationCompat,
bounds: BoundsCompat,
): BoundsCompat {
this.animation = animation
for (view in views) {
endPaddings[view] = view.paddingBottom
val startPadding = if (startPaddings.containsKey(view)) startPaddings[view]!! else 0
view.translationY = -(startPadding - view.paddingBottom).toFloat()
}
return bounds
}
override fun onProgress(
insets: WindowInsetsCompat,
runningAnimations: List<WindowInsetsAnimationCompat>,
): WindowInsetsCompat {
if (animation == null) {
return insets
}
var imeAnimation: WindowInsetsAnimationCompat? = null
for (animation in runningAnimations) {
if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
imeAnimation = animation
break
}
}
if (imeAnimation != null) {
for (view in views) {
val startPadding = if (startPaddings.containsKey(view)) startPaddings[view]!! else 0
val endPadding = if (endPaddings.containsKey(view)) endPaddings[view]!! else 0
view.translationY = MathUtils.lerp(
(endPadding - startPadding).toFloat(),
0f,
animation!!.interpolatedFraction,
)
}
}
return insets
}
override fun onEnd(animation: WindowInsetsAnimationCompat) {
super.onEnd(animation)
startPaddings.clear()
endPaddings.clear()
this.animation = null
for (view in views) {
view.translationY = 0f
}
}
}
@@ -1,16 +0,0 @@
package com.hippo.ehviewer.ui.main
object AdvanceTable {
const val SH = 0x1
const val STO = 0x2
const val SFL = 0x100
const val SFU = 0x200
const val SFT = 0x400
}
data class AdvancedSearchOption(
val advanceSearch: Int = 0,
val minRating: Int = 0,
val fromPage: Int = -1,
val toPage: Int = -1,
)
@@ -1,125 +0,0 @@
package com.hippo.ehviewer.ui.main
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R
@Composable
fun SearchAdvanced(
state: AdvancedSearchOption,
onStateChanged: (AdvancedSearchOption) -> Unit,
) = Column {
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
val adv = state.advanceSearch
fun checked(bit: Int) = adv and bit != 0
fun AdvancedSearchOption.inv(checked: Boolean, bit: Int) = onStateChanged(copy(advanceSearch = if (!checked) advanceSearch xor bit else advanceSearch or bit))
Row {
Row(modifier = Modifier.weight(1f)) {
Checkbox(checked = checked(AdvanceTable.SH), onCheckedChange = { state.inv(it, AdvanceTable.SH) })
Text(text = stringResource(id = R.string.search_sh), modifier = Modifier.align(Alignment.CenterVertically))
}
Row(modifier = Modifier.weight(1f)) {
Checkbox(checked = checked(AdvanceTable.STO), onCheckedChange = { state.inv(it, AdvanceTable.STO) })
Text(text = stringResource(id = R.string.search_sto), modifier = Modifier.align(Alignment.CenterVertically))
}
}
val minRatingItems = stringArrayResource(id = R.array.search_min_rating)
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
) {
val softwareKeyboardController = LocalSoftwareKeyboardController.current
SideEffect {
softwareKeyboardController?.hide()
}
OutlinedTextField(
modifier = Modifier.menuAnchor(),
readOnly = true,
value = minRatingItems[state.minRating],
onValueChange = {},
label = { Text(stringResource(id = R.string.search_sr)) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
minRatingItems.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption) },
onClick = {
expanded = false
onStateChanged(state.copy(minRating = minRatingItems.indexOf(selectionOption)))
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
var enabled by rememberSaveable { mutableStateOf(false) }
Checkbox(checked = enabled, onCheckedChange = { enabled = it })
Text(text = stringResource(id = R.string.search_sp), modifier = Modifier.align(Alignment.CenterVertically))
OutlinedTextField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
value = if (enabled && state.fromPage != -1) state.fromPage.toString() else "",
onValueChange = { onStateChanged(state.copy(fromPage = it.toInt())) },
modifier = Modifier.width(96.dp).padding(16.dp),
singleLine = true,
enabled = enabled,
)
Text(text = stringResource(id = R.string.search_sp_to), modifier = Modifier.align(Alignment.CenterVertically))
OutlinedTextField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
value = if (enabled && state.toPage != -1) state.toPage.toString() else "",
onValueChange = { onStateChanged(state.copy(toPage = it.toInt())) },
modifier = Modifier.width(96.dp).padding(16.dp),
singleLine = true,
enabled = enabled,
)
Text(text = stringResource(id = R.string.search_sp_suffix), modifier = Modifier.align(Alignment.CenterVertically))
}
Text(text = stringResource(id = R.string.search_sf))
Row {
Row(modifier = Modifier.weight(1f)) {
Checkbox(checked = checked(AdvanceTable.SFL), onCheckedChange = { state.inv(it, AdvanceTable.SFL) })
Text(text = stringResource(id = R.string.search_sfl), modifier = Modifier.align(Alignment.CenterVertically))
}
Row(modifier = Modifier.weight(1f)) {
Checkbox(checked = checked(AdvanceTable.SFU), onCheckedChange = { state.inv(it, AdvanceTable.SFU) })
Text(text = stringResource(id = R.string.search_sfu), modifier = Modifier.align(Alignment.CenterVertically))
}
Row(modifier = Modifier.weight(1f)) {
Checkbox(checked = checked(AdvanceTable.SFT), onCheckedChange = { state.inv(it, AdvanceTable.SFT) })
Text(text = stringResource(id = R.string.search_sft), modifier = Modifier.align(Alignment.CenterVertically))
}
}
}
}
@@ -1,100 +0,0 @@
package com.hippo.ehviewer.ui.main
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Help
import androidx.compose.material3.Checkbox
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhUtils
private val categoryTable = arrayOf(
EhUtils.DOUJINSHI to R.string.doujinshi,
EhUtils.MANGA to R.string.manga,
EhUtils.ARTIST_CG to R.string.artist_cg,
EhUtils.GAME_CG to R.string.game_cg,
EhUtils.WESTERN to R.string.western,
EhUtils.NON_H to R.string.non_h,
EhUtils.IMAGE_SET to R.string.image_set,
EhUtils.COSPLAY to R.string.cosplay,
EhUtils.ASIAN_PORN to R.string.asian_porn,
EhUtils.MISC to R.string.misc,
)
private val modeTable = arrayOf(
1 to R.string.search_normal_search,
2 to R.string.search_subscription_search,
3 to R.string.search_specify_uploader,
4 to R.string.search_specify_tag,
)
@Composable
fun NormalSearch(
category: Int,
onCategoryChanged: (Int) -> Unit,
searchMode: Int,
onSearchModeChanged: (Int) -> Unit,
isAdvanced: Boolean,
onAdvancedChanged: (Boolean) -> Unit,
showInfo: () -> Unit,
) {
FlowRow(modifier = Modifier.wrapContentHeight(align = Alignment.Top)) {
categoryTable.forEach {
val selected = category and it.first != 0
FilterChip(
selected = selected,
onClick = { onCategoryChanged(if (selected) category xor it.first else category or it.first) },
label = { Text(text = stringResource(id = it.second)) },
leadingIcon = { if (selected) Icon(imageVector = Icons.Default.Check, contentDescription = null) },
modifier = Modifier.padding(horizontal = 4.dp).align(alignment = Alignment.CenterVertically),
)
}
}
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
FlowRow {
modeTable.forEach {
Row(
modifier = Modifier.weight(1F).width(112.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
selected = (it.first == searchMode),
onClick = { onSearchModeChanged(it.first) },
)
Text(
text = stringResource(id = it.second),
)
}
}
}
Row(horizontalArrangement = Arrangement.Start) {
Box(modifier = Modifier.weight(1f)) {
IconButton(onClick = showInfo) {
Icon(imageVector = Icons.Default.Help, contentDescription = null)
}
}
Row(modifier = Modifier.weight(1f)) {
Checkbox(checked = isAdvanced, onCheckedChange = onAdvancedChanged)
Text(text = stringResource(id = R.string.search_enable_advance), modifier = Modifier.align(Alignment.CenterVertically))
}
}
}
}
@@ -1,205 +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.scene
import android.content.res.Resources.Theme
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.StringRes
import androidx.core.view.GravityCompat
import androidx.core.view.SoftwareKeyboardControllerCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.android.material.sidesheet.SideSheetDialog
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.util.getSparseParcelableArrayCompat
import rikka.core.res.resolveDrawable
abstract class BaseScene : Fragment() {
private var drawerView: View? = null
private var drawerViewState: SparseArray<Parcelable>? = null
private var sideSheetDialog: SideSheetDialog? = null
fun addAboveSnackView(view: View) {
val activity = activity
if (activity is MainActivity) {
activity.addAboveSnackView(view)
}
}
fun removeAboveSnackView(view: View) {
val activity = activity
if (activity is MainActivity) {
activity.removeAboveSnackView(view)
}
}
fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.setDrawerLockMode(lockMode, edgeGravity)
}
}
fun getDrawerLockMode(edgeGravity: Int): Int? {
val activity = activity
return if (activity is MainActivity) {
activity.getDrawerLockMode(edgeGravity)
} else {
null
}
}
fun isDrawerOpen(drawerGravity: Int): Boolean {
val activity = activity
return activity is MainActivity && activity.isDrawerOpen(drawerGravity)
}
fun openDrawer(drawerGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.openDrawer(drawerGravity)
}
}
fun closeDrawer(drawerGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.closeDrawer(drawerGravity)
}
}
fun toggleDrawer(drawerGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.toggleDrawer(drawerGravity)
}
}
fun openSideSheet() = sideSheetDialog!!.show()
fun closeSideSheet() = sideSheetDialog!!.hide()
fun showTip(message: CharSequence?, length: Int) {
val activity = activity
if (activity is MainActivity) {
activity.showTip(message!!, length)
}
}
fun showTip(@StringRes id: Int, length: Int) {
val activity = activity
if (activity is MainActivity) {
activity.showTip(id, length)
}
}
open val showLeftDrawer = true
private fun createDrawerView(savedInstanceState: Bundle?): View? {
drawerView = onCreateDrawerView(layoutInflater)
if (drawerView != null) {
val saved = drawerViewState ?: savedInstanceState?.getSparseParcelableArrayCompat(KEY_DRAWER_VIEW_STATE)
saved?.let {
drawerView!!.restoreHierarchyState(saved)
}
}
return drawerView
}
open fun onCreateDrawerView(inflater: LayoutInflater): View? {
return null
}
private fun destroyDrawerView() {
if (drawerView != null) {
drawerViewState = SparseArray()
drawerView!!.saveHierarchyState(drawerViewState)
}
onDestroyDrawerView()
drawerView = null
}
open fun onDestroyDrawerView() {
sideSheetDialog?.dismiss()
sideSheetDialog = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.background = requireActivity().theme.resolveDrawable(android.R.attr.windowBackground)
// Update left drawer locked state
if (showLeftDrawer) {
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
} else {
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
}
hideSoftInput()
createDrawerView(savedInstanceState)?.let {
sideSheetDialog = SideSheetDialog(requireContext()).apply {
window?.decorView?.apply {
val owner = viewLifecycleOwner
setViewTreeLifecycleOwner(owner)
setViewTreeViewModelStoreOwner(owner as ViewModelStoreOwner)
setViewTreeSavedStateRegistryOwner(owner as SavedStateRegistryOwner)
}
setContentView(it)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
destroyDrawerView()
}
val mainActivity: MainActivity?
get() {
val activity = activity
return if (activity is MainActivity) {
activity
} else {
null
}
}
fun hideSoftInput() = activity?.window?.decorView?.run { SoftwareKeyboardControllerCompat(this) }?.hide()
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (drawerView != null) {
drawerViewState = SparseArray()
drawerView!!.saveHierarchyState(drawerViewState)
outState.putSparseParcelableArray(KEY_DRAWER_VIEW_STATE, drawerViewState)
}
}
val theme: Theme
get() = requireActivity().theme
companion object {
const val LENGTH_SHORT = 0
const val LENGTH_LONG = 1
const val KEY_DRAWER_VIEW_STATE = "com.hippo.ehviewer.ui.scene.BaseScene:DRAWER_VIEW_STATE"
}
}
@@ -1,92 +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.scene
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.hippo.ehviewer.R
import com.hippo.ehviewer.databinding.SceneToolbarBinding
abstract class BaseToolbarScene : BaseScene(), ToolBarScene {
private var _binding: SceneToolbarBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = SceneToolbarBinding.inflate(inflater, container, false)
onCreateViewWithToolbar(inflater, binding.root, savedInstanceState)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.apply {
val menuResId = getMenuResId()
if (menuResId != 0) {
inflateMenu(menuResId)
setOnMenuItemClickListener { item: MenuItem -> onMenuItemClick(item) }
}
setNavigationOnClickListener { onNavigationClick() }
}
}
override fun getMenuResId(): Int {
return 0
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return if (item.itemId == R.id.action_open_side_sheet) {
openSideSheet()
true
} else {
false
}
}
override fun onNavigationClick() {
requireActivity().onBackPressedDispatcher.onBackPressed()
}
fun setNavigationIcon(@DrawableRes resId: Int) {
binding.toolbar.setNavigationIcon(resId)
}
fun setTitle(@StringRes resId: Int) {
setTitle(getString(resId))
}
fun setTitle(title: CharSequence?) {
_binding ?: return
binding.toolbar.title = title
}
override fun setLiftOnScrollTargetView(view: View?) {
binding.appbar.setLiftOnScrollTargetView(view)
}
}
File diff suppressed because it is too large. Load diff
@@ -1,578 +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.scene
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import androidx.paging.LoadState
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.CalendarConstraints.DateValidator
import com.google.android.material.datepicker.CompositeDateValidator
import com.google.android.material.datepicker.DateValidatorPointBackward
import com.google.android.material.datepicker.DateValidatorPointForward
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.client.ehUrl
import com.hippo.ehviewer.databinding.SceneFavoritesBinding
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.legacy.AddDeleteDrawable
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.FabLayout
import com.hippo.ehviewer.ui.legacy.FabLayout.OnClickFabListener
import com.hippo.ehviewer.ui.legacy.FastScroller.OnDragHandlerListener
import com.hippo.ehviewer.ui.legacy.HandlerDrawable
import com.hippo.ehviewer.ui.legacy.ViewTransition
import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.SimpleHandler
import com.hippo.ehviewer.util.getValue
import com.hippo.ehviewer.util.lazyMut
import com.hippo.ehviewer.util.setValue
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
import rikka.core.res.resolveColor
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
// Note that we do not really follow mvvm structure, just use it as ... storage
class VMStorage : ViewModel() {
var urlBuilder = FavListUrlBuilder(favCat = Settings.recentFavCat)
private val cloudDataFlow = Pager(PagingConfig(25)) {
object : PagingSource<String, BaseGalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): String? = null
override suspend fun load(params: LoadParams<String>) = withIOContext {
when (params) {
is LoadParams.Prepend -> urlBuilder.setIndex(params.key, isNext = false)
is LoadParams.Append -> urlBuilder.setIndex(params.key, isNext = true)
is LoadParams.Refresh -> {
val key = params.key
if (key.isNullOrBlank()) {
if (urlBuilder.jumpTo != null) {
urlBuilder.mNext ?: urlBuilder.setIndex("2", true)
}
} else {
urlBuilder.setIndex(key, false)
}
}
}
val r = runSuspendCatching {
EhEngine.getFavorites(urlBuilder.build())
}.onFailure {
return@withIOContext LoadResult.Error(it)
}.getOrThrow()
Settings.favCat = r.catArray
Settings.favCount = r.countArray
Settings.favCloudCount = r.countArray.sum()
urlBuilder.jumpTo = null
LoadResult.Page(r.galleryInfoList, r.prev, r.next)
}
}
}.flow.cachedIn(viewModelScope)
private val localFavDataFlow = Pager(PagingConfig(20, enablePlaceholders = false, jumpThreshold = 40)) {
val keyword = urlBuilder.keyword
if (keyword.isNullOrBlank()) {
EhDB.localFavLazyList
} else {
EhDB.searchLocalFav(keyword)
}
}.flow.cachedIn(viewModelScope)
fun dataflow() = if (urlBuilder.favCat == -2) localFavDataFlow else cloudDataFlow
val localFavCount = EhDB.localFavCount
}
class FavoritesScene : SearchBarScene() {
private val vm: VMStorage by viewModels()
private var urlBuilder by lazyMut { vm::urlBuilder }
private var _binding: SceneFavoritesBinding? = null
private val binding get() = _binding!!
private var mAdapter: GalleryAdapter? = null
private val tracker get() = mAdapter!!.tracker!!
private val showNormalFabsRunnable = Runnable {
updateJumpFab() // index: 0, 2
binding.fabLayout.run {
setSecondaryFabVisibilityAt(1, true)
(3..6).forEach { setSecondaryFabVisibilityAt(it, false) }
}
}
private val dialogState = DialogState()
override val fabLayout get() = binding.fabLayout
private fun onItemClick(position: Int) {
// Skip if in search mode
if (!tracker.isInCustomChoice) {
switchFav(position - 2)
updateJumpFab()
closeSideSheet()
}
}
private var collectJob: Job? = null
private fun switchFav(newCat: Int, keyword: String? = null) {
_binding ?: return
urlBuilder.keyword = keyword
urlBuilder.favCat = newCat
urlBuilder.jumpTo = null
urlBuilder.setIndex(null, true)
collectJob?.cancel()
collectJob = viewLifecycleOwner.lifecycleScope.launchIO {
vm.dataflow().collectLatest {
mAdapter?.submitData(it)
}
}
mAdapter?.refresh()
val favCatName: String = when (val favCat = urlBuilder.favCat) {
in 0..9 -> Settings.favCat[favCat]
FavListUrlBuilder.FAV_CAT_LOCAL -> getString(R.string.local_favorites)
else -> getString(R.string.cloud_favorites)
}
if (keyword.isNullOrEmpty()) {
setSearchBarHint(getString(R.string.favorites_title, favCatName))
setSearchBarText(null)
} else {
setSearchBarHint(getString(R.string.favorites_title_2, favCatName, keyword))
}
setEditTextHint(getString(R.string.search_bar_hint, favCatName))
Settings.recentFavCat = urlBuilder.favCat
}
override fun getMenuResId() = R.menu.scene_favorites
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mAdapter?.let { tracker.saveSelection(outState) }
}
override fun onResume() {
super.onResume()
mAdapter?.type = Settings.listMode
}
private val SceneFavoritesBinding.fastScroller get() = contentLayout.fastScroller
private val SceneFavoritesBinding.refreshLayout get() = contentLayout.refreshLayout
private val SceneFavoritesBinding.recyclerView get() = contentLayout.recyclerView
private val SceneFavoritesBinding.progress get() = contentLayout.progress
private val SceneFavoritesBinding.tip get() = contentLayout.tip
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = SceneFavoritesBinding.inflate(inflater, container!!)
container.addView(ComposeView(inflater.context).apply { setMD3Content { dialogState.Intercept() } })
setOnApplySearch {
if (!tracker.isInCustomChoice) {
switchFav(urlBuilder.favCat, it)
}
}
binding.fastScroller.attachToRecyclerView(binding.recyclerView)
binding.fastScroller.setHandlerDrawable(HandlerDrawable().apply { setColor(inflater.context.theme.resolveColor(androidx.appcompat.R.attr.colorPrimary)) })
binding.fabLayout.run {
ViewCompat.setWindowInsetsAnimationCallback(binding.root, WindowInsetsAnimationHelper(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP, this))
updateJumpFab()
val colorID = theme.resolveColor(com.google.android.material.R.attr.colorOnSurface)
val addDelete = AddDeleteDrawable(context, colorID)
primaryFab!!.setImageDrawable(addDelete)
setExpanded(expanded = false, animation = false)
setAutoCancel(true)
setHidePrimaryFab(false)
setOnClickFabListener(object : OnClickFabListener {
override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) {
if (tracker.isInCustomChoice) {
tracker.clearSelection()
} else {
binding.fabLayout.toggle()
}
}
override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {
if (!tracker.isInCustomChoice) {
when (position) {
0 -> showGoToDialog()
1 -> switchFav(urlBuilder.favCat)
2 -> {
urlBuilder.setIndex("1-0", false)
mAdapter?.refresh()
}
}
view.isExpanded = false
return
}
when (position) {
// Check all
3 -> tracker.selectAll()
// Download
4 -> CommonOperations.startDownload(mainActivity!!, takeCheckedInfo(), false)
// Delete
5 -> {
val helper = DeleteDialogHelper()
BaseDialogBuilder(context)
.setTitle(R.string.delete_favorites_dialog_title)
.setMessage(getString(R.string.delete_favorites_dialog_message, checkedSize()))
.setPositiveButton(android.R.string.ok, helper)
.show()
}
// Move
6 -> {
val helper = MoveDialogHelper()
// First is local favorite, the other 10 is cloud favorite
val localFav = getString(R.string.local_favorites)
val array = if (EhCookieStore.hasSignedIn()) {
arrayOf(localFav, *Settings.favCat)
} else {
arrayOf(localFav)
}
BaseDialogBuilder(context).setTitle(R.string.move_favorites_dialog_title).setItems(array, helper).show()
}
}
}
})
addOnExpandListener {
if (it) {
addDelete.setDelete(ANIMATE_TIME)
} else {
addDelete.setAdd(ANIMATE_TIME)
if (tracker.isInCustomChoice) tracker.clearSelection()
}
}
addAboveSnackView(this)
}
binding.fastScroller.setOnDragHandlerListener(object : OnDragHandlerListener {
override fun onStartDragHandler() {}
override fun onEndDragHandler() {
showSearchBar()
}
})
binding.recyclerView.run {
mAdapter = GalleryAdapter(
this@run,
false,
{ info ->
navAnimated(
R.id.galleryDetailScene,
bundleOf(
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
GalleryDetailScene.KEY_GALLERY_INFO to info,
),
)
},
{},
).also { adapter ->
val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)!!
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
binding.tip.setCompoundDrawables(null, drawable, null, null)
binding.tip.setOnClickListener { mAdapter?.refresh() }
binding.refreshLayout.setOnRefreshListener { mAdapter?.refresh() }
val transition = ViewTransition(binding.refreshLayout, binding.progress, binding.tip)
val empty = getString(R.string.gallery_list_empty_hit)
viewLifecycleOwner.lifecycleScope.launch {
adapter.loadStateFlow.collectLatest {
when (val state = it.refresh) {
is LoadState.Loading -> {
showSearchBar()
if (!binding.refreshLayout.isRefreshing) {
transition.showView(1)
}
}
is LoadState.Error -> {
binding.refreshLayout.isRefreshing = false
binding.tip.text = ExceptionUtils.getReadableString(state.error)
transition.showView(2)
}
is LoadState.NotLoading -> {
delay(500)
binding.refreshLayout.isRefreshing = false
if (mAdapter?.itemCount == 0) {
binding.tip.text = empty
transition.showView(2)
} else {
transition.showView(0)
}
}
}
}
}
adapter.tracker = GallerySelectionTracker(
"favorite-selection",
this,
{ adapter.snapshot().items },
{ (this as GalleryHolder).galleryId },
).apply {
addCustomChoiceListener({
showSelectionFab()
binding.fabLayout.setAutoCancel(false)
// Delay expanding action to make layout work fine
SimpleHandler.post { binding.fabLayout.isExpanded = true }
binding.refreshLayout.isEnabled = false
// Lock drawer
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
}) {
showNormalFab()
binding.fabLayout.setAutoCancel(true)
binding.fabLayout.isExpanded = false
binding.refreshLayout.isEnabled = true
// Unlock drawer
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
}
restoreSelection(savedInstanceState)
}
}
switchFav(Settings.recentFavCat)
}
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeMovementFlags(0, ItemTouchHelper.LEFT)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
) = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
val info = mAdapter!!.peek(position)!!
lifecycleScope.launchIO {
dialogState.awaitPermissionOrCancel(
confirmText = R.string.delete,
dismissText = android.R.string.cancel,
title = R.string.delete_favorites_dialog_title,
onDismiss = { mAdapter!!.notifyItemChanged(position) },
) {
Text(text = stringResource(id = R.string.delete_favorites_dialog_message, 1))
}
if (urlBuilder.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
EhDB.removeLocalFavorites(info)
} else {
EhEngine.modifyFavorites(info.gid, info.token)
}
}
}
})
itemTouchHelper.attachToRecyclerView(binding.recyclerView)
return binding.root
}
// Hide jump fab on local fav cat
private fun updateJumpFab() {
binding.fabLayout.setSecondaryFabVisibilityAt(
0,
urlBuilder.favCat != FavListUrlBuilder.FAV_CAT_LOCAL,
)
binding.fabLayout.setSecondaryFabVisibilityAt(
2,
urlBuilder.favCat != FavListUrlBuilder.FAV_CAT_LOCAL,
)
}
override fun onDestroyView() {
super.onDestroyView()
binding.recyclerView.stopScroll()
removeAboveSnackView(binding.fabLayout)
mAdapter = null
_binding = null
}
override fun onCreateDrawerView(inflater: LayoutInflater) = ComposeView(inflater.context).apply {
setMD3Content {
val localFavCount by vm.localFavCount.collectAsState(0)
ElevatedCard {
TopAppBar(title = { Text(text = stringResource(id = R.string.collections)) })
val scope = currentRecomposeScope
LaunchedEffect(Unit) {
Settings.favChangesFlow.collect {
scope.invalidate()
}
}
val localFav = stringResource(id = R.string.local_favorites) to localFavCount
val faves = if (EhCookieStore.hasSignedIn()) {
arrayOf(
localFav,
stringResource(id = R.string.cloud_favorites) to Settings.favCloudCount,
*Settings.favCat.zip(Settings.favCount.toTypedArray()).toTypedArray(),
)
} else {
arrayOf(localFav)
}
LazyColumn {
itemsIndexed(faves) { index, (name, count) ->
ListItem(
headlineContent = { Text(text = name) },
trailingContent = { Text(text = count.toString(), style = MaterialTheme.typography.bodyLarge) },
modifier = Modifier.clickable { onItemClick(index) },
)
}
}
}
}
}
override fun onSearchViewExpanded() {
super.onSearchViewExpanded()
selectSearchFab(true)
}
override fun onSearchViewHidden() {
super.onSearchViewHidden()
selectActionFab(true)
}
private fun showGoToDialog() {
context ?: return
val local = LocalDateTime.of(2007, 3, 21, 0, 0)
val fromDate = local.atZone(ZoneId.ofOffset("UTC", ZoneOffset.UTC)).toInstant().toEpochMilli()
val toDate = MaterialDatePicker.todayInUtcMilliseconds()
val listValidators = ArrayList<DateValidator>()
listValidators.add(DateValidatorPointForward.from(fromDate))
listValidators.add(DateValidatorPointBackward.before(toDate))
val constraintsBuilder = CalendarConstraints.Builder()
.setStart(fromDate)
.setEnd(toDate)
.setValidator(CompositeDateValidator.allOf(listValidators))
val datePicker = MaterialDatePicker.Builder.datePicker()
.setCalendarConstraints(constraintsBuilder.build())
.setTitleText(R.string.go_to)
.setSelection(toDate)
.build()
datePicker.show(requireActivity().supportFragmentManager, "date-picker")
datePicker.addOnPositiveButtonClickListener { time: Long ->
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC)
val jumpTo = formatter.format(Instant.ofEpochMilli(time))
urlBuilder.jumpTo = jumpTo
mAdapter?.refresh()
}
}
private fun takeCheckedInfo() = tracker.getAndClearSelection()
private fun checkedSize() = tracker.selectionSize
private fun showNormalFab() {
// Delay showing normal fab to avoid mutation
SimpleHandler.removeCallbacks(showNormalFabsRunnable)
SimpleHandler.postDelayed(showNormalFabsRunnable, 300)
}
private fun showSelectionFab() {
SimpleHandler.removeCallbacks(showNormalFabsRunnable)
binding.fabLayout.run {
(0..2).forEach { setSecondaryFabVisibilityAt(it, false) }
(3..6).forEach { setSecondaryFabVisibilityAt(it, true) }
}
}
private inner class DeleteDialogHelper : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, which: Int) {
val info = takeCheckedInfo()
lifecycleScope.launchIO {
if (urlBuilder.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Delete local fav
EhDB.removeLocalFavorites(info)
} else {
val delList = info.map { it.gid to it.token!! }
EhEngine.modifyFavoritesRange(delList, -1)
}
mAdapter?.refresh()
}
}
}
private inner class MoveDialogHelper : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, which: Int) {
val srcCat = urlBuilder.favCat
val dstCat = if (which == 0) { FavListUrlBuilder.FAV_CAT_LOCAL } else { which - 1 }
if (srcCat == dstCat) return
val info = takeCheckedInfo()
lifecycleScope.launchIO {
if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
// Move from local to cloud
EhDB.removeLocalFavorites(info)
val galleryList = info.map { it.gid to it.token!! }
runCatching {
EhEngine.modifyFavoritesRange(galleryList, dstCat)
}
} else if (dstCat == FavListUrlBuilder.FAV_CAT_LOCAL) {
// Move from cloud to local
EhDB.putLocalFavorites(info)
} else {
// Move from cloud to cloud
val gidArray = info.map { it.gid }.toLongArray()
val url = ehUrl { addPathSegments(EhUrl.FAV_PATH) }.toString()
runCatching {
EhEngine.modifyFavorites(url, gidArray, dstCat)
}
}
mAdapter?.refresh()
}
}
}
}
@@ -1,169 +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.scene
import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IntDef
import androidx.compose.ui.platform.ComposeView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import arrow.core.partially1
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.Settings.detailSize
import com.hippo.ehviewer.Settings.thumbSizeDp
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager
import com.hippo.ehviewer.ui.legacy.MarginItemDecoration
import com.hippo.ehviewer.ui.legacy.STRATEGY_MIN_SIZE
import com.hippo.ehviewer.ui.legacy.STRATEGY_SUITABLE_SIZE
import com.hippo.ehviewer.util.dp2px
import splitties.init.appCtx
private val diffCallback = object : DiffUtil.ItemCallback<BaseGalleryInfo>() {
override fun areItemsTheSame(oldItem: BaseGalleryInfo, newItem: BaseGalleryInfo) = oldItem.gid == newItem.gid
override fun areContentsTheSame(oldItem: BaseGalleryInfo, newItem: BaseGalleryInfo) = oldItem.gid == newItem.gid
}
class GalleryAdapter(
private val recyclerView: RecyclerView,
private val showFavorite: Boolean,
private val onItemClick: (BaseGalleryInfo) -> Unit,
private val onItemLongClick: (BaseGalleryInfo) -> Unit,
) : PagingDataAdapter<BaseGalleryInfo, GalleryHolder>(diffCallback) {
private val resources = recyclerView.context.resources
private val layoutManager: AutoStaggeredGridLayoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
private var mListDecoration: ItemDecoration? = null
private var mGirdDecoration: MarginItemDecoration? = null
private var mType = TYPE_INVALID
var tracker: GallerySelectionTracker<BaseGalleryInfo>? = null
var type: Int
get() = mType
set(type) {
if (type == mType) return
mType = type
when (type) {
TYPE_LIST -> {
val columnWidth = resources.getDimensionPixelOffset(
when (detailSize) {
0 -> R.dimen.gallery_list_column_width_long
1 -> R.dimen.gallery_list_column_width_short
else -> error("Unexpected value: $detailSize")
},
)
layoutManager.setColumnSize(columnWidth)
layoutManager.setStrategy(STRATEGY_MIN_SIZE)
if (null != mGirdDecoration) {
recyclerView.removeItemDecoration(mGirdDecoration!!)
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_h)
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_v)
recyclerView.setPadding(
recyclerView.paddingLeft - paddingH,
recyclerView.paddingTop - paddingV,
recyclerView.paddingRight - paddingH,
recyclerView.paddingBottom - paddingV,
)
mGirdDecoration = null
}
if (null == mListDecoration) {
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)
mListDecoration = object : ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
outRect.set(0, interval / 2, 0, interval / 2)
}
}
recyclerView.setPadding(
recyclerView.paddingLeft + paddingH,
recyclerView.paddingTop + paddingV,
recyclerView.paddingRight + paddingH,
recyclerView.paddingBottom + paddingV,
)
recyclerView.addItemDecoration(mListDecoration!!)
}
}
TYPE_GRID -> {
val columnWidth = dp2px(appCtx, thumbSizeDp.toFloat())
layoutManager.setColumnSize(columnWidth)
layoutManager.setStrategy(STRATEGY_SUITABLE_SIZE)
if (null != mListDecoration) {
recyclerView.removeItemDecoration(mListDecoration!!)
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)
recyclerView.setPadding(
recyclerView.paddingLeft - paddingH,
recyclerView.paddingTop - paddingV,
recyclerView.paddingRight - paddingH,
recyclerView.paddingBottom - paddingV,
)
mListDecoration = null
}
if (null == mGirdDecoration) {
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_grid_interval)
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_h)
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_v)
mGirdDecoration = MarginItemDecoration(interval, 0, 0, 0, 0)
recyclerView.setPadding(
recyclerView.paddingLeft + paddingH,
recyclerView.paddingTop + paddingV,
recyclerView.paddingRight + paddingH,
recyclerView.paddingBottom + paddingV,
)
recyclerView.addItemDecoration(mGirdDecoration!!)
}
}
}
refresh()
}
init {
recyclerView.adapter = this
recyclerView.layoutManager = layoutManager
type = Settings.listMode
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
TYPE_LIST -> ListGalleryHolder(ComposeView(parent.context), showFavorite)
TYPE_GRID -> GridGalleryHolder(ComposeView(parent.context))
else -> error("Unexpected value: $viewType")
}
override fun getItemViewType(position: Int) = mType
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
val gi = getItem(position) ?: return
val selected = tracker?.isSelected(gi.gid) ?: false
holder.galleryId = gi.gid
holder.bind(gi, selected, onItemClick.partially1(gi), onItemLongClick.partially1(gi))
}
@IntDef(TYPE_LIST, TYPE_GRID)
@Retention(AnnotationRetention.SOURCE)
annotation class Type
companion object {
const val TYPE_INVALID = -1
const val TYPE_LIST = 0
const val TYPE_GRID = 1
}
}
@@ -1,885 +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.scene
import android.animation.Animator
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Looper
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.CharacterStyle
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan
import android.text.style.URLSpan
import android.text.style.UnderlineSpan
import android.util.Log
import android.view.ActionMode
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat
import androidx.core.text.getSpans
import androidx.core.text.inSpans
import androidx.core.text.parseAsHtml
import androidx.core.text.set
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import androidx.viewbinding.ViewBinding
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhFilter.remember
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryComment
import com.hippo.ehviewer.client.data.GalleryCommentList
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.parser.VoteCommentResult
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.FilterMode
import com.hippo.ehviewer.databinding.ItemDrawerFavoritesBinding
import com.hippo.ehviewer.databinding.ItemGalleryCommentBinding
import com.hippo.ehviewer.databinding.ItemGalleryCommentMoreBinding
import com.hippo.ehviewer.databinding.ItemGalleryCommentProgressBinding
import com.hippo.ehviewer.databinding.SceneGalleryCommentsBinding
import com.hippo.ehviewer.ui.jumpToReaderByPage
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.CoilImageGetter
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
import com.hippo.ehviewer.ui.legacy.ViewTransition
import com.hippo.ehviewer.ui.legacy.WindowInsetsAnimationHelper
import com.hippo.ehviewer.ui.openBrowser
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.IntList
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.hippo.ehviewer.util.TextUrl
import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.getParcelableCompat
import com.hippo.ehviewer.util.toBBCode
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import moe.tarsin.coroutines.runSuspendCatching
import rikka.core.res.resolveColor
import kotlin.math.hypot
class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefreshListener {
private var _binding: SceneGalleryCommentsBinding? = null
private val binding get() = _binding!!
private val callback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
if (!mInAnimation && binding.editPanel.visibility == View.VISIBLE) {
hideEditPanel(true)
}
}
}
private var mGalleryDetail: GalleryDetail? = null
private var mAdapter: CommentAdapter? = null
private var mViewTransition: ViewTransition? = null
private var mSendDrawable: Drawable? = null
private var mPencilDrawable: Drawable? = null
private var mCommentId: Long = 0
private var mInAnimation = false
private var mShowAllComments = false
private var mRefreshingComments = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(callback)
if (savedInstanceState == null) {
onInit()
} else {
onRestore(savedInstanceState)
}
}
private fun handleArgs(args: Bundle?) {
if (args == null) {
return
}
mGalleryDetail = args.getParcelableCompat(KEY_GALLERY_DETAIL)
mShowAllComments =
mGalleryDetail != null && true && !mGalleryDetail!!.comments.hasMore
}
private fun onInit() {
handleArgs(arguments)
}
private fun onRestore(savedInstanceState: Bundle) {
mGalleryDetail = savedInstanceState.getParcelableCompat(KEY_GALLERY_DETAIL)
mShowAllComments = mGalleryDetail != null && !mGalleryDetail!!.comments.hasMore
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(KEY_GALLERY_DETAIL, mGalleryDetail)
}
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = SceneGalleryCommentsBinding.inflate(inflater, container!!)
setLiftOnScrollTargetView(binding.recyclerView)
val tip = binding.tip
ViewCompat.setWindowInsetsAnimationCallback(
binding.root,
WindowInsetsAnimationHelper(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
binding.editPanel,
binding.fabLayout,
),
)
binding.refreshLayout.setOnRefreshListener(this)
val context = requireContext()
val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
tip.setCompoundDrawables(null, drawable, null, null)
mSendDrawable = ContextCompat.getDrawable(context, R.drawable.v_send_dark_x24)
mPencilDrawable = ContextCompat.getDrawable(context, R.drawable.v_pencil_dark_x24)
mAdapter = CommentAdapter()
binding.recyclerView.adapter = mAdapter
binding.recyclerView.layoutManager = LinearLayoutManager(
context,
RecyclerView.VERTICAL,
false,
)
binding.recyclerView.setHasFixedSize(true)
// Cancel change animator
val itemAnimator = binding.recyclerView.itemAnimator
if (itemAnimator is DefaultItemAnimator) {
itemAnimator.supportsChangeAnimations = false
}
binding.send.setOnClickListener(this)
binding.editText.customSelectionActionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
requireActivity().menuInflater.inflate(R.menu.context_comment, menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return true
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
item?.let {
val text = binding.editText.editableText
val start = binding.editText.selectionStart
val end = binding.editText.selectionEnd
when (item.itemId) {
R.id.action_bold -> text[start, end] = StyleSpan(Typeface.BOLD)
R.id.action_italic -> text[start, end] = StyleSpan(Typeface.ITALIC)
R.id.action_underline -> text[start, end] = UnderlineSpan()
R.id.action_strikethrough -> text[start, end] = StrikethroughSpan()
R.id.action_url -> {
val oldSpans = text.getSpans<URLSpan>(start, end)
var oldUrl = "https://"
oldSpans.forEach {
if (!it.url.isNullOrEmpty()) {
oldUrl = it.url
}
}
val builder = EditTextDialogBuilder(
context,
oldUrl,
getString(R.string.format_url),
)
builder.setTitle(getString(R.string.format_url))
builder.setPositiveButton(android.R.string.ok, null)
val dialog = builder.show()
val button: View? = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
button?.setOnClickListener(
View.OnClickListener {
val url = builder.text.trim()
if (url.isEmpty()) {
builder.setError(getString(R.string.text_is_empty))
return@OnClickListener
} else {
builder.setError(null)
}
text.clearSpan(start, end, true)
text[start, end] = URLSpan(url)
dialog.dismiss()
},
)
}
R.id.action_clear -> {
text.clearSpan(start, end, false)
}
else -> return false
}
mode?.finish()
}
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
}
}
binding.fab.setOnClickListener(this)
addAboveSnackView(binding.editPanel)
addAboveSnackView(binding.fabLayout)
mViewTransition = ViewTransition(binding.refreshLayout, tip)
updateView(false)
return binding.root
}
fun Spannable.clearSpan(start: Int, end: Int, url: Boolean) {
val spans = if (url) getSpans<URLSpan>(start, end) else getSpans<CharacterStyle>(start, end)
spans.forEach {
val spanStart = getSpanStart(it)
val spanEnd = getSpanEnd(it)
removeSpan(it)
if (spanStart < start) {
this[spanStart, start] = it
}
if (spanEnd > end) {
this[end, spanEnd] = it
}
}
}
override fun onDestroyView() {
super.onDestroyView()
callback.remove()
binding.recyclerView.stopScroll()
removeAboveSnackView(binding.editPanel)
removeAboveSnackView(binding.fabLayout)
mAdapter = null
mViewTransition = null
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(R.string.gallery_comments)
setNavigationIcon(R.drawable.v_arrow_left_dark_x24)
}
private fun showFilterCommenterDialog(commenter: String?, position: Int) {
val context = context
if (context == null || commenter == null) {
return
}
BaseDialogBuilder(context)
.setMessage(getString(R.string.filter_the_commenter, commenter))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
Filter(FilterMode.COMMENTER, commenter).remember()
hideComment(position)
showTip(R.string.filter_added, LENGTH_SHORT)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun hideComment(position: Int) {
if (mGalleryDetail == null) {
return
}
val oldCommentsList = mGalleryDetail!!.comments.comments
val newCommentsList = arrayOfNulls<GalleryComment>(
oldCommentsList.size - 1,
)
var i = 0
var j = 0
while (i < oldCommentsList.size) {
if (i != position) {
newCommentsList[j] = oldCommentsList[i]
j++
}
i++
}
mGalleryDetail!!.comments.comments = newCommentsList.requireNoNulls()
mAdapter!!.notifyDataSetChanged()
updateView(true)
}
private fun voteComment(id: Long, vote: Int) {
val gd = mGalleryDetail ?: return
lifecycleScope.launchIO {
runSuspendCatching {
EhEngine.voteComment(gd.apiUid, gd.apiKey, gd.gid, gd.token, id, vote)
}.onSuccess { result ->
val voteUp = vote > 0
showTip(
if (voteUp) (if (0 != result.vote) R.string.vote_up_successfully else R.string.cancel_vote_up_successfully) else if (0 != result.vote) R.string.vote_down_successfully else R.string.cancel_vote_down_successfully,
LENGTH_SHORT,
)
withUIContext { onVoteCommentSuccess(result, voteUp) }
}.onFailure {
showTip(R.string.vote_failed, LENGTH_LONG)
}
}
}
@SuppressLint("InflateParams")
fun showVoteStatusDialog(context: Context, voteStatus: String) {
val temp = voteStatus.split(',')
val length = temp.size
val userArray = arrayOfNulls<String>(length)
val voteArray = arrayOfNulls<String>(length)
for (i in 0 until length) {
val str = temp[i].trim()
val index = str.lastIndexOf(' ')
if (index < 0) {
Log.d(TAG, "Something wrong happened about vote state")
userArray[i] = str
voteArray[i] = ""
} else {
userArray[i] = str.substring(0, index).trim()
voteArray[i] = str.substring(index + 1).trim()
}
}
val builder = BaseDialogBuilder(context)
val builderContext = builder.context
val inflater = LayoutInflater.from(builderContext)
val rv = inflater.inflate(R.layout.dialog_recycler_view, null) as RecyclerView
rv.adapter = object : RecyclerView.Adapter<VoteHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VoteHolder {
return VoteHolder(ItemDrawerFavoritesBinding.inflate(inflater, parent, false))
}
override fun onBindViewHolder(holder: VoteHolder, position: Int) {
holder.bind(userArray[position], voteArray[position])
}
override fun getItemCount(): Int {
return length
}
}
rv.layoutManager = LinearLayoutManager(builderContext)
rv.clipToPadding = false
builder.setView(rv).show()
}
private fun showCommentDialog(position: Int, text: CharSequence) {
val context = context
if (context == null || mGalleryDetail == null || position >= mGalleryDetail!!.comments.comments.size || position < 0) {
return
}
val comment = mGalleryDetail!!.comments.comments[position]
val menu: MutableList<String> = ArrayList()
val menuId = IntList()
val resources = context.resources
menu.add(resources.getString(R.string.copy_comment_text))
menuId.add(R.id.copy)
if (!comment.uploader && !comment.editable) {
menu.add(resources.getString(R.string.block_commenter))
menuId.add(R.id.block_commenter)
}
if (comment.editable) {
menu.add(resources.getString(R.string.edit_comment))
menuId.add(R.id.edit_comment)
}
if (comment.voteUpAble) {
menu.add(resources.getString(if (comment.voteUpEd) R.string.cancel_vote_up else R.string.vote_up))
menuId.add(R.id.vote_up)
}
if (comment.voteDownAble) {
menu.add(resources.getString(if (comment.voteDownEd) R.string.cancel_vote_down else R.string.vote_down))
menuId.add(R.id.vote_down)
}
if (!comment.voteState.isNullOrEmpty()) {
menu.add(resources.getString(R.string.check_vote_status))
menuId.add(R.id.check_vote_status)
}
BaseDialogBuilder(context)
.setItems(menu.toTypedArray()) { _: DialogInterface?, which: Int ->
if (which < 0 || which >= menuId.size) {
return@setItems
}
when (menuId[which]) {
R.id.copy -> requireActivity().addTextToClipboard(text, false)
R.id.block_commenter -> showFilterCommenterDialog(comment.user, position)
R.id.vote_up -> voteComment(comment.id, 1)
R.id.vote_down -> voteComment(comment.id, -1)
R.id.check_vote_status -> showVoteStatusDialog(context, comment.voteState!!)
R.id.edit_comment -> {
prepareEditComment(comment.id, text)
if (!mInAnimation && binding.editPanel.visibility != View.VISIBLE) {
showEditPanel(true)
}
}
}
}.show()
}
fun onItemClick(parent: RecyclerView?, view2: View?, position: Int): Boolean {
val activity = mainActivity ?: return false
val holder = parent!!.getChildViewHolder(view2!!)
if (holder is ActualCommentHolder) {
val span = holder.binding.comment.currentSpan
holder.binding.comment.clearCurrentSpan()
val detail = mGalleryDetail ?: return false
if (span is URLSpan) {
if (!activity.jumpToReaderByPage(span.url, detail)) {
if (!findNavController().navWithUrl(span.url)) {
activity.openBrowser(span.url)
}
}
} else {
showCommentDialog(position, holder.sp)
}
} else if (holder is MoreCommentHolder && !mRefreshingComments && mAdapter != null) {
mRefreshingComments = true
mShowAllComments = true
mAdapter!!.notifyItemChanged(position)
galleryDetailUrl?.let { refreshComment(it) }
}
return true
}
private fun refreshComment(url: String) {
lifecycleScope.launchIO {
runSuspendCatching {
EhEngine.getGalleryDetail(url)
}.onSuccess {
withUIContext {
onRefreshGallerySuccess(it.comments)
}
}.onFailure {
withUIContext {
onRefreshGalleryFailure()
}
}
}
}
private fun updateView(animation: Boolean) {
if (null == mViewTransition) {
return
}
if (mGalleryDetail == null || mGalleryDetail!!.comments.comments.isEmpty()) {
mViewTransition!!.showView(1, animation)
} else {
mViewTransition!!.showView(0, animation)
}
}
private fun prepareNewComment() {
mCommentId = 0
binding.send.setImageDrawable(mSendDrawable)
}
private fun prepareEditComment(commentId: Long, text: CharSequence) {
mCommentId = commentId
binding.editText.setText(text)
binding.send.setImageDrawable(mPencilDrawable)
}
private fun showEditPanelWithAnimation() {
mInAnimation = true
binding.fab.translationX = 0.0f
binding.fab.translationY = 0.0f
binding.fab.scaleX = 1.0f
binding.fab.scaleY = 1.0f
val fabEndX = binding.editPanel.left + binding.editPanel.width / 2 - binding.fab.width / 2
val fabEndY = binding.editPanel.top + binding.editPanel.height / 2 - binding.fab.height / 2
binding.fab.animate().x(fabEndX.toFloat()).y(fabEndY.toFloat()).scaleX(0.0f).scaleY(0.0f)
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
.setDuration(300L).setListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
(binding.fab as View).visibility = View.INVISIBLE
binding.editPanel.visibility = View.VISIBLE
val halfW = binding.editPanel.width / 2
val halfH = binding.editPanel.height / 2
val animator = ViewAnimationUtils.createCircularReveal(
binding.editPanel,
halfW,
halfH,
0f,
hypot(halfW.toDouble(), halfH.toDouble()).toFloat(),
).setDuration(300L)
animator.addListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mInAnimation = false
}
})
animator.start()
}
}).start()
}
private fun showEditPanel(animation: Boolean) {
callback.isEnabled = true
if (animation) {
showEditPanelWithAnimation()
} else {
(binding.fab as View).visibility = View.INVISIBLE
binding.editPanel.visibility = View.VISIBLE
}
}
private fun hideEditPanelWithAnimation() {
mInAnimation = true
val halfW = binding.editPanel.width / 2
val halfH = binding.editPanel.height / 2
val animator = ViewAnimationUtils.createCircularReveal(
binding.editPanel,
halfW,
halfH,
hypot(halfW.toDouble(), halfH.toDouble()).toFloat(),
0.0f,
).setDuration(300L)
animator.addListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
if (Looper.myLooper() != Looper.getMainLooper()) {
// Some devices may run this block in non-UI thread.
// It might be a bug of Android OS.
// Check it here to avoid crash.
return
}
binding.editPanel.visibility = View.GONE
(binding.fab as View).visibility = View.VISIBLE
val fabStartX =
binding.editPanel.left + binding.editPanel.width / 2 - binding.fab.width / 2
val fabStartY =
binding.editPanel.top + binding.editPanel.height / 2 - binding.fab.height / 2
binding.fab.x = fabStartX.toFloat()
binding.fab.y = fabStartY.toFloat()
binding.fab.scaleX = 0.0f
binding.fab.scaleY = 0.0f
binding.fab.rotation = -45.0f
binding.fab.animate().translationX(0.0f).translationY(0.0f).scaleX(1.0f)
.scaleY(1.0f)
.rotation(0.0f)
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
.setDuration(300L).setListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mInAnimation = false
}
}).start()
}
})
animator.start()
}
private fun hideEditPanel(animation: Boolean) {
callback.isEnabled = false
hideSoftInput()
if (animation) {
hideEditPanelWithAnimation()
} else {
(binding.fab as View).visibility = View.VISIBLE
binding.editPanel.visibility = View.INVISIBLE
}
}
private val galleryDetailUrl: String?
get() = if (mGalleryDetail != null && mGalleryDetail!!.gid != -1L && mGalleryDetail!!.token != null) {
EhUrl.getGalleryDetailUrl(
mGalleryDetail!!.gid,
mGalleryDetail!!.token,
0,
mShowAllComments,
)
} else {
null
}
override fun onClick(v: View) {
val context = context
val activity = mainActivity
if (null == context || null == activity) {
return
}
if (binding.fab === v) {
if (!mInAnimation) {
prepareNewComment()
showEditPanel(true)
}
} else if (binding.send === v) {
if (!mInAnimation) {
val comment = binding.editText.text?.toBBCode()?.takeIf { it.isNotBlank() } ?: return
val url = galleryDetailUrl ?: return
lifecycleScope.launchIO {
runSuspendCatching {
EhEngine.commentGallery(
url,
comment,
if (mCommentId != 0L) mCommentId.toString() else null,
)
}.onSuccess {
showTip(
if (mCommentId != 0L) R.string.edit_comment_successfully else R.string.comment_successfully,
LENGTH_SHORT,
)
withUIContext {
onCommentGallerySuccess(it)
}
}.onFailure {
showTip(
"""
${getString(if (mCommentId != 0L) R.string.edit_comment_failed else R.string.comment_failed)}
${ExceptionUtils.getReadableString(it)}
""".trimIndent(),
LENGTH_LONG,
)
}
}
hideSoftInput()
hideEditPanel(true)
}
}
}
private fun onRefreshGallerySuccess(result: GalleryCommentList) {
if (mGalleryDetail == null || mAdapter == null) {
return
}
binding.refreshLayout.isRefreshing = false
mRefreshingComments = false
mGalleryDetail!!.comments = result
mAdapter!!.notifyDataSetChanged()
updateView(true)
}
private fun onRefreshGalleryFailure() {
if (mAdapter == null) {
return
}
binding.refreshLayout.isRefreshing = false
mRefreshingComments = false
val position = mAdapter!!.itemCount - 1
if (position >= 0) {
mAdapter!!.notifyItemChanged(position)
}
}
private fun onCommentGallerySuccess(result: GalleryCommentList) {
if (mGalleryDetail == null || mAdapter == null) {
return
}
mGalleryDetail!!.comments = result
mAdapter!!.notifyDataSetChanged()
// Remove text
binding.editText.setText("")
updateView(true)
}
private fun onVoteCommentSuccess(result: VoteCommentResult, voteUp: Boolean) {
if (mAdapter == null) {
return
}
var position = -1
var i = 0
val n = mGalleryDetail!!.comments.comments.size
while (i < n) {
val comment = mGalleryDetail!!.comments.comments[i]
if (comment.id == result.id) {
position = i
break
}
i++
}
if (-1 == position) {
Log.d(TAG, "Can't find comment with id " + result.id)
return
}
// Update comment
val comment = mGalleryDetail!!.comments.comments[position]
comment.score = result.score
if (voteUp) {
comment.voteUpEd = 0 != result.vote
comment.voteDownEd = false
} else {
comment.voteDownEd = 0 != result.vote
comment.voteUpEd = false
}
mAdapter!!.notifyItemChanged(position)
}
override fun onRefresh() {
if (!mRefreshingComments && mAdapter != null) {
mRefreshingComments = true
galleryDetailUrl?.let { refreshComment(it) }
}
}
private class VoteHolder(private val binding: ItemDrawerFavoritesBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(user: String?, vote: String?) {
binding.key.text = user
binding.value.text = vote
}
}
private abstract class CommentHolder(binding: ViewBinding) :
RecyclerView.ViewHolder(binding.root)
private class MoreCommentHolder(binding: ItemGalleryCommentMoreBinding) : CommentHolder(binding)
private class ProgressCommentHolder(binding: ItemGalleryCommentProgressBinding) :
CommentHolder(binding)
private inner class ActualCommentHolder(val binding: ItemGalleryCommentBinding) :
CommentHolder(binding) {
lateinit var sp: CharSequence
private fun generateComment(
context: Context,
textView: TextView,
comment: GalleryComment,
): CharSequence {
sp = comment.comment.orEmpty().parseAsHtml(imageGetter = CoilImageGetter(textView))
val ssb = SpannableStringBuilder(sp)
if (0L != comment.id && 0 != comment.score) {
val score = comment.score
val scoreString = if (score > 0) "+$score" else score.toString()
ssb.append(" ").inSpans(
RelativeSizeSpan(0.8f),
StyleSpan(Typeface.BOLD),
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
) {
append(scoreString)
}
}
if (comment.lastEdited != 0L) {
val str = context.getString(
R.string.last_edited,
ReadableTime.getTimeAgo(comment.lastEdited),
)
ssb.append("\n\n").inSpans(
RelativeSizeSpan(0.8f),
StyleSpan(Typeface.BOLD),
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
) {
append(str)
}
}
return TextUrl.handleTextUrl(ssb)
}
fun bind(value: GalleryComment) {
binding.run {
user.text = value.user?.let {
if (value.uploader) getString(R.string.comment_user_uploader, it) else it
}
user.setOnClickListener {
value.user?.let {
val lub = ListUrlBuilder().apply {
mode = ListUrlBuilder.MODE_UPLOADER
keyword = it
}
navAnimated(R.id.galleryListScene, lub.toStartArgs(), true)
}
}
time.text = ReadableTime.getTimeAgo(value.time)
comment.text = generateComment(binding.comment.context, binding.comment, value)
}
}
}
private inner class CommentAdapter : RecyclerView.Adapter<CommentHolder>() {
private val mInflater: LayoutInflater = layoutInflater
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentHolder {
return when (viewType) {
TYPE_COMMENT -> ActualCommentHolder(
ItemGalleryCommentBinding.inflate(mInflater, parent, false),
)
TYPE_MORE -> MoreCommentHolder(
ItemGalleryCommentMoreBinding.inflate(mInflater, parent, false),
)
TYPE_PROGRESS -> ProgressCommentHolder(
ItemGalleryCommentProgressBinding.inflate(mInflater, parent, false),
)
else -> throw IllegalStateException("Invalid view type: $viewType")
}
}
override fun onBindViewHolder(holder: CommentHolder, position: Int) {
val context = context
if (context == null || mGalleryDetail == null) {
return
}
holder.itemView.setOnClickListener {
onItemClick(
binding.recyclerView,
holder.itemView,
position,
)
}
holder.itemView.isClickable = true
holder.itemView.isFocusable = true
if (holder is ActualCommentHolder) {
holder.bind(mGalleryDetail!!.comments.comments[position])
}
}
override fun getItemCount(): Int {
return if (mGalleryDetail == null) {
0
} else if (mGalleryDetail!!.comments.hasMore) {
mGalleryDetail!!.comments.comments.size + 1
} else {
mGalleryDetail!!.comments.comments.size
}
}
override fun getItemViewType(position: Int): Int {
return if (position >= mGalleryDetail!!.comments.comments.size) {
if (mRefreshingComments) TYPE_PROGRESS else TYPE_MORE
} else {
TYPE_COMMENT
}
}
}
companion object {
val TAG: String = GalleryCommentsScene::class.java.simpleName
const val KEY_API_UID = "api_uid"
const val KEY_API_KEY = "api_key"
const val KEY_GID = "gid"
const val KEY_TOKEN = "token"
const val KEY_COMMENT_LIST = "comment_list"
const val KEY_GALLERY_DETAIL = "gallery_detail"
private const val TYPE_COMMENT = 0
private const val TYPE_MORE = 1
private const val TYPE_PROGRESS = 2
}
}
File diff suppressed because it is too large. Load diff
@@ -1,99 +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.scene
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.recyclerview.widget.RecyclerView
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.Settings.listThumbSize
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
import com.hippo.ehviewer.ui.setMD3Content
import eu.kanade.tachiyomi.util.system.pxToDp
abstract class GalleryHolder(composeView: ComposeView) : RecyclerView.ViewHolder(composeView) {
var galleryId = 0L
abstract fun bind(
galleryInfo: GalleryInfo,
isChecked: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
)
}
class ListGalleryHolder(private val composeView: ComposeView, private val showFavourite: Boolean) : GalleryHolder(composeView) {
private val height = (3 * listThumbSize * 3).pxToDp.dp
private val showPages = Settings.showGalleryPages
override fun bind(galleryInfo: GalleryInfo, isChecked: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) {
composeView.setMD3Content {
CheckableItem(checked = isChecked) {
GalleryInfoListItem(
onClick = onClick,
onLongClick = onLongClick,
info = galleryInfo,
modifier = Modifier.height(height),
isInFavScene = !showFavourite,
showPages = showPages,
)
}
}
}
}
class GridGalleryHolder(private val composeView: ComposeView) : GalleryHolder(composeView) {
override fun bind(galleryInfo: GalleryInfo, isChecked: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) {
composeView.setMD3Content {
CheckableItem(checked = isChecked) {
GalleryInfoGridItem(
onClick = onClick,
onLongClick = onLongClick,
info = galleryInfo,
)
}
}
// Workaround https://github.com/Ehviewer-Overhauled/Ehviewer/issues/1023
// See https://issuetracker.google.com/issues/240449681
composeView.getChildAt(0)?.requestLayout()
}
}
@Composable
fun CheckableItem(checked: Boolean, content: @Composable () -> Unit) {
Box {
content()
if (checked) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.align(Alignment.TopEnd),
)
}
}
}
File diff suppressed because it is too large. Load diff
@@ -1,164 +0,0 @@
package com.hippo.ehviewer.ui.scene
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import androidx.room.paging.util.getClippedRefreshKey
import androidx.room.paging.util.getLimit
import androidx.room.paging.util.getOffset
import arrow.fx.coroutines.parMap
import coil.imageLoader
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.legacy.calculateSuitableSpanCount
import com.hippo.ehviewer.ui.main.EhPreviewItem
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid
import com.hippo.ehviewer.ui.tools.rememberDialogState
import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.util.getParcelableCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
class GalleryPreviewScreen : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = ComposeView(inflater.context).apply {
setMD3Content {
val galleryDetail = remember { requireArguments().getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)!! }
val context = LocalContext.current
fun onPreviewCLick(index: Int) = context.navToReader(galleryDetail.galleryInfo, index)
var toNextPage by rememberSaveable { mutableStateOf(requireArguments().getBoolean(KEY_NEXT_PAGE)) }
val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior()
val columnCount = calculateSuitableSpanCount()
val state = rememberLazyGridState()
val dialogState = rememberDialogState()
dialogState.Intercept()
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val pages = galleryDetail.pages
val pgSize = galleryDetail.previewList.size
LaunchedEffect(Unit) {
if (toNextPage) state.scrollToItem(pgSize)
toNextPage = false
}
suspend fun getPreviewListByPage(page: Int) = galleryDetail.run {
val url = EhUrl.getGalleryDetailUrl(gid, token, page, false)
val result = EhEngine.getPreviewList(url)
if (Settings.preloadThumbAggressively) {
coroutineScope.launch {
context.run { result.first.first.forEach { imageLoader.enqueue(imageRequest(it) { justDownload() }) } }
}
}
result.first.first
}
val data = rememberInVM {
val previewPagesMap = galleryDetail.previewList.associateBy { it.position } as MutableMap
Pager(PagingConfig(pageSize = pgSize, initialLoadSize = pgSize, jumpThreshold = 2 * pgSize)) {
object : PagingSource<Int, GalleryPreview>() {
override fun getRefreshKey(state: PagingState<Int, GalleryPreview>) = state.getClippedRefreshKey()
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GalleryPreview> {
val key = params.key ?: 0
val up = getOffset(params, key, pages)
val end = (up + getLimit(params, key) - 1).coerceAtMost(pages - 1)
runSuspendCatching {
(up..end).filterNot { it in previewPagesMap }.map { it / pgSize }.toSet()
.parMap(Dispatchers.IO) { getPreviewListByPage(it) }
.forEach { previews -> previews.forEach { previewPagesMap[it.position] = it } }
}.onFailure {
return LoadResult.Error(it)
}
val r = (up..end).map { requireNotNull(previewPagesMap[it]) }
val prevK = if (up <= 0 || r.isEmpty()) null else up
val nextK = if (end == pages - 1) null else end + 1
return LoadResult.Page(r, prevK, nextK, up, pages - end - 1)
}
override val jumpingSupported = true
}
}.flow.cachedIn(viewModelScope)
}.collectAsLazyPagingItems()
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.gallery_previews)) },
navigationIcon = {
IconButton(onClick = { findNavController().popBackStack() }) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
scrollBehavior = scrollBehaviour,
)
},
) { paddingValues ->
FastScrollLazyVerticalGrid(
columns = GridCells.Fixed(columnCount),
modifier = Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection).padding(horizontal = dimensionResource(id = R.dimen.gallery_list_margin_h)),
state = state,
contentPadding = paddingValues,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(
count = data.itemCount,
key = data.itemKey(key = { item -> item.position }),
contentType = data.itemContentType(),
) { index ->
val item = data[index]
EhPreviewItem(item, index) {
onPreviewCLick(index)
}
}
}
}
}
}
}
const val KEY_GALLERY_DETAIL = "gallery_detail"
const val KEY_NEXT_PAGE = "next_page"
@@ -1,77 +0,0 @@
package com.hippo.ehviewer.ui.scene
import android.os.Bundle
import android.view.MotionEvent
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.ItemKeyProvider
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.selection.StorageStrategy
import androidx.recyclerview.widget.RecyclerView
import com.hippo.ehviewer.client.data.GalleryInfo
class GallerySelectionTracker<T : GalleryInfo>(
selectionId: String,
recyclerView: RecyclerView,
private val getItems: () -> List<T>,
getItemKey: RecyclerView.ViewHolder.() -> Long,
builder: SelectionTracker.Builder<Long>.() -> Unit = {},
) {
private val tracker: SelectionTracker<Long>
val selectionSize get() = tracker.selection.size()
var isInCustomChoice = false
private set
init {
tracker = SelectionTracker.Builder(
selectionId,
recyclerView,
object : ItemKeyProvider<Long>(SCOPE_MAPPED) {
override fun getKey(position: Int) = getItems()[position].gid
override fun getPosition(key: Long) = getItems().indexOfFirst { it.gid == key }
},
object : ItemDetailsLookup<Long>() {
override fun getItemDetails(e: MotionEvent) =
recyclerView.findChildViewUnder(e.x, e.y)?.let {
recyclerView.getChildViewHolder(it).run {
object : ItemDetails<Long>() {
override fun getPosition() = bindingAdapterPosition
override fun getSelectionKey() = getItemKey()
}
}
}
},
StorageStrategy.createLongStorage(),
).apply(builder).build()
}
fun addCustomChoiceListener(onIntoCustomChoiceListener: () -> Unit, onOutOfCustomChoiceListener: () -> Unit) {
tracker.addObserver(object : SelectionTracker.SelectionObserver<Long>() {
override fun onSelectionChanged() {
if (tracker.selection.isEmpty) {
if (isInCustomChoice) {
isInCustomChoice = false
onOutOfCustomChoiceListener()
}
} else if (!isInCustomChoice) {
isInCustomChoice = true
onIntoCustomChoiceListener()
}
}
override fun onSelectionRestored() {
if (!tracker.selection.isEmpty) {
isInCustomChoice = true
onIntoCustomChoiceListener()
}
}
})
}
fun isSelected(key: Long) = tracker.isSelected(key)
fun selectAll() = tracker.setItemsSelected(getItems().map(GalleryInfo::gid), true)
fun clearSelection() = tracker.clearSelection()
fun saveSelection(state: Bundle) = tracker.onSaveInstanceState(state)
fun restoreSelection(state: Bundle?) = tracker.onRestoreInstanceState(state)
fun getAndClearSelection() = getItems().run { tracker.selection.map { gid -> first { it.gid == gid } } }
.also { clearSelection() }
}
@@ -1,188 +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.scene
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ClearAll
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.DismissValue
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.core.view.GravityCompat
import androidx.lifecycle.lifecycleScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings.listThumbSize
import com.hippo.ehviewer.ui.doGalleryInfoAction
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.Deferred
import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn
import com.hippo.ehviewer.ui.tools.rememberDialogState
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.pxToDp
import kotlinx.coroutines.delay
class HistoryScene : BaseScene() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = ComposeView(inflater.context).apply {
setMD3Content {
val dialogState = rememberDialogState()
dialogState.Intercept()
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val historyData = remember { Pager(PagingConfig(20, jumpThreshold = 40)) { EhDB.historyLazyList }.flow.cachedIn(lifecycleScope) }.collectAsLazyPagingItems()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(
title = { Text(text = stringResource(id = R.string.history)) },
navigationIcon = {
IconButton(onClick = { toggleDrawer(GravityCompat.START) }) {
Icon(imageVector = Icons.Default.Menu, contentDescription = null)
}
},
actions = {
IconButton(onClick = {
coroutineScope.launchIO {
dialogState.awaitPermissionOrCancel(
confirmText = R.string.clear_all,
dismissText = android.R.string.cancel,
text = { Text(text = stringResource(id = R.string.clear_all_history)) },
)
EhDB.clearHistoryInfo()
}
}) {
Icon(imageVector = Icons.Default.ClearAll, contentDescription = null)
}
},
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
val cardHeight = remember { (3 * listThumbSize * 3).pxToDp.dp }
FastScrollLazyColumn(
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.gallery_list_margin_h)),
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)),
) {
items(
count = historyData.itemCount,
key = historyData.itemKey(key = { item -> item.gid }),
contentType = historyData.itemContentType(),
) { index ->
val info = historyData[index]
if (info != null) {
val dismissState = rememberDismissState(
confirmValueChange = {
if (it == DismissValue.DismissedToStart) {
coroutineScope.launchIO {
EhDB.deleteHistoryInfo(info)
}
}
true
},
)
SwipeToDismiss(
state = dismissState,
background = {},
dismissContent = {
// TODO: item delete & add animation
// Bug tracker: https://issuetracker.google.com/issues/150812265
GalleryInfoListItem(
onClick = {
navAnimated(
R.id.galleryDetailScene,
bundleOf(
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GALLERY_INFO,
GalleryDetailScene.KEY_GALLERY_INFO to info,
),
)
},
onLongClick = {
coroutineScope.launchIO {
dialogState.doGalleryInfoAction(info, context)
}
},
info = info,
modifier = Modifier.height(cardHeight),
)
},
directions = setOf(DismissDirection.EndToStart),
)
} else {
CrystalCard(modifier = Modifier.height(cardHeight).fillMaxWidth()) {}
}
}
}
Deferred({ delay(200) }) {
if (historyData.itemCount == 0) {
Column(
modifier = Modifier.padding(paddingValues).fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.big_history),
contentDescription = null,
Modifier.padding(16.dp),
)
Text(
text = stringResource(id = R.string.no_history),
style = MaterialTheme.typography.headlineMedium,
)
}
}
}
}
}
}
}
@@ -1,70 +0,0 @@
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.annotation.MainThread
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.fragment.findNavController
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
import com.hippo.ehviewer.client.parser.GalleryListUrlParser
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
@MainThread
@SuppressLint("PrivateResource")
fun NavController.navAnimated(id: Int, args: Bundle?, singleTop: Boolean = false) {
val options: NavOptions = NavOptions.Builder().setLaunchSingleTop(singleTop)
.setEnterAnim(androidx.fragment.R.animator.fragment_open_enter)
.setExitAnim(androidx.fragment.R.animator.fragment_open_exit)
.setPopEnterAnim(androidx.fragment.R.animator.fragment_close_enter)
.setPopExitAnim(androidx.fragment.R.animator.fragment_close_exit)
.build()
navigate(id, args, options)
}
@MainThread
fun NavController.navWithUrl(url: String): Boolean {
if (url.isEmpty()) return false
GalleryListUrlParser.parse(url)?.let {
navAnimated(
R.id.galleryListScene,
bundleOf(
GalleryListScene.KEY_ACTION to GalleryListScene.ACTION_LIST_URL_BUILDER,
GalleryListScene.KEY_LIST_URL_BUILDER to it,
),
)
return true
}
GalleryDetailUrlParser.parse(url)?.apply {
navAnimated(
R.id.galleryDetailScene,
bundleOf(
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GID_TOKEN,
GalleryDetailScene.KEY_GID to gid,
GalleryDetailScene.KEY_TOKEN to token,
),
)
return true
}
GalleryPageUrlParser.parse(url)?.apply {
navAnimated(
R.id.progressScene,
bundleOf(
ProgressScene.KEY_ACTION to ProgressScene.ACTION_GALLERY_TOKEN,
ProgressScene.KEY_GID to gid,
ProgressScene.KEY_PTOKEN to pToken,
ProgressScene.KEY_PAGE to page,
),
)
return true
}
return false
}
@MainThread
fun Fragment.navAnimated(id: Int, args: Bundle?, singleTop: Boolean = false) = findNavController().navAnimated(id, args, singleTop)
@@ -1,107 +0,0 @@
package com.hippo.ehviewer.ui.scene
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.navigation.fragment.findNavController
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.withUIContext
import moe.tarsin.coroutines.runSuspendCatching
class ProgressScene : BaseScene() {
override val showLeftDrawer = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(inflater.context).apply {
setMD3Content {
val action = rememberSaveable { requireArguments().getString(KEY_ACTION, INVALID) }
val gid = rememberSaveable { requireArguments().getLong(KEY_GID, -1) }
val token = rememberSaveable { requireArguments().getString(KEY_PTOKEN, INVALID) }
val page = rememberSaveable { requireArguments().getInt(KEY_PAGE, -1) }
val wrong = stringResource(id = R.string.error_something_wrong_happened)
var error by rememberSaveable { mutableStateOf("") }
LaunchedEffect(error) {
if (error.isEmpty()) {
if (action != ACTION_GALLERY_TOKEN || gid == -1L || token == INVALID || page == -1) {
error = wrong
} else {
runSuspendCatching {
EhEngine.getGalleryToken(gid, token, page)
}.onSuccess {
withUIContext {
findNavController().popBackStack()
navAnimated(
R.id.galleryDetailScene,
bundleOf(
GalleryDetailScene.KEY_ACTION to GalleryDetailScene.ACTION_GID_TOKEN,
GalleryDetailScene.KEY_GID to gid,
GalleryDetailScene.KEY_TOKEN to it,
GalleryDetailScene.KEY_PAGE to page,
),
)
}
}.onFailure {
error = ExceptionUtils.getReadableString(it)
}
}
}
}
Box(contentAlignment = Alignment.Center) {
if (error.isNotBlank()) {
Column(
modifier = Modifier.clickable { error = "" },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.big_sad_pandroid),
contentDescription = null,
modifier = Modifier.padding(16.dp),
)
Text(
text = wrong,
style = MaterialTheme.typography.headlineMedium,
)
}
} else {
CircularProgressIndicator()
}
}
}
}
}
companion object {
const val KEY_ACTION = "action"
const val ACTION_GALLERY_TOKEN = "gallery_token"
const val KEY_GID = "gid"
const val KEY_PTOKEN = "ptoken"
const val KEY_PAGE = "page"
const val INVALID = "invalid"
}
}
@@ -1,427 +0,0 @@
package com.hippo.ehviewer.ui.scene
import android.animation.Animator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.annotation.CallSuper
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MenuBook
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.core.view.GravityCompat
import androidx.core.widget.addTextChangedListener
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import com.google.android.material.search.SearchView
import com.google.android.material.shape.MaterialShapeDrawable
import com.hippo.ehviewer.EhApplication.Companion.searchDatabase
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.dao.Search
import com.hippo.ehviewer.dao.SearchDao
import com.hippo.ehviewer.databinding.SceneSearchbarBinding
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.FabLayout
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.util.AnimationUtils
import com.hippo.ehviewer.util.SimpleAnimatorListener
import com.jamal.composeprefs3.ui.ifNotNullThen
import com.jamal.composeprefs3.ui.ifTrueThen
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
abstract class SearchBarScene : BaseScene(), ToolBarScene {
private var _binding: SceneSearchbarBinding? = null
private val binding get() = _binding!!
private var mSuggestionList by mutableStateOf(emptyList<Suggestion>())
private var mSuggestionProvider: SuggestionProvider? = null
var allowEmptySearch = true
private val mSearchDatabase = searchDatabase.searchDao()
private var onApplySearch: (String) -> Unit = {}
protected val mSearchFab get() = binding.searchFab
protected abstract val fabLayout: FabLayout
protected var mShowActionFab = true
protected val mActionFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
fabLayout.primaryFab?.visibility = View.INVISIBLE
}
}
private val mSearchFabAnimatorListener = object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mSearchFab.visibility = View.INVISIBLE
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = SceneSearchbarBinding.inflate(inflater, container, false)
binding.appbar.statusBarForeground = MaterialShapeDrawable.createWithElevationOverlay(context)
binding.searchview.editText.addTextChangedListener { updateSuggestions() }
binding.searchview.editText.setOnEditorActionListener { _, _, _ ->
onApplySearch()
true
}
binding.searchBarList.setMD3Content {
LazyColumn(contentPadding = WindowInsets.navigationBars.asPaddingValues()) {
items(mSuggestionList) {
ListItem(
headlineContent = { Text(text = it.keyword) },
supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) },
leadingContent = it.canOpenDirectly.ifTrueThen {
Icon(
imageVector = Icons.Default.MenuBook,
contentDescription = null,
)
},
trailingContent = it.canDelete.ifTrueThen {
IconButton(onClick = { deleteKeyword(it.keyword) }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
)
}
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
modifier = Modifier.clickable { it.onClick() },
)
}
}
}
binding.searchview.addTransitionListener { _, _, newState ->
if (newState == SearchView.TransitionState.SHOWING) {
onSearchViewExpanded()
} else if (newState == SearchView.TransitionState.HIDING) {
onSearchViewHidden()
}
}
mSearchFab.setOnClickListener { onApplySearch() }
onCreateViewWithToolbar(inflater, binding.root, savedInstanceState)
// This has to be placed after onCreateViewWithToolbar() since
// callbacks are invoked in the reverse order in which they are added
binding.searchview.addTransitionListener(mSearchViewOnBackPressedCallback)
requireActivity().onBackPressedDispatcher.addCallback(mSearchViewOnBackPressedCallback)
binding.appbar.bringToFront()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.apply {
val menuResId = getMenuResId()
if (menuResId != 0) {
inflateMenu(menuResId)
setOnMenuItemClickListener { item: MenuItem -> onMenuItemClick(item) }
}
setNavigationOnClickListener { onNavigationClick() }
}
}
override fun onDestroyView() {
super.onDestroyView()
mSearchViewOnBackPressedCallback.remove()
_binding = null
}
private var privLockModeStart: Int? = null
@CallSuper
open fun onSearchViewExpanded() {
privLockModeStart = getDrawerLockMode(GravityCompat.START)
privLockModeStart?.let {
setDrawerLockMode(
DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
GravityCompat.START,
)
}
updateSuggestions()
}
@CallSuper
open fun onSearchViewHidden() {
binding.toolbar.setText(binding.searchview.text)
privLockModeStart?.let { setDrawerLockMode(it, GravityCompat.START) }
privLockModeStart = null
}
fun showSearchFab(animation: Boolean, delay: Long = 0) {
if (animation) {
mSearchFab.visibility = View.VISIBLE
mSearchFab.rotation = -45.0f
mSearchFab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
mSearchFab.visibility = View.VISIBLE
mSearchFab.scaleX = 1.0f
mSearchFab.scaleY = 1.0f
}
}
fun hideSearchFab(animation: Boolean): Long {
return if (View.INVISIBLE == mSearchFab.visibility) {
0L
} else {
if (animation) {
mSearchFab.animate().scaleX(0.0f).scaleY(0.0f)
.setListener(mSearchFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
} else {
mSearchFab.visibility = View.INVISIBLE
mSearchFab.scaleX = 0.0f
mSearchFab.scaleY = 0.0f
}
ANIMATE_TIME
}
}
fun selectSearchFab(animation: Boolean) {
_binding ?: return
mShowActionFab = false
val delay = if (animation) {
val fab = fabLayout.primaryFab!!
if (View.INVISIBLE == fab.visibility) {
0L
} else {
fabLayout.setExpanded(expanded = false, animation = true)
fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)
.setDuration(ANIMATE_TIME).setStartDelay(0L)
.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()
ANIMATE_TIME
}
} else {
fabLayout.setExpanded(expanded = false, animation = false)
val fab = fabLayout.primaryFab!!
fab.visibility = View.INVISIBLE
fab.scaleX = 0.0f
fab.scaleY = 0.0f
0L
}
showSearchFab(animation, delay)
}
fun selectActionFab(animation: Boolean) {
_binding ?: return
mShowActionFab = true
val delay = hideSearchFab(animation)
if (animation) {
val fab = fabLayout.primaryFab!!
fab.visibility = View.VISIBLE
fab.rotation = -45.0f
fab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)
.setDuration(ANIMATE_TIME).setStartDelay(delay)
.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()
} else {
fabLayout.setExpanded(expanded = false, animation = false)
val fab = fabLayout.primaryFab!!
fab.visibility = View.VISIBLE
fab.scaleX = 1.0f
fab.scaleY = 1.0f
}
}
fun setSearchBarHint(hint: String?) {
binding.toolbar.hint = hint
}
fun setSearchBarText(text: String?) {
binding.toolbar.setText(text)
binding.searchview.setText(text)
}
fun setEditTextHint(hint: String?) {
binding.searchview.editText.hint = hint
}
override fun onNavigationClick() {
toggleDrawer(GravityCompat.START)
}
override fun getMenuResId(): Int {
return 0
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return if (item.itemId == R.id.action_open_side_sheet) {
openSideSheet()
true
} else {
false
}
}
override fun setLiftOnScrollTargetView(view: View?) {
binding.appbar.setLiftOnScrollTargetView(view)
}
fun setOnApplySearch(lambda: (String) -> Unit) {
onApplySearch = lambda
}
private suspend fun addQuery(query: String) {
mSearchDatabase.deleteQuery(query)
if (query.isBlank()) return
val search = Search(System.currentTimeMillis(), query)
mSearchDatabase.insert(search)
}
fun onApplySearch() {
binding.toolbar.setText(binding.searchview.text)
binding.searchview.hide()
val query = binding.toolbar.text.toString().trim()
if (!allowEmptySearch && query.isEmpty()) return
lifecycleScope.launchIO { addQuery(query) }
onApplySearch(query)
}
fun interface SuggestionProvider {
fun providerSuggestions(text: String): Suggestion?
}
private fun deleteKeyword(keyword: String) {
BaseDialogBuilder(requireContext())
.setMessage(requireContext().getString(R.string.delete_search_history, keyword))
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.delete) { _, _ ->
lifecycleScope.launchIO {
mSearchDatabase.deleteQuery(keyword)
updateSuggestions()
}
}
.show()
}
abstract class Suggestion {
abstract val keyword: String
open val hint: String? = null
abstract fun onClick()
open val canDelete: Boolean = false
open val canOpenDirectly: Boolean = false
}
inner class TagSuggestion(
override val hint: String?,
override val keyword: String,
) : Suggestion() {
override fun onClick() {
binding.searchview.editText.let {
var keywords = it.text.toString().substringBeforeLast(' ', "")
if (keywords.isNotEmpty()) keywords += ' '
keywords += wrapTagKeyword(keyword)
if (!keywords.endsWith(':')) keywords += ' '
it.setText(keywords)
it.setSelection(keywords.length)
}
}
}
inner class KeywordSuggestion(
override val keyword: String,
) : Suggestion() {
override val canDelete = true
override fun onClick() {
binding.searchview.editText.run {
setText(keyword)
setSelection(length())
}
}
}
private fun updateSuggestions() {
_binding ?: return
viewLifecycleOwner.lifecycleScope.launchIO {
mSuggestionList = mergedSuggestionFlow().toList()
}
}
private suspend fun SearchDao.suggestions(prefix: String, limit: Int) = (if (prefix.isBlank()) list(limit) else rawSuggestions(prefix, limit)).map { it.query }
private fun mergedSuggestionFlow(): Flow<Suggestion> = flow {
binding.searchview.editText.text?.toString()?.let { text ->
mSuggestionProvider?.run { providerSuggestions(text)?.let { emit(it) } }
mSearchDatabase.suggestions(text, 128).forEach { emit(KeywordSuggestion(it)) }
EhTagDatabase.takeIf { it.initialized }?.run {
if (text.isNotEmpty() && !text.endsWith(' ')) {
val keyword = text.substringAfterLast(' ')
val translate = Settings.showTagTranslations && isTranslatable(requireContext())
suggestFlow(keyword, translate, true).collect {
emit(TagSuggestion(it.first, it.second))
}
suggestFlow(keyword, translate).collect {
emit(TagSuggestion(it.first, it.second))
}
}
}
}
}
fun setSuggestionProvider(suggestionProvider: SuggestionProvider) {
mSuggestionProvider = suggestionProvider
}
fun showSearchBar() {
_binding ?: return
binding.appbar.setExpanded(true)
}
private val mSearchViewOnBackPressedCallback =
object : OnBackPressedCallback(false), SearchView.TransitionListener {
override fun handleOnBackPressed() {
binding.searchview.hide()
}
override fun onStateChanged(
searchView: SearchView,
previousState: SearchView.TransitionState,
newState: SearchView.TransitionState,
) {
if (newState == SearchView.TransitionState.SHOWING) {
isEnabled = true
} else if (newState == SearchView.TransitionState.HIDING) {
isEnabled = false
}
}
}
}
fun wrapTagKeyword(keyword: String, translate: Boolean = false): String {
return if (keyword.endsWith(':')) {
keyword
} else {
val tag = keyword.substringAfter(':')
val prefix = keyword.dropLast(tag.length + 1)
if (translate) {
val namespacePrefix = EhTagDatabase.namespaceToPrefix(prefix)
val newPrefix = EhTagDatabase.getTranslation(tag = prefix) ?: prefix
val newTag = EhTagDatabase.getTranslation(namespacePrefix, tag) ?: tag
"$newPrefix:$newTag"
} else if (keyword.contains(' ')) {
"$prefix:\"$tag$\""
} else {
"$keyword$"
}
}
}
const val ANIMATE_TIME = 300L
@@ -1,23 +0,0 @@
package com.hippo.ehviewer.ui.scene
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
interface ToolBarScene {
fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View
fun onNavigationClick()
fun getMenuResId(): Int
fun onMenuItemClick(item: MenuItem): Boolean
fun setLiftOnScrollTargetView(view: View?)
}
@@ -1,58 +0,0 @@
package com.hippo.ehviewer.ui.tools
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.Dp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.roundToInt
import kotlin.math.sin
private const val FullCircle: Float = (PI * 2).toFloat()
private const val QuarterCircle = PI / 2
@Composable
fun CircularLayout(
modifier: Modifier = Modifier,
radius: Dp,
placeFirstItemInCenter: Boolean = false,
content: @Composable () -> Unit,
) {
Layout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val radiusPx = radius.toPx()
val itemConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
val outPlaceables = if (placeFirstItemInCenter) placeables.drop(1) else placeables
val theta = FullCircle / (outPlaceables.count())
layout(
width = constraints.minWidth,
height = constraints.minHeight,
) {
if (placeFirstItemInCenter) {
placeables[0].let {
val centerOffsetX = constraints.maxWidth / 2 - it.width / 2
val centerOffsetY = constraints.maxHeight / 2 - it.height / 2
it.place(
x = centerOffsetX,
y = centerOffsetY,
)
}
}
outPlaceables.forEachIndexed { i, it ->
val centerOffsetX = constraints.maxWidth / 2 - it.width / 2
val centerOffsetY = constraints.maxHeight / 2 - it.height / 2
val offsetX = radiusPx * cos(theta * i - QuarterCircle) + centerOffsetX
val offsetY = radiusPx * sin(theta * i - QuarterCircle) + centerOffsetY
it.place(
x = offsetX.roundToInt(),
y = offsetY.roundToInt(),
)
}
}
}
}
@@ -1,8 +0,0 @@
package com.hippo.ehviewer.ui.tools
object CropDefaults {
private const val CROP_MIN_ASPECT = 0.5F
private const val CROP_MAX_ASPECT = 0.8f
private val CROP_GOOD_RANGE = CROP_MIN_ASPECT..CROP_MAX_ASPECT
fun shouldCrop(width: Int, height: Int) = width.toFloat() / height in CROP_GOOD_RANGE
}
@@ -1,312 +0,0 @@
package com.hippo.ehviewer.ui.tools
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.jamal.composeprefs3.ui.ifNotNullThen
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
interface DialogScope<R> {
var expectedValue: R
}
fun interface DismissDialogScope<R> {
fun dismissWith(value: R)
}
class DialogState {
var content: (@Composable () -> Unit)? by mutableStateOf(null)
@Composable
fun Intercept() = content?.invoke()
fun dismiss() {
content = null
}
suspend inline fun <R> dialog(crossinline block: @Composable (CancellableContinuation<R>) -> Unit) = suspendCancellableCoroutine { cont ->
cont.invokeOnCancellation { dismiss() }
val realContinuation = object : CancellableContinuation<R> by cont {
override fun resumeWith(result: Result<R>) {
dismiss()
cont.resumeWith(result)
}
}
content = { block(realContinuation) }
}
suspend fun <R> awaitResult(initial: R, @StringRes title: Int? = null, block: @Composable DialogScope<R>.() -> Unit): R {
return dialog { cont ->
val state = remember(cont) { mutableStateOf(initial) }
val impl = remember(cont) { object : DialogScope<R> { override var expectedValue by state } }
AlertDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = { cont.resume(state.value) }) {
Text(text = stringResource(id = android.R.string.ok))
}
},
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
text = { block(impl) },
)
}
}
suspend fun awaitInputText(
initial: String = "",
title: String? = null,
hint: String? = null,
isNumber: Boolean = false,
invalidator: ((String) -> String?)? = null,
): String {
return dialog { cont ->
var state by remember(cont) { mutableStateOf(initial) }
var error by remember(cont) { mutableStateOf<String?>(null) }
AlertDialog(
onDismissRequest = { cont.cancel() },
confirmButton = {
TextButton(onClick = {
error = invalidator?.invoke(state)
error ?: cont.resume(state)
}) {
Text(text = stringResource(id = android.R.string.ok))
}
},
title = title.ifNotNullThen { Text(text = title!!) },
text = {
OutlinedTextField(
value = state,
onValueChange = { state = it },
label = hint.ifNotNullThen {
Text(text = hint!!)
},
trailingIcon = error.ifNotNullThen {
Icon(
imageVector = Icons.Filled.Info,
contentDescription = null,
)
},
supportingText = error.ifNotNullThen {
Text(text = error!!)
},
isError = error != null,
keyboardOptions = if (isNumber) KeyboardOptions(keyboardType = KeyboardType.Number) else KeyboardOptions.Default,
)
},
)
}
}
suspend fun awaitPermissionOrCancel(
@StringRes confirmText: Int? = null,
@StringRes dismissText: Int? = null,
@StringRes title: Int? = null,
onDismiss: () -> Unit = {},
text: (@Composable () -> Unit)? = null,
) {
return dialog { cont ->
AlertDialog(
onDismissRequest = {
onDismiss()
cont.cancel()
},
confirmButton = {
TextButton(onClick = { cont.resume(Unit) }) {
Text(text = stringResource(id = confirmText ?: android.R.string.ok))
}
},
dismissButton = dismissText.ifNotNullThen {
TextButton(onClick = {
onDismiss()
cont.cancel()
}) {
Text(text = stringResource(id = dismissText!!))
}
},
title = title.ifNotNullThen { Text(text = stringResource(id = title!!)) },
text = text,
)
}
}
private suspend fun <R> showNoButton(respectDefaultWidth: Boolean = true, block: @Composable DismissDialogScope<R>.() -> Unit): R {
return dialog { cont ->
val impl = remember(cont) {
DismissDialogScope<R> {
cont.resume(it)
}
}
AlertDialog(
onDismissRequest = { cont.cancel() },
content = {
Surface(
modifier = with(Modifier) { if (!respectDefaultWidth) defaultMinSize(280.dp) else width(280.dp) },
shape = AlertDialogDefaults.shape,
color = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
content = { block(impl) },
)
},
)
}
}
suspend fun showSelectItem(
vararg items: String,
@StringRes title: Int,
): Int = showNoButton {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(id = title),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
style = MaterialTheme.typography.headlineSmall,
)
LazyColumn {
itemsIndexed(items) { index, text ->
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.tertiary) {
Text(
text = text,
modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp),
style = MaterialTheme.typography.titleMedium,
)
}
}
}
}
}
suspend fun showSelectItemWithIcon(
vararg items: Pair<ImageVector, Int>,
title: String,
): Int = showNoButton {
LazyColumn {
stickyHeader {
Text(text = title, modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium)
}
itemsIndexed(items) { index, (icon, text) ->
ListItem(
headlineContent = {
Text(text = stringResource(id = text), style = MaterialTheme.typography.titleMedium)
},
modifier = Modifier.clickable { dismissWith(index) },
leadingContent = {
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
},
)
}
}
}
suspend fun showSelectItemWithIconAndTextField(
vararg items: Pair<ImageVector, String>,
@StringRes title: Int,
@StringRes hint: Int,
maxChar: Int,
adjustTextPosition: Boolean = true,
): Pair<Int, String> = showNoButton(false) {
Column {
Text(text = stringResource(id = title), modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium)
CircularLayout(
radius = 112.dp,
modifier = Modifier.fillMaxWidth().aspectRatio(1F),
placeFirstItemInCenter = true,
) {
var note by remember { mutableStateOf("") }
TextField(
value = note,
onValueChange = { note = it },
modifier = Modifier.width(128.dp),
textStyle = TextStyle(fontFamily = FontFamily.Monospace),
label = { Text(text = stringResource(id = hint)) },
trailingIcon = {
if (note.isNotEmpty()) {
IconButton(onClick = { note = "" }) {
Icon(imageVector = Icons.Default.Close, contentDescription = null)
}
}
},
supportingText = {
Text(
text = "${note.toByteArray().size} / $maxChar",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End,
)
},
maxLines = 6,
shape = ShapeDefaults.ExtraSmall,
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
)
items.forEachIndexed { index, (icon, text) ->
Column(
modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note) },
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (index == 0 && adjustTextPosition) {
Text(text = text, style = MaterialTheme.typography.bodySmall)
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
} else {
Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor)
Text(text = text, style = MaterialTheme.typography.bodySmall)
}
}
}
}
}
}
}
private val IconWithTextCorner = RoundedCornerShape(8.dp)
@Composable
fun rememberDialogState(): DialogState {
return remember { DialogState() }
}
@@ -1,20 +0,0 @@
package com.hippo.ehviewer.ui.tools
import android.text.Spanned
import android.text.style.StrikethroughSpan
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
val spanned = this@toAnnotatedString
append(spanned.toString())
getSpans(0, spanned.length, Any::class.java).forEach { span ->
val start = getSpanStart(span)
val end = getSpanEnd(span)
when (span) {
is StrikethroughSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough), start, end)
}
}
}
Loaded 100 of 580 files, more files were not shown because too many files have changed in this diff. Show more