Compare commits

..
945 Commits
Author SHA1 Message Date
tarsin afbe0a4de5 Release 1.8.8.3
Releases / Build (push) Has been cancelled
2023-05-26 15:46:41 +08:00
tarsin 0a5cdf7c80 Fixup aa1e2102fc 2023-05-26 15:46:11 +08:00
tarsin 9d689923ac Fixup unsafe Composable behaviour 2023-05-26 15:45:58 +08:00
tarsin 98c5ae7490 Workaround aspect is NaN, Resolve #1116 2023-05-26 15:45:48 +08:00
tarsin 868ac22c69 Fix wrong thumb crop behaviour 2023-05-26 15:45:38 +08:00
tarsin 636ac64692 Release 1.8.8.2
Releases / Build (push) Has been cancelled
2023-05-26 14:35:53 +08:00
tarsin a9eec6a783 Fixup! Resolve #1113 2023-05-26 14:35:30 +08:00
tarsin 00867c7dc1 Release 1.8.8.1 bugfix version
Releases / Build (push) Has been cancelled
2023-05-26 13:46:38 +08:00
tarsin cf79cdc7c1 Resolve #1113 2023-05-26 13:45:49 +08:00
tarsin 25d4d5e393 Bump to 1.8.8.0
Releases / Build (push) Has been cancelled
2023-05-25 15:33:07 +08:00
tarsin 1743caa869 Remove unused padding 2023-05-25 15:13:27 +08:00
tarsin 958fdb19b4 Revert "Decode cf protected email, step 1"
This reverts commit d44981a70c.
2023-05-25 15:02:14 +08:00
tarsin 4b63648a79 Revert "Decode cf protected email, step 2, Resolve #1102"
This reverts commit 74c6bfa479.
2023-05-25 15:02:00 +08:00
tarsin 498b84df37 Revert "Optimize code"
This reverts commit 6ed3fb29d9.
2023-05-25 15:01:49 +08:00
tarsin 7a4e65259d Revert "Uniform html parse behaviour"
This reverts commit c5c1170383.
2023-05-25 15:01:38 +08:00
tarsin 8cb8dcbe0f Resolve #1067 2023-05-25 14:16:13 +08:00
tarsin da37b3cdff Upgrade compose bom 2023-05-25 13:44:10 +08:00
renovate[bot] 0a1768e481 Update dependency androidx.core:core-ktx to v1.11.0-beta01 2023-05-25 13:41:57 +08:00
renovate[bot] 818d16a32d Update dependency androidx.paging:paging-common-ktx to v3.2.0-alpha06 2023-05-25 13:41:03 +08:00
renovate[bot] e9a5d1cc7c Update dependency androidx.paging:paging-compose to v1.0.0-alpha20 2023-05-25 13:40:19 +08:00
renovate[bot] dac56f3e32 Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.0-alpha10 2023-05-25 03:55:42 +00:00
renovate[bot] d6cbcdc6cd Update dependency androidx.activity:activity-compose to v1.7.2 2023-05-25 01:29:39 +00:00
tarsin c5c1170383 Uniform html parse behaviour 2023-05-24 20:12:24 +08:00
tarsin 6ed3fb29d9 Optimize code 2023-05-24 19:28:55 +08:00
tarsin 74c6bfa479 Decode cf protected email, step 2, Resolve #1102 2023-05-24 19:19:47 +08:00
tarsin d44981a70c Decode cf protected email, step 1 2023-05-24 18:51:22 +08:00
tarsin 06e411d332 Fixup! Rewrite LauncherUtils 2023-05-23 13:44:32 +08:00
tarsin f419189749 Rewrite LauncherUtils 2023-05-23 13:11:57 +08:00
tarsin aa1e2102fc Fix deprecated coil API 2023-05-22 21:53:59 +08:00
tarsin 306ce9ed21 Always use ImageDecoderDecoder for better performance 2023-05-22 12:33:12 +08:00
renovate[bot] 544fcb2e88 Update dependency io.coil-kt:coil-compose to v2.4.0 2023-05-21 21:44:29 +00:00
tarsin cb01df4071 Revert "Update dependency dev.chrisbanes.compose:compose-bom to v2023.04.00-SNAPSHOT"
This reverts commit 38310b7c5a.
2023-05-21 10:42:08 +08:00
renovate[bot] 38310b7c5a Update dependency dev.chrisbanes.compose:compose-bom to v2023.04.00-SNAPSHOT 2023-05-20 19:21:10 +00:00
tarsin fcd56ff4e9 Use compose-bom 2023.04.00-alpha04 for reproducible artifact 2023-05-21 00:16:58 +08:00
tarsin 41300a9fd4 Fix okhttp dependency 2023-05-21 00:12:28 +08:00
tarsin 0bd5f9dc31 Downgrade ArrowKt to stable release for reproducible artifacts 2023-05-21 00:11:10 +08:00
tarsin 004ecd0dd3 Resolve #1095 2023-05-21 00:06:56 +08:00
tarsin fc24a18ac5 Bump to 1.8.8.0-beta
Releases / Build (push) Has been cancelled
2023-05-19 19:19:43 +08:00
tarsin a9a8c68b94 Fix code style, fuck Android Studio 2023-05-19 18:55:18 +08:00
tarsin 193d2f8d9e Fix rememberMemorized implementation 2023-05-19 18:50:17 +08:00
tarsin 538da9b98f Cleanup rememberSaveable usage 2023-05-19 17:25:29 +08:00
tarsin 071b597552 Introduce rememberMemorized 2023-05-19 17:06:32 +08:00
tarsin 8926d74378 Introduce rememberGlobally 2023-05-19 13:10:15 +08:00
tarsin 39a4b03bc3 Avoid TransactionTooLargeException 2023-05-19 12:16:07 +08:00
tarsin 9202bbc7dc Workaround #1092 2023-05-19 11:43:21 +08:00
tarsin f5601ebca1 Don't saveInstanceState when data too large, Workaround #1092 2023-05-19 10:50:18 +08:00
tarsin 2f978b554e Adjust kotlin compiler options 2023-05-18 13:55:06 +08:00
tarsin f48a90d941 Upgrade r8 2023-05-18 13:29:41 +08:00
tarsin 4f0d9fa83c Allow downsampling on LargePreviews 2023-05-18 11:52:25 +08:00
tarsin 57a747ed9c Fix cycled preview when PreviewPages == 1 2023-05-17 23:39:19 +08:00
tarsin 746a045328 Bump to 1.8.8.0-alpha
Releases / Build (push) Has been cancelled
2023-05-17 22:49:34 +08:00
tarsin 2a4c7823c2 Bind downloadHolder with coroutine 2023-05-17 22:42:55 +08:00
tarsin b995510df1 Resolve #1082, #1081 again 2023-05-17 22:21:17 +08:00
tarsin a2852d6df9 Resolve #1082, #1081 again 2023-05-17 19:08:07 +08:00
tarsin 47863580a0 Resolve #1082, #1081 again 2023-05-17 19:01:39 +08:00
tarsin 9af6532331 Resolve #1082, #1081 2023-05-17 16:28:50 +08:00
txyyh 36f411a031 Release 1.8.8.0-preview
Releases / Build (push) Has been cancelled
2023-05-17 06:28:17 +00:00
tarsin 00b6185188 Fix recover from decode error 2023-05-17 13:36:34 +08:00
tarsin a7db6c8613 Resolve #1077 2023-05-17 13:34:09 +08:00
tarsin 86097ace72 Revert "Dummy HistoryScene item remove animation"
Revert reason: This severely reduced jumping performance.
This reverts commit 467ab0b5a2.
2023-05-17 11:57:16 +08:00
tarsin b62dd1fcac Apply migration when import DB as well 2023-05-17 11:44:48 +08:00
tarsin 9a3b91c610 Cleanup thumbKey set
TODO: make thumbKey field nonnull!
2023-05-17 11:36:35 +08:00
tarsin 0f25789d74 Move EhAsyncThumb to dataflow Composable package 2023-05-17 11:28:25 +08:00
tarsin b50185639e Make EhAsyncThumb receive a GalleryInfo 2023-05-17 11:24:28 +08:00
tarsin 95478a615e Do db migration, Resolve #1074 2023-05-17 10:59:37 +08:00
renovate[bot] 91164e4dd6 chore(deps): update plugin com.android.application to v8.1.0-beta03 2023-05-16 22:24:55 +00:00
skymkmk ea42fa5613 fix: preview cannot be loaded in exhentai mode 2023-05-16 18:55:05 +08:00
tarsin c620478e38 Animate image search select 2023-05-15 16:38:08 +08:00
tarsin 467ab0b5a2 Dummy HistoryScene item remove animation 2023-05-15 16:27:06 +08:00
tarsin 1d14e3ceb4 Fix GalleryDetailHeaderCard thumb flicker 2023-05-15 12:58:53 +08:00
tarsin c4ff48f60f Avoid allocation?
I'm not sure kotlin compiler will optimize this
2023-05-14 18:24:43 +08:00
tarsin 196190314b Fix aggressive thumb preload
https://github.com/coil-kt/coil/commit/65ff90ea2b11db9da98cc7763d9c5f60e9ca364a
2023-05-14 18:17:08 +08:00
tarsin 4c7b4dbc51 Introduce CropDefaults 2023-05-14 14:55:51 +08:00
tarsin b820159736 No need to compareAndSet
set to the same value is a no-op
2023-05-14 14:48:07 +08:00
tarsin 41991b8f22 Refactor compose layout package 2023-05-14 14:44:07 +08:00
tarsin 6246d01247 Move CookiesDao to dao package 2023-05-14 14:29:36 +08:00
tarsin 3734d0c585 Refactor package structure 2023-05-14 14:20:50 +08:00
tarsin d70c5be15a Refactor package structure 2023-05-14 14:08:59 +08:00
tarsin 814230f4aa Remove "more_previews" header
As suggested, it is really ugly
2023-05-14 13:54:36 +08:00
tarsin 2b64491e76 Recover download db concurrently 2023-05-14 11:39:15 +08:00
tarsin f770ac9ac9 Implement #1042
Since this View-based reader will never be actively maintained, and we are to rewrite it with Compose,
Forgive me messing up the code...
2023-05-14 00:57:05 +08:00
tarsin a6a3219d1e Implement imageSearch 2023-05-13 20:51:26 +08:00
tarsin c1eddb5876 Initial compose ImageSearchLayout layout 2023-05-13 19:39:59 +08:00
tarsin 37dcad7d6f Always do activity launch in Dispatcher.Main 2023-05-13 18:22:30 +08:00
tarsin d06cd5cc2d Don't forget to drop continuation 2023-05-13 18:20:53 +08:00
tarsin c39e8eb43c Fuck off the silly Android launcher and callback :) 2023-05-13 18:14:06 +08:00
tarsin ae3caf84b5 Kick pickVisualMediaLauncher out of GalleryListScene scope 2023-05-13 17:41:49 +08:00
renovate[bot] 185d47fcdf Update dependency com.pinterest:ktlint to v0.49.1 2023-05-13 12:25:49 +08:00
renovate[bot] a63b2b3283 Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.7.1 2023-05-12 22:59:40 +00:00
tarsin 576ff1b007 Workaround #1065 2023-05-13 00:41:44 +08:00
tarsin 7a20d7ca49 Maybe should use default size 2023-05-12 19:59:55 +08:00
tarsin 346b614999 Implement CheckableComposeCardView 2023-05-12 19:58:48 +08:00
tarsin 6b0ac4bf0a Fix duplicated signature 2023-05-12 19:16:22 +08:00
tarsin 4e859eefb8 Introduce CheckableComposeView 2023-05-12 18:39:25 +08:00
tarsin 578aff7dea Move onLongClick card to dummy hint holder 2023-05-12 18:33:28 +08:00
tarsin f81d9de2c6 Allow jump in history pager 2023-05-12 17:50:07 +08:00
tarsin 255de89a43 Fixup sideEffect 2023-05-12 17:40:29 +08:00
tarsin c561c223d6 Sync galleryListCard margin 2023-05-12 17:33:06 +08:00
tarsin ba7d501a6e Restore GalleryPreviewScreen behaviour 2023-05-12 16:57:34 +08:00
tarsin 5b75afbdc8 Adjust grid padding 2023-05-12 16:54:39 +08:00
tarsin f7b9be3bc5 Revert "Adjust padding"
This reverts commit 743bd951c8.
2023-05-12 16:42:46 +08:00
tarsin 743bd951c8 Adjust padding 2023-05-12 16:13:58 +08:00
tarsin 3d2bbe1463 Cleanup GalleryDetailWidgets interface 2023-05-12 15:47:04 +08:00
tarsin 4b1adab0eb Cleanup GalleryDetailWidgets codestyle 2023-05-12 15:42:49 +08:00
tarsin e8ad2f2185 Conditionally crop thumb, Resolve #1047 2023-05-12 12:15:38 +08:00
tarsin 8986289702 Resolve #1023 2023-05-12 12:05:32 +08:00
renovate[bot] 69ef88909e Update dependency org.jetbrains.kotlinx:kotlinx-serialization-cbor to v1.5.1 2023-05-12 00:16:29 +08:00
tarsin 443cacca00 Fixup androidx core NPE 2023-05-11 16:55:03 +08:00
tarsin ae5286bebb Resolve #1062 2023-05-11 16:39:54 +08:00
tarsin c3facb9787 Introduce calculateSuitableSpanCount 2023-05-11 16:35:18 +08:00
tarsin 22d5832ba2 Fixup! Purification GalleryPreviewsScreen 2023-05-11 16:30:22 +08:00
tarsin 68d55f416e Purification GalleryPreviewsScreen 2023-05-11 16:29:50 +08:00
tarsin dfda4c0bc2 Suppress BaseScene usage 2023-05-11 16:20:50 +08:00
tarsin c7289d07a0 Drop unused code 2023-05-11 16:01:45 +08:00
tarsin df1ff07745 Cleanup Code 2023-05-11 15:59:41 +08:00
tarsin 8ef017fb94 Cleanup Code 2023-05-11 15:59:16 +08:00
tarsin 4e896927a9 Maybe use badge for category color and simpleLang 2023-05-11 15:53:57 +08:00
tarsin 3aa9fddfd5 Refine name and drop unused res 2023-05-11 15:43:02 +08:00
tarsin d3bb169cdc And restore pages display 2023-05-11 15:36:01 +08:00
tarsin d8e168ed61 And never show uploader in FavScene
That field is missing
2023-05-11 15:22:34 +08:00
tarsin b924cc5341 Restore ListMode fav icon behaviour 2023-05-11 15:17:22 +08:00
tarsin bb6ed820a7 Initial move GalleryHolder to compose
TODO: Grid categoryColor, simpleLang badge, and don't show favourite icon in FavouriteScene
2023-05-11 15:08:43 +08:00
tarsin 1bb0c1d537 Cleanup unused code 2023-05-11 13:59:31 +08:00
tarsin 6ec69ca989 Enlarge prefetchDistance 2023-05-11 11:28:25 +08:00
tarsin 65f19f0782 Mitigate androidx core NPE 2023-05-11 11:02:18 +08:00
tarsin cd48f0aed8 Code cleanup 2023-05-11 10:50:49 +08:00
tarsin b165e96a07 More efficient way to implement goto 2023-05-11 10:42:30 +08:00
renovate[bot] d15dd900cc Update dependency androidx.core:core-ktx to v1.10.1 2023-05-10 22:40:23 +00:00
tarsin ce2391b6f3 Cleanup code 2023-05-10 23:48:07 +08:00
tarsin c786ff0087 Restore jumpToCertainPage ability 2023-05-10 23:12:15 +08:00
tarsin 577286eb56 Restore moveToNextPage support 2023-05-10 21:57:35 +08:00
tarsin f91c69906f Refactor GalleryPreviewsScene with Paging Compose 2023-05-10 21:37:40 +08:00
tarsin a3bdb70ef1 [Don't use] Initial move GalleryPreviewsScene to compose 2023-05-10 18:29:52 +08:00
tarsin 5f9bdabf7e Initial copy of material PullRefresh implementation, prepare to rewrite ContentLayout with compose 2023-05-10 15:00:57 +08:00
renovate[bot] 86ecb9b931 Update plugin com.android.application to v8.1.0-beta02 2023-05-08 20:55:43 +00:00
tarsin 9da687f168 Fix dialog appears when url being consumed 2023-05-08 08:44:43 +08:00
tarsin 8efea01d78 Upgrade r8 2023-05-08 00:10:00 +08:00
tarsin f824aed3be Dsl motto motto 2023-05-07 23:37:16 +08:00
tarsin 8b01906f1e Dsl motto motto 2023-05-07 23:31:27 +08:00
tarsin 61aab22c02 Fixup! Drop Ktor 2023-05-07 23:16:29 +08:00
tarsin 16e1e20812 Fixup! Drop Ktor 2023-05-07 23:14:16 +08:00
tarsin 25931f9068 Drop Ktor 2023-05-07 23:00:54 +08:00
renovate[bot] 65d5377683 Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.7.0 2023-05-05 22:21:19 +00:00
renovate[bot] 383c37323a Update dependency com.google.android.material:material to v1.9.0 2023-05-05 07:45:40 +08:00
tarsin aa2073194d Always use setMD3Content 2023-05-04 22:12:12 +08:00
tarsin a57e91cd0a Revert "Workaround #1056"
This reverts commit 56de696330.
2023-05-04 21:08:14 +08:00
tarsin 56de696330 Workaround #1056 2023-05-04 21:05:05 +08:00
tarsin a6002712fd Optimize SignInScreen tablet layout 2023-05-04 19:33:30 +08:00
tarsin f752636c25 Cleanup CookieSignInScene layout 2023-05-04 19:02:28 +08:00
tarsin 6656399112 Cleanup code 2023-05-04 18:15:49 +08:00
tarsin 780bab2125 Convert SignInScreen to use a suspend dialog 2023-05-04 18:09:29 +08:00
tarsin 681be33fed Convert CookieSignInScene to use a suspend dialog 2023-05-04 18:05:05 +08:00
tarsin d30837665a Introduce coroutine bounded dialog 2023-05-04 17:38:17 +08:00
tarsin a6cefed13f Fix lag when popBackStack 2023-05-04 13:50:01 +08:00
tarsin 0299ca8b1f Add LazyColumnWithScrollBar 2023-05-04 13:12:53 +08:00
tarsin dd95878ba7 Replace deprecated method 2023-05-04 12:42:14 +08:00
tarsin f0ac27b159 Fix HistoryScene Tip bounce 2023-05-04 12:16:24 +08:00
tarsin f39ca55379 Drop unused res 2023-05-04 11:33:25 +08:00
tarsin 738c76ccd7 Reland Compose HistoryScene 2023-05-04 11:30:49 +08:00
tarsin 012ae19ca2 Cleanup unused res 2023-05-04 10:46:43 +08:00
renovate[bot] bfb3329e83 Update dependency androidx.paging:paging-runtime-ktx to v3.2.0-alpha05 2023-05-04 08:18:45 +08:00
renovate[bot] ff480291fe Update dependency composeOptions to v1.4.7 2023-05-04 08:18:33 +08:00
renovate[bot] 775441d223 Update dependency androidx.paging:paging-compose to v1.0.0-alpha19 2023-05-03 23:02:40 +00:00
tarsin 7c5f1da1fc Cleanup code 2023-05-03 19:31:17 +08:00
tarsin 71bd11a941 Cleanup code 2023-05-03 19:21:40 +08:00
tarsin ca62b9f4bf Cleanup code 2023-05-03 18:53:39 +08:00
tarsin 9272ff9fbf Resolve #1050 2023-05-03 18:46:42 +08:00
tarsin 366b106234 Fix redundant code 2023-05-03 18:18:03 +08:00
tarsin d333524559 Promote ArrowKt 2023-05-03 17:14:26 +08:00
tarsin 3f9c57ad2d Don't keep all activity reference 2023-05-01 12:17:12 +08:00
tarsin 64e55be02f Remove offsetY field in GalleryPreview 2023-05-01 12:00:09 +08:00
tarsin 99ed2b35d3 Store imageKey in GalleryPreview to mitigate Parcelable too large 2023-05-01 11:45:32 +08:00
tarsin f94a2350ab Spilt LargeGalleryPreview and NormalGalleryPreview 2023-05-01 11:16:54 +08:00
tarsin 7abeba8b87 Refine GalleryCommentList nullability and immutability 2023-05-01 11:04:19 +08:00
tarsin 7d16ad2119 Refine GalleryDetail nullability and immutability 2023-05-01 10:59:25 +08:00
tarsin 97e5636813 Fixup! Move pageUrl field out of GalleryPreview 2023-05-01 10:40:07 +08:00
tarsin 3d67e886d2 Move pageUrl field out of GalleryPreview 2023-05-01 10:37:45 +08:00
tarsin 919c8c7ee0 Upgrade r8 2023-04-30 21:56:56 +08:00
tarsin a47b2b234b Fixup! Initial compose GalleryInfoBottomSheet implementation 2023-04-30 16:46:43 +08:00
tarsin 2b95ebede8 Code cleanup 2023-04-30 16:41:41 +08:00
tarsin 87a4b6a91b Initial compose GalleryInfoBottomSheet implementation 2023-04-30 15:45:34 +08:00
tarsin bac07abf1f Kill global map 2023-04-30 12:56:14 +08:00
tarsin 3f2184766c Assert LocalPinnableContainer exists 2023-04-30 11:25:34 +08:00
tarsin 53d317cfa4 Pin GalleryDetailScene header 2023-04-30 11:23:18 +08:00
tarsin 8aa4c34794 Implement spinlock, don't use platform one 2023-04-29 19:26:47 +08:00
tarsin 797182fbf8 Fixup! Use spinlock for better performance 2023-04-29 17:18:30 +08:00
renovate[bot] dcbc79fd80 Update dependency org.jsoup:jsoup to v1.16.1 2023-04-29 17:12:52 +08:00
tarsin 711848f027 Use spinlock for better performance 2023-04-29 17:12:23 +08:00
tarsin 8eeacf49cb Cleanup code 2023-04-29 12:31:41 +08:00
tarsin bdf99d01a6 Optimize EhFilter comment regex memory allocation 2023-04-29 12:21:15 +08:00
tarsin 0ed68b55b3 Refactor EhFilter 2023-04-29 11:56:38 +08:00
tarsin 83ad66d27c Fix responseBody builder dsl name 2023-04-28 14:21:05 +08:00
tarsin 15d7bc5037 Upgrade Kotlin 1.8.21 2023-04-28 14:05:35 +08:00
tarsin e83d0b6c55 Reorganize EhEngine 2023-04-28 13:57:18 +08:00
tarsin 004d8a4bff Cleanup EhFilter 2023-04-28 13:25:08 +08:00
tarsin 969c9a66af Cleanup unused func 2023-04-28 10:13:36 +08:00
tarsin 8eec363581 Cleanup EhEngine 2023-04-28 10:10:45 +08:00
tarsin cec0578b77 Cleanup EhEngine 2023-04-28 10:04:43 +08:00
tarsin 2ce2f0ee9b Cleanup EhEngine 2023-04-28 09:51:08 +08:00
tarsin f2641adfd2 Cleanup EhEngine 2023-04-28 09:34:30 +08:00
tarsin af9125792c Fix imageSearch 2023-04-28 09:15:56 +08:00
tarsin a9c37ba8fb Cleanup EhEngine 2023-04-25 16:20:23 +08:00
tarsin 94ebb4573a Cleanup Okhttp API usage 2023-04-25 14:56:28 +08:00
tarsin 5f14919755 Cleanup Okhttp API usage 2023-04-25 14:29:23 +08:00
tarsin ed673b5bc1 Kill EhClient and EhRequest 2023-04-22 16:49:20 +08:00
tarsin 78d8ed922c Cleanup code 2023-04-22 16:15:02 +08:00
tarsin e0797cd519 Kill EhClient usage in GalleryDetailScene 2023-04-22 16:03:53 +08:00
tarsin eb5f39def7 Kill EhCallback 2023-04-22 15:52:32 +08:00
tarsin d200dd9cbe Kill FavouriteScene AddFavoritesListener 2023-04-22 15:43:00 +08:00
tarsin ec4446bb9b Kill GalleryCommentsScene CommentGalleryListener 2023-04-22 15:35:02 +08:00
tarsin c20bc215a8 Kill GalleryCommentsScene RefreshCommentListener 2023-04-22 15:29:25 +08:00
tarsin 13705f9872 Kill ProgressScene GetGalleryTokenListener 2023-04-22 15:20:40 +08:00
tarsin f02cd58a6b Kill GalleryListScene GetGalleryListListener 2023-04-22 15:15:09 +08:00
tarsin f36480d5d6 Kill GalleryPreviewsScene GetPreviewListListener 2023-04-22 15:03:32 +08:00
tarsin 821f9f8037 Fix GalleryDetailScene tag display 2023-04-22 14:50:22 +08:00
tarsin ba8e517e98 Cleanup codeStyle 2023-04-22 13:06:03 +08:00
tarsin abf7676ae7 Kill GalleryCommentsScene voteComment callback 2023-04-22 13:03:17 +08:00
tarsin 45cc6acd9f Kill GalleryDetailScene rateGallery callback 2023-04-22 12:52:41 +08:00
tarsin 32038531e1 Kill GalleryDetailScene downloadArchive callback 2023-04-22 12:31:49 +08:00
tarsin 7d74bdc3bd Kill GalleryDetailScene voteTag callback 2023-04-22 12:18:10 +08:00
tarsin b310e98c5a Cleanup code 2023-04-22 11:25:50 +08:00
tarsin ce0f160f31 Upgrade ktlint and do format 2023-04-22 10:51:24 +08:00
renovate[bot] b8c9040030 Update dependency gradle to v8.1.1 2023-04-22 01:32:00 +08:00
tarsin da21628222 Remove outdated workaround 2023-04-21 14:16:48 +08:00
tarsin 17a99abece Upgrade AGP and r8 2023-04-21 14:11:22 +08:00
tarsin 9d76132a5d Build with JDK 17 LTS
Since JDK 20 is out, there is no more jdk19 on ArchLinux, but kotlin compiler don't recognise JvmTarget 20 yet
2023-04-21 13:44:36 +08:00
tarsin 86a2d34f8e Revert "Update plugin com.android.application to v8.1.0-beta01"
This reverts commit 0a5b9e3178.
2023-04-21 12:51:57 +08:00
renovate[bot] 0a5b9e3178 Update plugin com.android.application to v8.1.0-beta01 2023-04-20 23:35:40 +00:00
tarsin 7665e86812 Fix GalleryDetailScene action layout 2023-04-20 12:29:00 +08:00
tarsin 29e5db87d9 Resolve #1031 2023-04-20 12:20:58 +08:00
renovate[bot] ffaa7c92e3 Update dependency composeOptions to v1.4.6 2023-04-20 01:40:51 +08:00
renovate[bot] b74a57967c Update dependency androidx.activity:activity-compose to v1.7.1 2023-04-20 01:40:38 +08:00
renovate[bot] dd368f4925 Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.7.0-RC 2023-04-20 01:11:05 +08:00
renovate[bot] fa6c55636b Update dependency io.ktor:ktor-client-okhttp to v2.3.0 2023-04-20 01:09:12 +08:00
renovate[bot] 9fa98671ac Update plugin com.google.devtools.ksp to v1.8.20-1.0.11 2023-04-18 16:49:45 +00:00
renovate[bot] 60f917a52d Update dependency com.google.android.material:material to v1.9.0-rc01 2023-04-14 02:10:51 +00:00
renovate[bot] 0f848a3f92 Update dependency composeOptions to v1.4.5 2023-04-13 01:40:30 +00:00
renovate[bot] bf03d3f15a Update dependency gradle to v8.1 2023-04-12 23:21:35 +08:00
tarsin b864eba08f Suspendize removeFromFavorites 2023-04-12 14:20:15 +08:00
tarsin a8988ac17b Suspendize addToFavorites 2023-04-12 13:57:33 +08:00
tarsin d72a44b9f2 Optimize CrystalCard, reduce compose allocation 2023-04-11 14:47:11 +08:00
tarsin f9ed777c7f Fix crash 2023-04-11 14:11:00 +08:00
tarsin 2cc6e8a1a6 Do cleanup 2023-04-10 19:01:30 +08:00
tarsin 9efb8cf450 Cleanup unused res 2023-04-10 19:00:47 +08:00
tarsin 2befb766ca Kill LoadImageView 2023-04-10 18:18:26 +08:00
tarsin ded8e77f5d Sync download item with gallery list item layout 2023-04-10 18:05:15 +08:00
tarsin 7ec9d9e1cf Drop FixedThumb 2023-04-10 17:56:47 +08:00
tarsin be1d80de0f Convert GalleryPreviewsAdapter holder to compose 2023-04-10 17:11:55 +08:00
tarsin 7dc28f36dc Fix to save preview data onSaveInstanceState 2023-04-08 16:20:23 +08:00
tarsin 98de1d89da Remove explicitly dependency accompanist drawable painter 2023-04-06 15:33:22 +08:00
tarsin 8a542aebe2 Optimize compose async thumb clip 2023-04-06 15:28:09 +08:00
tarsin e7c077c31e Fixup! Cleanup permission request 2023-04-06 14:25:23 +08:00
tarsin 3c0da086f5 Cleanup permission request 2023-04-06 14:20:02 +08:00
renovate[bot] 06dc33b126 Update dependency androidx.core:core-ktx to v1.10.0 2023-04-06 00:50:50 +00:00
FooIbar 817ed1ce8f Fix gallery preview not being restored 2023-04-05 10:05:16 +08:00
FooIbar ec027d3d98 Restore old strategy of grid column count 2023-04-05 10:05:16 +08:00
tarsin 29214b58c0 Temp workaround for thumb column calculate 2023-04-04 21:38:30 +08:00
renovate[bot] bbc8f6b15e Update dependency com.google.accompanist:accompanist-drawablepainter to v0.30.1 2023-04-04 13:54:32 +08:00
renovate[bot] 4f597584fb Update dependency com.google.accompanist:accompanist-themeadapter-material3 to v0.30.1 2023-04-04 13:53:47 +08:00
tarsin 2d40cf801d Kotlin 1.8.20 2023-04-04 12:43:22 +08:00
FooIbar 3bb4772aea Use the same strategy as gallery detail preview 2023-04-04 12:11:34 +08:00
FooIbar 5b44df4476 Tweak gallery detail layout 2023-04-04 12:11:34 +08:00
tarsin 3762b69fed Introduce NamedMutex
Maybe we should have a better impl?
2023-04-03 22:55:24 +08:00
tarsin 18c205dde2 Don't let network IO delay EhTagDatabase initialization 2023-04-03 22:12:42 +08:00
tarsin caa4e07f8c Tweak GalleryDetailHeaderCard uploader chip icon usage 2023-04-03 21:58:41 +08:00
FooIbar a3ae2d98a5 Fixed by upstream, remove workaround 2023-04-03 20:42:29 +08:00
tarsin 0fd4cdd498 Fix GalleryDetailScene error tip width, Resolve #1009 2023-04-03 00:25:55 +08:00
FooIbar 0e271bbbc2 Convert MyTagsFragment to kotlin 2023-04-03 00:06:40 +08:00
FooIbar ff18198cd1 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 9354ca7c68 Convert AboutFragment to kotlin 2023-04-03 00:06:40 +08:00
FooIbar e8ed55bb41 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 6a9648e7f5 Convert SettingsFragment to kotlin 2023-04-03 00:06:40 +08:00
FooIbar b41097790d Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar e91a3d74b3 Convert ParserUtils to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 4d5ffe1618 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 3b04624b88 Convert VoteTagParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 21b3b66bcd Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 69826ccb1f Convert VoteCommentParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 66f9bfd871 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar fdc1868cd4 Convert TorrentParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 917cb107ec Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar d3b3467989 Convert SignInParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 3f09122e6d Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar bfd0361fd5 Convert RateGalleryParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 1c52e246f0 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 09a403cb53 Convert GalleryPageUrlParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar e9699abc81 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar b186c47e07 Convert UrlOpener to kotlin 2023-04-03 00:06:40 +08:00
FooIbar f26f11f89b Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar f8bbc2d931 Convert UrlPreference to kotlin 2023-04-03 00:06:40 +08:00
FooIbar cbe0d5c3dd Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar c6a802a286 Convert GalleryPageParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 27fce7267f Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 5b0c302530 Convert GalleryPageApiParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 47c29d9eb5 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 228dad8b85 Convert GalleryListUrlParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 70ad933435 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar b061219bac Convert GalleryDetailUrlParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar da25290fad Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 9f55aa657b Convert GalleryApiParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 2740dd094b Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 0089f232c4 Convert GetText to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 64ffe1ee14 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar b3d1cc81cd Convert ExceptionUtils to kotlin 2023-04-03 00:06:40 +08:00
FooIbar f8691e3868 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 1ab14afeaa Convert EhProxySelector to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 27a6b1a5c4 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 78c8034b4b Convert ProfileParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 3a9c6f65de Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar f5bedf6239 Convert GalleryTokenApiParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 6e3dcc5192 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 5f6c3f1078 Convert GalleryNotAvailableParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar 5f67e5ab03 Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar e7a9837da1 Convert GalleryMultiPageViewerPTokenParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar c80c892faf Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 94dce083f1 Convert ForumsParser to kotlin 2023-04-03 00:06:40 +08:00
FooIbar bad39c697f Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar c2a9ea96e9 Convert AppHelper to kotlin 2023-04-03 00:06:40 +08:00
FooIbar aff110df5d Rename .java to .kt 2023-04-03 00:06:40 +08:00
FooIbar 428bed2cde Fix garbled version code in crash logs 2023-04-02 15:16:26 +08:00
FooIbar f06da4c033 Fix DownloadService nullability 2023-04-02 15:16:26 +08:00
小白-白 fd79d5d034 Don't update existing dir 2023-04-01 11:18:32 +08:00
FooIbar c4c4fdc647 Fix Arrow dependency 2023-04-01 11:18:09 +08:00
小白-白 661bf3ecc2 Ktlint: do code format 2023-03-31 22:37:36 +08:00
小白-白 04c99a5a36 Add ktlint, resolve #999 2023-03-31 22:37:36 +08:00
renovate[bot] ded4e015bd Update plugin com.android.application to v8.1.0-alpha11 2023-03-30 22:49:47 +00:00
tarsin 97becdb7e3 Fix to use NonCancellable coroutine ctx
Now that it's suspend
2023-03-29 18:29:40 +08:00
tarsin 929766b177 Fix to use suspend mutex 2023-03-29 18:23:55 +08:00
tarsin 2db6f7a9ae Modify .nomedia status in background thread 2023-03-29 13:18:55 +08:00
tarsin b6d8a340e5 Cleanup lrucache create 2023-03-28 16:20:00 +08:00
tarsin 175f75c3a2 Obey new m3 drawer design token 2023-03-27 16:19:22 +08:00
tarsin 0eda9b109f Do limit navigation drawer maxWidth
Why the default value gone after MDC 1.9.0-alpha01 ???
2023-03-27 15:58:01 +08:00
tarsin f07be06dfe Opt-in ExperimentalMaterial3WindowSizeClassApi 2023-03-26 19:39:29 +08:00
tarsin 32a770b9b5 Convert to use window size class 2023-03-26 18:34:59 +08:00
tarsin 2f2b440196 Import material3 window size class 2023-03-26 18:09:23 +08:00
tarsin 379422f457 Reformat code 2023-03-26 18:07:21 +08:00
hathlife 3519756da3 Initial support of adaptive landscape layout with
jetpack compose.
2023-03-26 17:39:38 +08:00
renovate[bot] 8746f04030 Update dependency io.coil-kt:coil-compose to v2.3.0 2023-03-26 11:30:54 +08:00
tarsin c7d9697b92 Adjust comment and preview spacer 2023-03-25 17:35:16 +08:00
tarsin 66053895ad Fixup card flavour usage 2023-03-25 17:27:53 +08:00
tarsin d7c21e06ad Fix tag search 2023-03-25 11:46:53 +08:00
renovate[bot] 799589874e Update dependency androidx.collection:collection-ktx to v1.3.0-alpha04 2023-03-25 02:09:44 +00:00
小白-白 97733ca337 Fix save img 2023-03-24 08:21:08 +08:00
tarsin 1a0474420b Fix old data offset 2023-03-23 16:40:47 +08:00
tarsin f1c1f2dbe8 Let room generate kotlin 2023-03-23 16:14:26 +08:00
tarsin bfb5752622 Tweak dependencies 2023-03-23 15:40:52 +08:00
renovate[bot] b3e9f594dc Update dependency dev.chrisbanes.compose:compose-bom to v2023.03.00 2023-03-23 15:08:40 +08:00
tarsin f81ca0688e Workaround for drawerLayout handle OnBackPressedInvoked only above API 33 2023-03-23 13:03:49 +08:00
tarsin 095a9149b4 Let DrawerLayout handle OnBackPressedInvoked internally 2023-03-23 12:38:57 +08:00
renovate[bot] 322a86c105 Update dependency com.google.accompanist:accompanist-drawablepainter to v0.30.0 2023-03-23 10:16:33 +08:00
renovate[bot] 015fb299ba Update room_version to v2.5.1 2023-03-23 10:14:58 +08:00
renovate[bot] 9e7f407d35 Update dependency androidx.activity:activity-compose to v1.7.0 2023-03-23 10:14:46 +08:00
renovate[bot] 96b891fbd0 Update dependency com.google.accompanist:accompanist-themeadapter-material3 to v0.30.0 2023-03-23 10:14:20 +08:00
renovate[bot] 0cdb35dcfe Update dependency androidx.work:work-runtime-ktx to v2.8.1 2023-03-23 10:12:30 +08:00
renovate[bot] 56be5c84cb Update dependency composeOptions to v1.4.4 2023-03-23 10:12:18 +08:00
renovate[bot] 2cb8fc25f5 Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha09 2023-03-23 10:10:38 +08:00
renovate[bot] 941c5efd07 Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.0-alpha09 2023-03-23 10:06:22 +08:00
renovate[bot] bbdb335e92 Update dependency androidx.lifecycle:lifecycle-process to v2.6.1 2023-03-23 10:06:10 +08:00
renovate[bot] cd0c5c8e2a Update dependency androidx.collection:collection-ktx to v1.3.0-alpha03 2023-03-22 23:23:27 +00:00
tarsin 8924d36925 Tidy native function namespace 2023-03-22 17:48:10 +08:00
tarsin 8a39bf6ac8 Cleanup and prepare to directly rename image blob from cache to download 2023-03-22 16:51:01 +08:00
tarsin a6b4f05f7f Fix copy to download dir in mixed mode 2023-03-22 12:27:01 +08:00
tarsin 34c39464ae Adjust icon usage 2023-03-22 11:02:37 +08:00
tarsin 3dd45c1628 Fix crash
java.lang.IllegalStateException: Fragment GalleryDetailScene{b6e25c2} (70626de1-16ff-47db-b4eb-e3358b5f53fc) not attached to a context.
        at androidx.fragment.app.Fragment.requireContext(Unknown Source:17)
        at androidx.fragment.app.Fragment.getResources(Unknown Source:0)
        at androidx.fragment.app.Fragment.getString(Unknown Source:0)
        at com.hippo.ehviewer.ui.scene.GalleryDetailScene.updateDownloadText(Unknown Source:93)
        at com.hippo.ehviewer.ui.scene.GalleryDetailScene.bindViewSecond(Unknown Source:68)
        at com.hippo.ehviewer.ui.scene.GalleryDetailScene$request$1.com.hippo.ehviewer.ui.scene.GalleryDetailScene.access$bindViewSecond(Unknown Source:131)
                                                             invokeSuspend
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(Unknown Source:8)
        at kotlinx.coroutines.DispatchedTask.run(Unknown Source:113)
        at kotlinx.coroutines.internal.LimitedDispatcher.run(Unknown Source:12)
        at kotlinx.coroutines.scheduling.TaskImpl.run(Unknown Source:2)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(Unknown Source:93)
                                                            kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask
                                                            kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker
                                                            run
        Suppressed: qs: [Is0{Cancelling}@bfcdc0e, Dispatchers.IO]
2023-03-22 11:01:08 +08:00
renovate[bot] b962c168c4 Update plugin com.android.application to v8.1.0-alpha10 2023-03-21 22:17:38 +00:00
tarsin c12bd32682 Allow decode avif 2023-03-19 00:32:10 +08:00
tarsin ef20f1578d Fixup! Allow larger read cache 2023-03-18 21:32:07 +08:00
tarsin 47c9e07857 Allow larger read cache 2023-03-18 21:30:47 +08:00
tarsin 6d8208d7f4 Allow preload thumbs aggressively 2023-03-18 21:09:20 +08:00
tarsin 946e76c3cc Make galleryDetail preview footer still a textbutton, but no nestScroll 2023-03-17 21:52:50 +08:00
5idereal 08bd8176b9 update zh-tw translation 2023-03-17 21:37:09 +08:00
tarsin 2ea06425f4 Add trailingIcon to block uploader 2023-03-17 21:34:04 +08:00
tarsin 95ca7b23d3 Restore click tip to refresh 2023-03-17 21:28:13 +08:00
tarsin ca6eb03f1e Fix edge to edge 2023-03-17 21:21:07 +08:00
tarsin 8831a41bff Fix no tag hint arrangement 2023-03-17 21:16:30 +08:00
tarsin 6418beb186 Fix GalleryDetailScene comment 2023-03-17 21:13:24 +08:00
tarsin ebbc5cdc2c Restore galleryDetail Comment
Any better way to implement this?
2023-03-17 21:01:44 +08:00
tarsin abceb71dcb Cleanup legacy code 2023-03-17 20:39:06 +08:00
tarsin 7ab898ee85 Cleanup GalleryDetailScene 2023-03-17 20:37:55 +08:00
tarsin 904840c4cb Fix card style 2023-03-17 20:12:32 +08:00
tarsin ecd39f5435 Fix clip preview crop 2023-03-17 20:01:42 +08:00
tarsin 7742b2c570 Stub impl of preview image clip
this way sucks, later we will clip it with compose method
2023-03-17 19:07:34 +08:00
tarsin 89b20b3cda Apply proper crop 2023-03-17 18:31:53 +08:00
tarsin 4b0f62e058 Use normal lazyGrid 2023-03-17 18:19:10 +08:00
tarsin f7281a7481 Restore preview items 2023-03-17 18:11:30 +08:00
tarsin ae38fecae7 Restore preview hint 2023-03-17 17:42:34 +08:00
tarsin f5577d45c5 Restore preview padding 2023-03-17 17:32:11 +08:00
tarsin 0b45c41a99 Convert to LazyVerticalStaggeredGrid, preparing add previews 2023-03-17 17:28:22 +08:00
tarsin 9b74e7fdec Optimize recompose 2023-03-17 17:05:08 +08:00
tarsin b522360357 Restore GalleryDetailScene popmenu 2023-03-17 16:53:02 +08:00
tarsin 109cb776a0 limit max title lines 2023-03-17 16:42:33 +08:00
tarsin 9fdc55814d Restore navback icon 2023-03-17 16:41:04 +08:00
tarsin 2079a90f02 Fix scroll behaviour 2023-03-17 16:36:53 +08:00
tarsin 7fdf90e87d First stub migrate whole GalleryDetailScene to compose 2023-03-17 16:17:13 +08:00
tarsin f7bc2e2cf7 Use partial function 2023-03-17 13:21:45 +08:00
tarsin 9d442b9b18 Fix non-local return in Composable cause internal error
They should have a check in compose compiler...
2023-03-17 12:38:26 +08:00
tarsin 3c777905f5 Cleanup res 2023-03-17 00:19:27 +08:00
tarsin c91bbc94fd Apply alpha to weak tag 2023-03-16 23:47:38 +08:00
tarsin e51142ae32 Fix flowrow multiple row padding 2023-03-16 23:36:17 +08:00
tarsin 1fd7d7f602 Fix tag focused ripple leak 2023-03-16 23:33:01 +08:00
tarsin 6ceb50c0bd Convert galleryDetail tags to compose 2023-03-16 23:19:54 +08:00
tarsin b63b189fda Strip unused res 2023-03-16 20:04:56 +08:00
tarsin b8ff8cb535 Make galleryDetail rating clickable 2023-03-16 19:56:12 +08:00
tarsin 2e9124c52d Restore galleryDetail rating 2023-03-16 19:49:10 +08:00
tarsin 261ccdda51 Refactor RatingWidget to use imageVec 2023-03-16 19:38:15 +08:00
tarsin 974108088a Restore some exist function 2023-03-16 19:23:53 +08:00
tarsin ebc9d2d1b1 Fix padding 2023-03-16 19:19:01 +08:00
tarsin 3733d827af Fix newer version card 2023-03-16 19:16:52 +08:00
tarsin 1333aa10f8 Rewrite gallery detail actions with compose 2023-03-16 19:05:48 +08:00
tarsin 8621de6d09 Cleanup JvmStatic usage 2023-03-16 16:56:24 +08:00
tarsin b48775d222 Fix legacy spiderInfo read 2023-03-16 12:47:44 +08:00
fanhl 3b918c38fd Add padding for quick_search item. 2023-03-15 10:37:17 +08:00
renovate[bot] e21e7d863d Update plugin com.android.application to v8.1.0-alpha09 2023-03-14 22:03:58 +00:00
tarsin 3b4329fcc2 Fix ridiculous bug 2023-03-13 20:26:07 +08:00
tarsin 08bc5ef72f Sync with 1.8.7.2 versionCode 2023-03-13 19:40:48 +08:00
tarsin f378ee441a Refactor out GalleryDetailHeaderCard 2023-03-13 19:38:07 +08:00
tarsin bbf0fda484 Revert a padding 2023-03-13 19:16:36 +08:00
tarsin 8cb4d8149b Refactor out 2023-03-13 19:15:01 +08:00
tarsin eceadf9f10 Fix layout 2023-03-13 19:07:27 +08:00
tarsin ae63e09a92 Fixup Import coordinatorlayout-compose 2023-03-13 18:46:35 +08:00
tarsin b778578353 Import coordinatorlayout-compose 2023-03-13 18:32:54 +08:00
tarsin a2ec9c4a41 Fake Impl of infoCard, need refine 2023-03-13 18:31:32 +08:00
tarsin 30fc4a91d6 Fix Header Chip Text 2023-03-13 18:00:03 +08:00
tarsin 1d6307d142 Refine filter chip Icon 2023-03-13 17:39:47 +08:00
tarsin 93524c0e4a First migration Header Card to Compose 2023-03-13 17:26:05 +08:00
tarsin 30e2d310f3 Prepare to rewrite GalleryDetailScene with compose 2023-03-13 16:32:16 +08:00
tarsin d1c87a65b2 Fix db export 2023-03-13 16:09:38 +08:00
renovate[bot] 961615f1c4 Update dependency dev.chrisbanes.compose:compose-bom to v2023.02.00-SNAPSHOT 2023-03-12 18:25:37 +08:00
tarsin 9a92132981 Import ArrowKt 2.0 2023-03-12 16:02:05 +08:00
tarsin d78120c80d Upgrade to chrisbanes compose bom 2023-03-12 15:47:39 +08:00
tarsin c560b7cd1f Enlarge recyclerview height 2023-03-12 15:17:02 +08:00
tarsin f0a8e1c0bc Rename FooterAdapter to HintAdapter 2023-03-12 15:09:12 +08:00
tarsin 9e77f104f6 Convert footer holder to compose 2023-03-12 15:05:17 +08:00
tarsin 029d6bf7f6 Normalize footer ripple 2023-03-12 14:32:21 +08:00
tarsin 3cb0e3ecd4 Revert Horizontal padding 2023-03-12 14:18:42 +08:00
tarsin 8708897751 Fix and simplify layout 2023-03-12 14:03:10 +08:00
renovate[bot] d920e8705f Update dependency dev.rikka.rikkax.layoutinflater:layoutinflater to v1.3.0 2023-03-11 16:09:27 +00:00
tarsin f5e9bf2459 Revert "Workaround for massive thumb requests delay post request for (adding favourite, get torrent list etc.)"
This reverts commit 0a9d7c5560.
2023-03-10 23:11:02 +08:00
FooIbar 6a88aaf782 Use ConcatAdapter for previews in GalleryDetailScene 2023-03-10 20:08:52 +08:00
FooIbar 9d9c09743c Convert GalleryListParser and FavoritesParser to kotlin 2023-03-10 20:08:52 +08:00
FooIbar e076f34993 Rename .java to .kt 2023-03-10 20:08:52 +08:00
FooIbar 923ec7ce5e Convert previews in GalleryDetailScene to RecyclerView 2023-03-10 11:00:24 +08:00
FooIbar 15ae3cecb1 Convert AutoGridLayoutManager to kotlin 2023-03-10 11:00:24 +08:00
FooIbar 3f8e26644a Rename .java to .kt 2023-03-10 11:00:24 +08:00
FooIbar a6a2a6ebba Introduce BaseListAdapter and BaseViewHolder 2023-03-10 11:00:24 +08:00
FooIbar 4175816577 Cleanup ContentLayout 2023-03-10 11:00:24 +08:00
FooIbar 36679ffbc7 Fix gallery previews 2023-03-10 01:58:38 +08:00
FooIbar b2c4cd721c Use async version of call.execute() 2023-03-10 01:58:38 +08:00
tarsin da9aeb54f0 Upgrade kotlinx coroutines 2023-03-09 22:58:55 +08:00
tarsin 0a9d7c5560 Workaround for massive thumb requests delay post request for (adding favourite, get torrent list etc.)
Currently okhttp does not support request priority(See https://github.com/square/okhttp/issues/404), and our GalleryDetailScene will launch at most 200 image requests once, which stuffed okhttp async request queue.
We need another http client to ensure these plain-text request executed in time
2023-03-09 22:30:22 +08:00
tarsin 14843ddda7 Reland enableNewResourceShrinker.preciseShrinking 2023-03-09 21:47:09 +08:00
tarsin dada671f97 Cleanup GalleryCard 2023-03-09 19:04:25 +08:00
tarsin 1a7214c75b Fix rating calculate 2023-03-09 18:07:30 +08:00
tarsin 23eede12fc Cleanup HistoryScene 2023-03-09 17:46:45 +08:00
tarsin 58910be98e Fix HistoryScene lag when pop backstack from detailScene
Why supportsPredictiveItemAnimations = true cause lag?
2023-03-09 17:39:38 +08:00
tarsin f27319e8cb Use autogenerated localeConfig 2023-03-09 13:11:37 +08:00
tarsin 737c190107 Upgrade AGP, let's enjoy new AS UI 2023-03-09 12:42:33 +08:00
tarsin 6c2989a4d3 Fix add favourites 2023-03-09 11:32:37 +08:00
tarsin a5c05bd2f3 Continue to cleanup EhEngine 2023-03-09 11:25:14 +08:00
tarsin a136c5349e Make some constants top-level 2023-03-09 11:09:05 +08:00
tarsin 8b8d845068 Continue to cleanup EhEngine 2023-03-09 11:00:38 +08:00
FooIbar 7c41b5d4ed Drop HashCodeUtils
And don't reread the same file
2023-03-09 09:17:04 +08:00
FooIbar 39336a447b Copy star cookie to ex as well 2023-03-09 09:17:04 +08:00
FooIbar f58c33354e Refactor EhEngine.getUConfig() 2023-03-09 09:17:04 +08:00
renovate[bot] 9a9b5037ec Update dependency androidx.lifecycle:lifecycle-process to v2.6.0 2023-03-09 09:16:51 +08:00
renovate[bot] effe9c281a Update dependency androidx.core:core-ktx to v1.10.0-rc01 2023-03-09 09:16:32 +08:00
renovate[bot] c590ff875b Update dependency androidx.recyclerview:recyclerview to v1.3.0 2023-03-09 09:16:19 +08:00
renovate[bot] 3771c325a2 Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha08 2023-03-08 23:24:22 +00:00
tarsin 7746e95cad Cleanup EhEngine 2023-03-08 22:12:08 +08:00
tarsin 767c10f886 Fix code style 2023-03-08 22:02:01 +08:00
tarsin d97a15df57 Fix mainthread db query 2023-03-08 21:35:37 +08:00
tarsin 55cd89a0e5 Fix body consumed twice 2023-03-08 21:31:57 +08:00
FooIbar 8ef7ac5a87 Disable SupportsPredictiveItemAnimations in DownloadsScene 2023-03-08 21:19:59 +08:00
FooIbar 5b6a06b33b Convert AutoStaggeredGridLayoutManager to kotlin 2023-03-08 21:19:59 +08:00
FooIbar cd4971962a Rename .java to .kt 2023-03-08 21:19:59 +08:00
tarsin f18e2e1e55 Refactor EhEngine 2023-03-08 21:06:15 +08:00
tarsin 0d8925c787 Cleanup code 2023-03-08 20:14:25 +08:00
tarsin c773cd1842 Revert "Cleanup code"
This reverts commit bf9d2c82ac.
2023-03-08 20:13:13 +08:00
tarsin bf9d2c82ac Cleanup code 2023-03-08 20:12:32 +08:00
tarsin 7958785588 Cleanup some warnings 2023-03-08 20:05:24 +08:00
tarsin 56ea23c4bf Fix previewscene toolbar liftonscroll 2023-03-08 20:04:27 +08:00
FooIbar c382f773a7 Convert ContentHelper to view binding 2023-03-08 19:41:40 +08:00
FooIbar 46995511f1 Convert ContentLayout to kotlin 2023-03-08 19:41:40 +08:00
FooIbar 7d60037998 Rename .java to .kt 2023-03-08 19:41:40 +08:00
tarsin cdf53c812a Resolve #934 2023-03-08 10:53:42 +08:00
tarsin b628755464 Fix similarity search 2023-03-08 10:45:21 +08:00
tarsin 69da2f14c7 Revert to ktor okhttp engine 2023-03-08 10:38:16 +08:00
tarsin 3ea0b0847a Cleanup code
filechannel is close when ios closed
2023-03-07 23:20:02 +08:00
tarsin 47da1bcdb4 Update README.md 2023-03-07 22:13:46 +08:00
FooIbar 691dc18f3b Refactor GalleryPreviewsScene 2023-03-07 22:11:30 +08:00
FooIbar 5ddd948ace Initialize previews with existing data 2023-03-07 22:11:30 +08:00
FooIbar b815c924ff Drop PreviewSet 2023-03-07 22:11:30 +08:00
tarsin f46d72a9eb Fix df 2023-03-07 22:05:56 +08:00
tarsin 9b58d1161b Let's try Apache5 http engine 2023-03-07 21:34:11 +08:00
tarsin 3208a34b75 Update README.md 2023-03-07 16:57:40 +08:00
tarsin e50bb39770 Update README.md 2023-03-07 16:53:02 +08:00
tarsin bcf9c7df30 Update README.md 2023-03-07 16:46:04 +08:00
tarsin e8f97beb84 Update README.md 2023-03-07 16:45:37 +08:00
tarsin d73ff60c36 Prepare for 1.8.8.x development 2023-03-07 13:36:49 +08:00
tarsin 6414f14420 fixup! Resolve #931 2023-03-07 13:19:02 +08:00
tarsin 5787753279 Resolve #931 2023-03-07 13:17:24 +08:00
tarsin 4a838597f8 Fix restore download 2023-03-07 12:55:28 +08:00
tarsin c3c5e2b5b0 Change image cache dir
Since we haven't release, we can do that
2023-03-07 11:51:04 +08:00
tarsin 6a184462c6 Bring back httpcache, but for non-Image only 2023-03-07 11:49:02 +08:00
tarsin 10def6e48a Cleanup dependencies 2023-03-07 10:58:56 +08:00
tarsin 29c97b9d79 Move some temporary code to separated package 2023-03-07 10:53:25 +08:00
tarsin d5e1f8814f Fix name 2023-03-07 10:46:43 +08:00
tarsin 972cb55801 Minor cleanup 2023-03-07 10:45:38 +08:00
tarsin a65a1a05f0 Minor cleanup 2023-03-07 10:32:36 +08:00
tarsin 659afd9245 Respect NinePatchDrawable 2023-03-07 10:28:52 +08:00
tarsin 73b9b353e6 Cleanup UniFile api 2023-03-07 10:23:20 +08:00
tarsin 34337d18fa Revert to use ktor as okhttp delegate
Ktor CIO is still not a jvm-heapless http client, actually it does the same thing okhttp does, decrypt TLS packet to JVM heap, and copy it to direct buffer, then write it to fd... , just the same thing we do use transferFrom with Okio Buffer.. let's implement a native zero-copy ktor client now!
2023-03-07 10:16:04 +08:00
tarsin f57917e2b6 Minor cleanup 2023-03-07 10:09:20 +08:00
tarsin a39bc1182a Fix normal preview set load 2023-03-07 10:04:27 +08:00
FooIbar 795e42f609 Clean obsolete cache on start 2023-03-07 09:18:09 +08:00
FooIbar b39b0a5ca6 Use relative URL as thumb cache key
Also increase default cache size to 640MB
2023-03-07 09:18:09 +08:00
FooIbar dcea49c682 Convert UnikeryDrawable to kotlin 2023-03-07 09:18:09 +08:00
FooIbar c1e7d043f9 Rename .java to .kt 2023-03-07 09:18:09 +08:00
FooIbar f354b86d1d Implement partial bind in DownloadAdapter 2023-03-06 23:27:23 +08:00
FooIbar 9d4977e2b3 Convert DownloadsScene to view binding 2023-03-06 23:27:23 +08:00
FooIbar 595147bacc Convert drawer list item to view binding 2023-03-06 23:27:23 +08:00
FooIbar 39681dcb8a Convert download item to view binding 2023-03-06 23:27:23 +08:00
FooIbar 31be6a2cca Update translation 2023-03-06 18:30:28 +08:00
tarsin 0286557701 Rewrite BBCode kot style and fix wildcard import 2023-03-06 18:30:09 +08:00
FooIbar 805e3c920c Update issue templates 2023-03-05 18:19:34 +08:00
FooIbar 26f05a0800 Fix settings profile cookie 2023-03-05 10:41:30 +08:00
FooIbar b944d073de Avoid querying database on main thread
No need to use NonCancellable since it's not cancellable anyway
2023-03-05 10:41:30 +08:00
FooIbar 56fdb2d4a8 Convert UConfigFragment to kotlin 2023-03-05 10:41:30 +08:00
FooIbar c83bd885f6 Rename .java to .kt 2023-03-05 10:41:30 +08:00
FooIbar d4ec177c28 Apply list mode setting in onResume 2023-03-05 01:33:16 +08:00
FooIbar e9839335c6 Convert GalleryHolder to view binding 2023-03-05 01:33:16 +08:00
tarsin 562a6d6ab8 Merge image cache 2023-03-05 01:32:51 +08:00
renovate[bot] 4cfeb88bb3 Update dependency gradle to v8.0.2 2023-03-04 15:48:36 +08:00
renovate[bot] 54858261f8 Update plugin com.android.application to v8.0.0-beta04 2023-03-03 01:09:45 +00:00
tarsin 8e4d3df400 Provide cookies database autoMigration 2023-03-02 21:52:32 +08:00
tarsin dc47f68ff3 Proper way to emit outdated cookies 2023-03-02 21:24:48 +08:00
tarsin c1852f4728 Fix login via Webview 2023-03-02 21:23:08 +08:00
tarsin 0e9c7761f5 Minor cleanup 2023-03-02 16:06:18 +08:00
tarsin b4acb06665 Refactor to support general cookies 2023-03-02 15:53:59 +08:00
tarsin 98107e40bc Refactor cookies database 2023-03-02 15:34:42 +08:00
tarsin d1d1da7875 Refactor cookies database with Room
No db migration available since re-login is not difficult
2023-03-02 15:08:23 +08:00
FooIbar a800094b21 Use cd . instead of true for Windows compatibility 2023-03-02 03:53:43 +08:00
FooIbar 89736d1d45 Minor refactor 2023-03-02 03:51:42 +08:00
FooIbar 06650c14ad Do not respond to click if gallery detail is not ready 2023-03-02 03:51:42 +08:00
tarsin 1c1066eff2 Resolve #901 2023-03-01 18:18:13 +08:00
tarsin 5bb63cf3e5 Resolve #903 2023-03-01 18:04:00 +08:00
FooIbar 2ba39a9b80 Do not build other ABIs in debug 2023-03-01 09:57:55 +08:00
renovate[bot] 7fef84a006 Update dependency io.ktor:ktor-client-cio to v2.2.4 2023-02-28 21:35:52 +08:00
tarsin f6458cd8ee Cleanup some IO overhead 2023-02-28 17:58:45 +08:00
tarsin 5c18dad1d8 Bring back ktor CIO for image downloading 2023-02-28 17:52:16 +08:00
tarsin 3f4efc51c9 Don't create an arraylist per request 2023-02-27 23:59:02 +08:00
tarsin 841ac0cb74 fixup! Implement image request merging in standalone Interceptor 2023-02-27 23:01:41 +08:00
tarsin e2be9580c6 Use IO Dispatcher to collect history data 2023-02-27 22:16:56 +08:00
tarsin 154cf18b34 Move HistoryScene ViewHolder to Compose 2023-02-27 21:53:02 +08:00
FooIbar 0a93763c48 Check dawn on start in case the periodic work failed 2023-02-27 20:53:19 +08:00
FooIbar c4c654079d Convert Settings to kotlin 2023-02-27 20:53:19 +08:00
FooIbar 92f8e67a60 Rename .java to .kt 2023-02-27 20:53:19 +08:00
FooIbar 1814f06311 Convert DownloadFragment to kotlin 2023-02-27 20:53:19 +08:00
FooIbar 2dbc704448 Rename .java to .kt 2023-02-27 20:53:19 +08:00
FooIbar e2478b4eb8 Drop EhConfig
No longer valid due to eh changes
2023-02-27 20:53:19 +08:00
tarsin b7b3e8d4cb Cleanup crash utils 2023-02-27 20:43:52 +08:00
tarsin 0a089351e9 Cleanup some StreamIO usecase 2023-02-27 19:01:26 +08:00
tarsin 386a991ad7 Remove unused code 2023-02-27 18:49:32 +08:00
tarsin e028834ac3 Convert a usecase to sendTo 2023-02-27 18:42:20 +08:00
tarsin 695a4966b5 Clarify that Java Stream IO sucks 2023-02-27 18:18:29 +08:00
tarsin 3973f5ef70 Make UniFile actually backed by ParcelFileDescriptor, implement IO stream as extensions, Resolve #896 2023-02-27 18:13:00 +08:00
tarsin 24ccf60870 Move getImageSource to UniFile interface, and remove UniRandomAccessFile, it's a suck design 2023-02-27 17:24:04 +08:00
tarsin c4a3f06b51 Cleanup unnecessary jni call 2023-02-27 17:05:54 +08:00
tarsin 5dff1f7678 Use platform delegate systemcall sendfile 2023-02-27 16:30:46 +08:00
tarsin d3d07f9e4e Release 1.8.7.0 2023-02-27 15:53:49 +08:00
tarsin e5500b978c Apply alpha only to tag text, Resolve #894 2023-02-27 12:06:57 +08:00
FooIbar 2fea94ce9b fixup! Implement image request merging in standalone Interceptor 2023-02-27 02:32:33 +08:00
tarsin 90b6106fb4 Convert libarchive build to FetchContent 2023-02-26 17:31:59 +08:00
tarsin 6e15d1e480 Convert lzma build to FetchContent 2023-02-26 15:11:45 +08:00
tarsin d35588b993 Don't add custom optimization in debug build, and remove some flags already passed 2023-02-26 14:04:20 +08:00
tarsin 957ecf0fe1 Fix rating calculate 2023-02-25 23:47:10 +08:00
tarsin 83dc47c3b5 Fix SimpleRatingWidget state changed recompose and properly set theme 2023-02-25 23:40:01 +08:00
tarsin fa4d35487c Optimize SimpleRatingWidget 2023-02-25 23:19:09 +08:00
tarsin 624a20c9bf Move RatingView to Compose impl 2023-02-25 23:14:57 +08:00
tarsin 58cd5c2896 Rename .java to .kt 2023-02-25 23:14:57 +08:00
renovate[bot] bb8a802d65 Update dependency org.jetbrains.kotlinx:kotlinx-serialization-cbor to v1.5.0 2023-02-25 02:44:44 +08:00
FooIbar f757e98da0 Implement onNewIntent() in ReaderActivity 2023-02-25 02:36:37 +08:00
FooIbar 49661c0bf0 Implement tachiyomi.util.system.logcat 2023-02-25 02:36:37 +08:00
tarsin 42f36b8e74 Release 1.8.7.0-alpha 2023-02-24 20:30:15 +08:00
tarsin 9bc3c9a902 Add Resource lock to avoid memory corruption 2023-02-24 20:02:35 +08:00
tarsin 1dd04740bb Use MADV_DONTNEED as fallback 2023-02-24 19:22:10 +08:00
FooIbar 35201e6615 Use natsort for better archive file sorting 2023-02-24 18:43:08 +08:00
tarsin 86d416bccf Don't populate archive 2023-02-24 18:33:44 +08:00
tarsin 979841665a Convert PreciselyClipDrawable to kt 2023-02-24 16:53:35 +08:00
tarsin c29b8c114b Rename .java to .kt 2023-02-24 16:53:35 +08:00
tarsin a03eae3c87 Refactor importDB! 2023-02-24 16:36:41 +08:00
tarsin 704c2a261e Use MAP_UNINITIALIZED to improve performance 2023-02-24 13:05:43 +08:00
tarsin 8fabdd1926 Suppress lint 2023-02-24 12:41:07 +08:00
tarsin f9df373a16 Some infeasible optimization 2023-02-24 12:39:58 +08:00
tarsin f959f98012 Minor cleanup 2023-02-24 12:02:42 +08:00
tarsin e12a93f36b Introduce new archive decompress mempool, Resolve #307 2023-02-24 11:37:30 +08:00
FooIbar c3aca0a88d Fix missing parentheses 2023-02-24 10:01:49 +08:00
FooIbar add7819945 Minor refactor
Revert "Fix RTL load"

This reverts commit 323d258b

Revert "Resolve #814"

This reverts commit eeed0720
2023-02-24 10:01:49 +08:00
FooIbar 3c6d3bbd80 Add commit sha and build time, drop PackageUtils 2023-02-24 10:00:45 +08:00
FooIbar 66e1315b08 Convert Crash to kotlin 2023-02-24 10:00:45 +08:00
FooIbar 9bbe010fc9 Rename .java to .kt 2023-02-24 10:00:45 +08:00
tarsin 4f81d164c8 KernelIO motto motto 2023-02-23 22:52:57 +08:00
tarsin c808e9135d Print stacktrace if failed to export
Though it is unlikely to happen
2023-02-23 22:40:28 +08:00
tarsin 1e7861ef15 Refactor exportDB! 2023-02-23 22:37:57 +08:00
tarsin a70b8c6dff Cleanup SubsamplingScaleImageView
InputStream sucks, we may add ImageDecoder impl later
2023-02-23 21:45:57 +08:00
tarsin ce86638e8e Cleanup ReaderPageImageView 2023-02-23 21:32:29 +08:00
tarsin 06ab978f53 Revert "Remove SubsamplingScaleImageView"
This reverts commit 5eda7b3fc3.
2023-02-23 21:29:04 +08:00
tarsin 9c2026982c Revert "Cleanup ReaderPageImageView"
This reverts commit c07325e71e.
2023-02-23 21:28:50 +08:00
tarsin 863b910311 Revert "Remove unused res"
This reverts commit 9462f1455b.
2023-02-23 21:28:34 +08:00
tarsin 68da3b7580 Upgrade compose compiler 2023-02-23 19:51:50 +08:00
FooIbar 43556f219b Improve login process regarding IP restriction 2023-02-23 17:55:45 +08:00
renovate[bot] 6022144dce Update dependency androidx.core:core-ktx to v1.10.0-beta01 2023-02-23 00:03:57 +00:00
tarsin 18a0e3fd9e Clarify we use concurrency, not multi-thread 2023-02-22 23:10:43 +08:00
FooIbar 6c7c6e4b22 Fix DailyCheckWork initialDelay being negative 2023-02-22 21:50:36 +08:00
tarsin ffa7a74575 Add youtrack issue link 2023-02-22 21:01:02 +08:00
tarsin 37f9c28133 Opt-in ExperimentalContractsApi globally 2023-02-22 20:34:11 +08:00
FooIbar ea7a012e0a Remove crop borders until we support it 2023-02-22 20:28:43 +08:00
tarsin 0d594da6c9 Cleanup some nullability 2023-02-22 20:26:06 +08:00
tarsin ed588bf26c Convert EhDB to kotlin 2023-02-22 19:23:49 +08:00
tarsin b6673fd828 Rename .java to .kt 2023-02-22 19:23:49 +08:00
tarsin 060bcc76ef Drop support for old db 2023-02-22 19:11:57 +08:00
tarsin 8fc8dd2960 Use version in BuildConfig 2023-02-22 19:07:22 +08:00
FooIbar 1dc36275aa Do not use notifyDataSetChanged() when refreshing WebtoonAdapter 2023-02-22 18:41:09 +08:00
tarsin f305e68202 Reland: Fix a atomic usage 2023-02-22 18:01:05 +08:00
tarsin 5c481df41f Revert "Migrate to atomicfu and fix a atomic usage"
Reason for revert: Don't use atomicfu, use platform atomic impl to
benefit from VarHandler backend
This reverts commit a0a215400e.
2023-02-22 17:58:23 +08:00
tarsin e86c626536 Revert "Replace all atomic usage"
This reverts commit a6c204e444.
2023-02-22 17:58:08 +08:00
tarsin 44559bab1c Revert "Revert "Replace all atomic usage""
This reverts commit 8b9985a7dc.
2023-02-22 17:57:53 +08:00
tarsin 2ae28f1c63 Revert "Reland: Replace all atomic usage"
This reverts commit 37f705d517.
2023-02-22 17:57:39 +08:00
tarsin b3e16d2b2a Revert "Proper way to configure atomicfu"
This reverts commit 22c8a36224.
2023-02-22 17:57:26 +08:00
tarsin 6f186080df Revert "Workaround atomicfu with r8"
This reverts commit c9d9335b71.
2023-02-22 17:57:16 +08:00
tarsin c9d9335b71 Workaround atomicfu with r8 2023-02-22 17:17:15 +08:00
tarsin 22c8a36224 Proper way to configure atomicfu 2023-02-22 16:49:20 +08:00
tarsin 37f705d517 Reland: Replace all atomic usage 2023-02-22 16:14:19 +08:00
tarsin 8b9985a7dc Revert "Replace all atomic usage"
This reverts commit a6c204e444.
2023-02-22 16:10:13 +08:00
tarsin f51e585d8d Resolve #865 2023-02-22 15:58:55 +08:00
tarsin a6c204e444 Replace all atomic usage 2023-02-22 15:35:40 +08:00
tarsin a0a215400e Migrate to atomicfu and fix a atomic usage 2023-02-22 15:30:10 +08:00
tarsin 37e3166f98 Remove all explicitly thread usage 2023-02-22 11:46:24 +08:00
tarsin ddccdc2ade Partially revert 7d87417095
DownloadManager is still piece of shit, need time to rewrite
2023-02-22 00:19:47 +08:00
tarsin 755a15d5e5 Explicitly init DownloadManager on app startup 2023-02-21 23:00:26 +08:00
tarsin 5c6716bd98 Migrate to new downloadManager accessor 2023-02-21 22:48:02 +08:00
tarsin 7d87417095 Deny mainthread db query! 2023-02-21 22:32:09 +08:00
tarsin 77bb6e9a82 Don't take SpiderQueen reference in detailFragment 2023-02-21 22:26:44 +08:00
tarsin 019174d7e2 Remove all AsyncTask usage, Resolve #631 2023-02-21 22:11:36 +08:00
tarsin cac88cbb81 Rename .java to .kt 2023-02-21 22:11:36 +08:00
FooIbar 25500da486 Set page status to QUEUE if its image needs update
Old image was recycled due to cache eviction
2023-02-21 08:56:30 +08:00
tarsin 53f2c7126b Add debug info for tracing resource leak 2023-02-21 00:43:37 +08:00
FooIbar eb95026a47 Do not launch new coroutines in invokeOnCompletion()
The scope might has been cancelled
2023-02-21 00:31:05 +08:00
tarsin 6b0f8bc325 Convert all PageLoader to kotlin 2023-02-21 00:30:19 +08:00
tarsin a80f41a89a Rename .java to .kt 2023-02-21 00:30:19 +08:00
tarsin 8690f8aa8d Rewrite ArchivePageLoader with modern kotlin coroutine and flows 2023-02-21 00:23:24 +08:00
tarsin d9c55a284b Rename .java to .kt 2023-02-21 00:23:24 +08:00
tarsin c8a10a8b75 Fixup! Fix thumb retry 2023-02-20 22:44:41 +08:00
tarsin 8e8dc76c3d Fix thumb retry 2023-02-20 22:38:53 +08:00
tarsin 725679f5d4 All refresh thumb at any time 2023-02-20 22:23:34 +08:00
tarsin dfce07d7eb Use framework's DrawableWrapper 2023-02-20 21:39:48 +08:00
FooIbar 73c9e37eba Fix failed download task won't report 2023-02-20 19:21:46 +08:00
tarsin ccd5bd8394 Sync with 1.8.6.2 versionCode 2023-02-20 19:09:02 +08:00
FooIbar fd9b7db999 Fix 509 detection false positive 2023-02-20 18:58:59 +08:00
tarsin f3fe115733 Upgrade buildTools 2023-02-20 18:52:55 +08:00
FooIbar 16f3f74a90 Clean up unfinished download files 2023-02-20 15:15:00 +08:00
FooIbar 4a6b85003a Workaround for del tag parsing in comments 2023-02-20 14:56:29 +08:00
tarsin bf651c7e40 Manage ImageView load status through coil
Will implement retry later
2023-02-20 13:32:53 +08:00
FooIbar 473ff4d308 Use initializer instead of getter for coroutineContext 2023-02-20 08:50:26 +08:00
FooIbar 5d64e8c61b fixup! Introduce kotlin style diskcache operation 2023-02-19 23:40:41 +08:00
FooIbar b559bb3904 Temporarily disable vpn bypass on login 2023-02-19 22:08:45 +08:00
tarsin 359f250b3b Introduce new CharSequence operator trimAnd 2023-02-19 21:06:28 +08:00
tarsin a20336a11a Introduce kotlin style diskcache operation 2023-02-19 20:54:34 +08:00
tarsin 0313c5705f Don't limit coil parallelism and don't allow rgb565 2023-02-19 18:23:01 +08:00
tarsin 14469638b3 Delegate all image request listener
In case request being cancelled
2023-02-19 18:14:25 +08:00
tarsin d15c5fc9d1 Implement image request merging in standalone Interceptor, Don't modify coil 2023-02-19 18:08:35 +08:00
tarsin 16e7d266fe Resolve #842 2023-02-19 09:02:16 +08:00
FooIbar b716a98548 Resolve #789 2023-02-19 08:04:40 +08:00
FooIbar 39f40ed906 fixup! Implement #817 (#821) 2023-02-19 08:04:08 +08:00
tarsin a644a1cee1 Fixup! Move tag database downloading to FileChannel based operation 2023-02-18 13:07:26 +08:00
tarsin 61fd8dc778 Fix transfer failed msg 2023-02-18 12:55:59 +08:00
tarsin 145d5e6302 Move tag database downloading to FileChannel based operation 2023-02-18 12:18:15 +08:00
tarsin ad071e48df cleanup: Remove AssertUtils 2023-02-18 12:02:36 +08:00
tarsin 28b5717f5b Rename .java to .kt 2023-02-18 12:02:36 +08:00
tarsin 41b818caa6 cleanup: Use jsoup xml escaper 2023-02-18 11:44:55 +08:00
tarsin cf87a654fb Fixup! Prepare for 1.8.7.0 dev 2023-02-18 10:48:29 +08:00
tarsin 9ffab097bb Prepare for 1.8.7.0 dev 2023-02-18 10:43:09 +08:00
renovate[bot] bdc3c2b6b5 Update dependency gradle to v8.0.1 2023-02-18 10:35:02 +08:00
tarsin 3f39662ba1 Fix quicksearch progress 2023-02-18 10:34:47 +08:00
tarsin aa485547a6 Fix a stupid memleak 2023-02-18 01:08:56 +08:00
tarsin 747f23bfe1 Resolve #831 2023-02-18 00:20:52 +08:00
tarsin d885841826 Revert "Resolve #831"
This reverts commit 0c457ef61d.
2023-02-18 00:16:55 +08:00
tarsin 0c457ef61d Resolve #831 2023-02-18 00:09:55 +08:00
tarsin 9462f1455b Remove unused res 2023-02-17 19:22:02 +08:00
tarsin c07325e71e Cleanup ReaderPageImageView 2023-02-17 19:15:54 +08:00
tarsin 5eda7b3fc3 Remove SubsamplingScaleImageView
We have no reason to keep it, just use PhotoView
2023-02-17 19:03:32 +08:00
tarsin 6b0c208b75 Proper way to define inner singleton class 2023-02-17 18:15:53 +08:00
FooIbar b0c6a26bea Fix search keyword in tag mode 2023-02-17 18:02:51 +08:00
tarsin 67a0893a97 Fix crash of metadata not available 2023-02-17 18:02:32 +08:00
tarsin d7438a26fb Get rid of PageLoader state, convert to readyOrDie 2023-02-17 15:18:09 +08:00
tarsin ee7ab38028 Dummy coroutine operation to work ArchivePageLoader 2023-02-17 15:13:44 +08:00
tarsin 8d999ea278 Get rid of state based gallery provider 2023-02-17 15:04:25 +08:00
tarsin 0851cf50a0 Rename .java to .kt 2023-02-17 15:04:25 +08:00
tarsin 0b6676aaa3 Preheat SpiderQueen in GalleryDetailScene, Resolve #818 2023-02-17 14:38:39 +08:00
tarsin c64167c76a Merge SpiderInfo Utils 2023-02-17 14:27:11 +08:00
tarsin 72a6419aff Reorganize SpiderQueen code 2023-02-17 14:21:24 +08:00
FooIbar 64e26a6b8b Fix comment last edited time parsing 2023-02-17 14:01:57 +08:00
tarsin 23184df1b9 Coroutinize SpiderQueen 2023-02-17 14:01:29 +08:00
tarsin dd123d7b87 Rename .java to .kt 2023-02-17 14:01:29 +08:00
tarsin 0e5d1e2325 Fixup some default values 2023-02-17 12:03:22 +08:00
tarsin 4939ce37c3 Implement #817 (#821)
* Rename .java to .kt

* Initial Workmanager migration

* Fix notification

* *

* Add timepicker

* Fix TimePicker layout

* Use 12H clock

* *

* Fixup
2023-02-17 11:39:42 +08:00
tarsin 6e7e668d39 Release 1.8.6.0
Releases / Build (push) Has been cancelled
2023-02-17 11:39:05 +08:00
tarsin 2d086a5c6e Resolve #820 2023-02-17 11:36:36 +08:00
renovate[bot] f7e592eac0 Update plugin com.android.application to v8.0.0-beta03 2023-02-17 10:39:32 +08:00
tarsin c13abafb09 Release 1.8.6.0-beta01
Releases / Build (push) Has been cancelled
2023-02-16 09:22:37 +08:00
tarsin eeed0720f7 Resolve #814 2023-02-16 09:21:14 +08:00
FooIbar f099b74aa6 fixup! Use .jpg instead of .jpeg for image/jpeg 2023-02-15 08:46:57 +08:00
FooIbar f7f54692a7 Resolve #806 2023-02-15 08:46:30 +08:00
FooIbar b97eff4284 Resolve #805 2023-02-15 08:46:30 +08:00
FooIbar f234dc61bc Singletonize DownloadManager 2023-02-15 02:18:57 +08:00
FooIbar 83b40d2ccb Fix #808 2023-02-15 02:17:56 +08:00
FooIbar a8d3d71ea9 Close navigation drawer when reselected 2023-02-15 02:17:23 +08:00
FooIbar 621772f359 Implement #646 2023-02-15 02:17:23 +08:00
tarsin 63cd60b237 Release 1.8.6.0-alpha04
Releases / Build (push) Has been cancelled
2023-02-14 16:40:49 +08:00
tarsin 0df8e8522f Fix #791 2023-02-14 16:39:10 +08:00
tarsin 1ebbf6e349 Release 1.8.6.0-alpha03
Releases / Build (push) Has been cancelled
2023-02-14 16:22:12 +08:00
tarsin 1b71f6a60a Fix issue that download progress being reset when entering reader 2023-02-14 16:20:38 +08:00
tarsin a3241e48df Release 1.8.6.0-alpha02
Releases / Build (push) Has been cancelled
2023-02-14 16:07:15 +08:00
FooIbar 69396f3d59 Use .jpg instead of .jpeg for image/jpeg
Revert "Resolve #795"

This reverts commit d6939b2913.
2023-02-14 16:05:07 +08:00
tarsin 9042d065ae Release 1.8.6.0-alpha01
Releases / Build (push) Has been cancelled
2023-02-14 16:01:54 +08:00
tarsin 300505e127 Allow 4 decode threads at same time 2023-02-14 15:47:57 +08:00
tarsin f7bf1d624b Only cancel decode job for recycled viewHolder 2023-02-14 15:45:55 +08:00
tarsin d7b08a0181 Readaround logic tweaks 2023-02-14 15:35:04 +08:00
tarsin 3941f59bbc Fix image decode 2023-02-14 14:58:02 +08:00
tarsin 802a30e16c Add a small delay between download and decode job launch 2023-02-14 14:07:21 +08:00
tarsin 032563d7a4 Should never update state by request 2023-02-14 14:05:58 +08:00
tarsin fe7ad2032b Minor code refactor 2023-02-14 13:53:04 +08:00
tarsin fcde4b3fc2 Minor cleanup 2023-02-14 13:46:51 +08:00
tarsin 1073ed6c24 Minor cleanup 2023-02-14 13:40:56 +08:00
tarsin e5ce3450e8 Coroutinize Decoder 2023-02-14 13:16:53 +08:00
tarsin a6f1e91747 Add error message support for reader, Resolve #788 2023-02-14 12:33:33 +08:00
tarsin 8dcf5a3654 Resolve #794 2023-02-14 12:04:02 +08:00
tarsin 22f9cf7c4c Tweak navigation logic 2023-02-14 11:46:22 +08:00
tarsin 38f2879d82 Minor code cleanup 2023-02-14 11:39:04 +08:00
renovate[bot] 4060695db6 Update dependency gradle to v8.0 2023-02-13 20:43:41 +00:00
tarsin 81fab695ff Default transfer block should be 8192
https://android.googlesource.com/platform/libcore/+/refs/heads/master/ojluni/src/main/java/sun/nio/ch/FileChannelImpl.java#707
2023-02-13 19:10:19 +08:00
tarsin 5259428106 Minor cleanup 2023-02-13 18:34:32 +08:00
tarsin 9774bd858f Remove KNI support
Not funny, reland it when we really need Kotlin Native
2023-02-13 18:28:46 +08:00
tarsin d30379251d Refactor Image source provider
Skia use SKMemoryStream to read file mapping data
SKMemoryStream always use memcpy to emulate posix read stream behaviour, cannot take advantages of file mapping reading

* https://github.com/google/skia/blob/1f193df9b393d50da39570dab77a0bb5d28ec8ef/src/core/SkStream.cpp#L335
2023-02-13 18:21:57 +08:00
tarsin 0e6156e51c Only do mapping based rewrite for archive reading, prepare for cleanup mmaped decode usage 2023-02-13 18:07:08 +08:00
tarsin d610c9727d Rewrite source when downloading image 2023-02-13 18:03:31 +08:00
tarsin adba04e2dd Revert image downloading to okhttp 2023-02-13 17:52:10 +08:00
tarsin b2aa6552b1 Revert "Ktor CIO Client"
This reverts commit 67c529ffe9.
2023-02-13 17:28:28 +08:00
tarsin 8b4b060425 Revert "Ktor CIO Client Proxy"
This reverts commit 4419ab154b.
2023-02-13 17:28:17 +08:00
tarsin 2f11853f0d Revert "Enable ktor cio http pipelining"
This reverts commit 6caec5dbce.
2023-02-13 17:28:08 +08:00
tarsin 388685b784 Revert "Remove unsupported proxy types"
This reverts commit be761071f8.
2023-02-13 17:27:07 +08:00
FooIbar d6939b2913 Resolve #795 2023-02-12 17:20:42 +08:00
FooIbar 90ee3b749f Avoid log spam 2023-02-12 17:20:42 +08:00
FooIbar e51c297a7d Call getImageLimits() in SettingsActivity's onCreate() 2023-02-12 01:40:21 +08:00
FooIbar 283e8f20b3 Show image limits in preference summary 2023-02-12 01:40:21 +08:00
tarsin 850c3fb8cd Upgrade compose compiler 2023-02-11 09:31:46 +08:00
FooIbar 7046e994d7 Use alpha to identity weak tags 2023-02-11 09:25:36 +08:00
FooIbar 099d802a03 Get cookies for image limits if site is Ex 2023-02-11 07:54:11 +08:00
FooIbar 453df64f69 Drop SignInRequiredPreference 2023-02-11 07:54:11 +08:00
FooIbar 8a2cb42546 Support reset image limits 2023-02-11 07:54:11 +08:00
FooIbar 6413ed67c6 Introduce HomeParser 2023-02-11 07:54:11 +08:00
tarsin 4dc7a97322 Refactor GalleryDetailParser 2023-02-10 19:37:17 +08:00
tarsin a2d8189c74 Refactor GalleryDetailParser 2023-02-10 19:16:05 +08:00
renovate[bot] f806c82c33 Update dependency gradle to v8.0-rc-5 2023-02-10 09:35:34 +08:00
renovate[bot] f46598cd74 Update dependency gradle to v8.0-rc-4 2023-02-10 08:37:35 +08:00
FooIbar 919d34c90d Refine GalleryCommentsScene spans usage
And wildcard imports are not allowed in Android style guide
https://developer.android.com/kotlin/style-guide#import_statements
2023-02-10 00:55:28 +08:00
FooIbar e230af8991 Fix uploader comment in disowned gallery 2023-02-10 00:55:28 +08:00
FooIbar 09842f5d72 Convert GalleryDetailParser to kotlin 2023-02-10 00:55:28 +08:00
FooIbar 911ff31f69 Rename .java to .kt 2023-02-10 00:55:28 +08:00
renovate[bot] f1c9c4e231 Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha07 2023-02-09 09:10:50 +08:00
renovate[bot] fa04ce5465 Update dependency androidx.browser:browser to v1.5.0 2023-02-09 09:10:39 +08:00
renovate[bot] 45ff03abfd Update dependency androidx.paging:paging-compose to v1.0.0-alpha18 2023-02-09 09:10:27 +08:00
renovate[bot] 20cebb8a05 Update dependency androidx.paging:paging-runtime-ktx to v3.2.0-alpha04 2023-02-09 09:09:23 +08:00
renovate[bot] f79e3ff51f Update dependency androidx.appcompat:appcompat to v1.6.1 2023-02-08 23:41:23 +00:00
FooIbar 45a67a7f3d Do not set navGraph until login is finished 2023-02-09 00:43:16 +08:00
tarsin 281469fe5c Fix wrong comment 2023-02-08 23:16:29 +08:00
tarsin 461ad7fa42 Better impl of runSuspendCatching 2023-02-08 22:38:18 +08:00
tarsin 899d221b67 Fix cancellation handle 2023-02-08 22:29:57 +08:00
tarsin b246e163d2 Introduce runCatchingCancellationTransparent 2023-02-08 22:26:45 +08:00
tarsin d83bb0f964 Default site is Ex, set it to e in tourist mode 2023-02-08 22:03:01 +08:00
tarsin a9626475e6 Refactor Image Fetcher with coroutine locks 2023-02-08 21:58:50 +08:00
FooIbar d1374eca15 Limit concurrency instead of parallelism 2023-02-08 20:36:16 +08:00
FooIbar 6289162684 Fix typo 2023-02-08 20:36:16 +08:00
tarsin 25b3187094 Add tr to supported locales 2023-02-08 09:35:03 +08:00
renovate[bot] bd2c9b172a Update plugin com.android.application to v8.0.0-beta02 2023-02-08 09:19:13 +08:00
renovate[bot] 246f953b4d Update dependency gradle to v8.0-rc-3 2023-02-08 09:19:02 +08:00
tarsin 433098a388 Use CompositionLocalProvider provide local navController 2023-02-08 09:05:22 +08:00
tarsin f442d9d030 Use LocalContext as Activity to execute finish 2023-02-08 08:55:55 +08:00
tarsin 61d7ae03f6 Remove ExperimentalMaterial3Api annotation usage 2023-02-08 08:50:37 +08:00
tarsin 22e144cb3b Use top-level func 2023-02-08 08:48:30 +08:00
tarsin a3f0d7092d Upgrade Coil 2023-02-08 08:43:06 +08:00
Kyoya 27efdba185 Translated using Weblate (Turkish)
Currently translated at 23.7% (148 of 623 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/tr/
2023-02-08 08:09:09 +08:00
Kyoya 59a1fa9992 Added translation using Weblate (Turkish) 2023-02-08 08:09:09 +08:00
FooIbar a5140ca293 Do not show SelectSiteScreen if sad panda 2023-02-08 08:08:21 +08:00
FooIbar a639b02890 Avoid unnecessary log dump 2023-02-08 08:08:21 +08:00
FooIbar ead64d3c73 Fix camelCase naming 2023-02-07 20:20:07 +08:00
FooIbar be761071f8 Remove unsupported proxy types 2023-02-07 20:20:07 +08:00
tarsin e48f19269f Do readaround, not readahead 2023-02-06 23:18:10 +08:00
FooIbar 82ebfb99c5 fixup! Tweak EhEngine transformException 2023-02-05 22:00:12 +08:00
tarsin 4d69bf2aba Introduce new Decoder scope 2023-02-05 14:32:31 +08:00
tarsin 4e4f77de90 Fix failed pages cannot retry 2023-02-05 14:23:38 +08:00
FooIbar fe0c3176d8 Tweak EhEngine transformException 2023-02-05 14:14:26 +08:00
FooIbar 8cfbdf46e0 Tweak disowned chip 2023-02-05 14:14:26 +08:00
tarsin 2234d056fc Fix download finished 2023-02-05 13:46:01 +08:00
tarsin 4a4c447bd2 Fix download 2023-02-05 13:16:53 +08:00
tarsin 933a95023c Tweak readahead logic 2023-02-05 12:31:30 +08:00
tarsin 1d8b5f81a1 Set stroke color for category and uploader chip 2023-02-05 11:41:02 +08:00
FooIbar 81df632e48 Set background color for category and uploader chip 2023-02-04 23:01:51 +08:00
FooIbar b0df7cbc68 Revert "Resolve #756"
This reverts commit 01d8dabef4.
2023-02-04 23:01:51 +08:00
tarsin 25d90a3c78 Rewrite Image fetching with kotlin coroutines 2023-02-04 22:59:35 +08:00
tarsin 384637efdf Fix typo 2023-02-04 20:32:20 +08:00
tarsin ef3d3e6443 Introduce new SpiderQueenWorker stub 2023-02-04 20:31:56 +08:00
tarsin 675e98fa1a Add Ktor to Credits 2023-02-04 19:32:09 +08:00
tarsin 0b059c71c1 Revert "Tweak disowned chip"
This reverts commit f59dccd5fd.
2023-02-04 18:05:04 +08:00
lijia 8988f08243 feat: apply advanceSearch through rawKeyword | key1:value1 | key2:value2 ... in keyword 2023-02-04 17:28:11 +08:00
FooIbar adb54d9d7c Reset read button text if startPage is 0 2023-02-04 17:11:01 +08:00
FooIbar fdf58388a3 Fix typos 2023-02-04 17:11:01 +08:00
tarsin a5244cdb41 Fix crash 2023-02-04 14:54:17 +08:00
tarsin 6caec5dbce Enable ktor cio http pipelining 2023-02-04 14:15:52 +08:00
tarsin 4419ab154b Ktor CIO Client Proxy 2023-02-04 13:49:22 +08:00
tarsin 67c529ffe9 Ktor CIO Client 2023-02-04 13:36:29 +08:00
tarsin 057ba0c6e1 Migrate image downloading to ktor 2023-02-04 12:20:41 +08:00
tarsin 6117ea755f Rename .java to .kt 2023-02-04 12:20:41 +08:00
tarsin a56c923f0e Drop cache key manage utils 2023-02-04 10:43:28 +08:00
tarsin 3686241bf5 Rename .java to .kt 2023-02-04 10:43:28 +08:00
tarsin 0311c6f52b Fix parcelable too large 2023-02-04 10:28:22 +08:00
tarsin f59dccd5fd Tweak disowned chip 2023-02-04 09:49:06 +08:00
tarsin 01d8dabef4 Resolve #756 2023-02-04 09:47:16 +08:00
tarsin 773993c6f2 Introduce ktor http client with okhttp backend 2023-02-04 09:38:40 +08:00
tarsin 2c5646b1c2 Convert all buildscript to kts 2023-02-03 11:01:32 +08:00
tarsin b8d71e24e7 Enable v3 & v4 signing 2023-02-03 10:34:28 +08:00
tarsin 4db3f7a742 Convert buildscript to kts 2023-02-03 10:21:42 +08:00
tarsin 298203f909 Respect platform's default malloc impl 2023-02-03 09:28:03 +08:00
tarsin a1cafe62cf Fix typo 2023-02-03 08:48:53 +08:00
tarsin cab75ef0d6 Kotlin 1.8.10 2023-02-03 08:48:09 +08:00
FooIbar 33a8d86684 Refine EhTagDatabase (#750)
* Refine EhTagDatabase

* Fix warnings
2023-02-02 22:44:09 +08:00
tarsin b4d329725b Sync minsdk with app 2023-02-02 20:20:29 +08:00
tarsin 6efe8922b3 Convert munmap operation to JNI 2023-02-02 20:15:05 +08:00
tarsin f5bd097584 Refine mmap failure log 2023-02-02 19:57:32 +08:00
tarsin 7492841b5f Check Long to size_t conversion 2023-02-02 19:47:16 +08:00
tarsin 1430bd9e88 Rewrite mmap operations with KNI 2023-02-02 19:37:49 +08:00
tarsin e0400305ab Don't cache NativeLink task 2023-02-02 17:33:53 +08:00
tehcneko 65d5120272 Increase comment edittext max height (#749) 2023-02-02 17:11:33 +08:00
tehcneko cbb398e58c Fix underline support in comments (#748)
Fix underline support
2023-02-02 16:46:48 +08:00
tehcneko 57d8735624 Add WYSIWYG editor (#747) 2023-02-02 16:29:36 +08:00
tarsin fd99bd2ed1 KNI Stub (#746)
* Revert "Revert "Kotlin Native Jni Stub""

This reverts commit 46c89534fd.

* Work KNI build script

* Load Kni library as well

* Fix lib name

* Setup Kotlin Compiler

* Setup Kotlin Compiler

* Setup Kotlin Compiler

* Fix kotlin native compiler not available

* Fix
2023-02-02 16:21:52 +08:00
tarsin 46c89534fd Revert "Kotlin Native Jni Stub"
This reverts commit 5650ee580f.
2023-02-02 12:50:15 +08:00
tarsin 5650ee580f Kotlin Native Jni Stub 2023-02-02 12:08:28 +08:00
tarsin 62f126b5d9 Rewrite ArchiveParser with kotlin regex interface (#745)
* Upgrade NDK

* Fixup!
2023-02-02 09:50:57 +08:00
tarsin 51cb354f35 Upgrade NDK 2023-02-01 18:21:13 +08:00
tarsin d4f591354d Fix warning 2023-02-01 15:20:43 +08:00
tarsin 0895af0ecc Fix deprecated funcs
Only fix kotlin code, because JAVA SUCKS
2023-02-01 15:17:35 +08:00
tarsin fafe25fee4 Fix deprecated funcs
Only fix kotlin code, because JAVA SUCKS
2023-02-01 15:07:13 +08:00
tarsin 3db6cc1a1c Emit log spams 2023-02-01 12:02:26 +08:00
tarsin 408f5a05fc Add issuetracker comments 2023-02-01 11:53:00 +08:00
tarsin 28ee23c5f3 Fix serialization proguard 2023-02-01 11:50:10 +08:00
tarsin 37804e67c1 Fix serialization 2023-02-01 11:28:24 +08:00
tarsin a7bce9c8e9 Disable okhttp cache, Resolve #743 2023-02-01 09:02:32 +08:00
tarsin bcf2d16a10 Fixup! Adjust webtoon viewer image default height 2023-01-31 12:41:10 +08:00
tarsin 28d2b92abb Fix NPE caused by compose components compatibility when click TextField
java.lang.NullPointerException
        at androidx.compose.ui.node.OnPositionedDispatcher.androidx.compose.ui.node.LayoutNode.dispatchOnPositionedCallbacks$ui_release(Unknown Source:46)
                                                    dispatchHierarchy
        at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(Unknown Source:89)
        at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(Unknown Source:89)
        at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(Unknown Source:89)
        at androidx.compose.ui.node.MeasureAndLayoutDelegate.androidx.compose.ui.node.OnPositionedDispatcher.dispatch(Unknown Source:61)
                                                      dispatchOnPositionedCallbacks
        at androidx.compose.ui.platform.AndroidComposeView.updatePositionCacheAndDispatch(Unknown Source:53)
        at androidx.compose.ui.platform.AndroidComposeView.onLayout(Unknown Source:10)
        at android.view.View.layout(View.java:23700)
        at android.view.ViewGroup.layout(ViewGroup.java:6413)
        at androidx.compose.ui.platform.AbstractComposeView.internalOnLayout$ui_release(Unknown Source:27)
        at androidx.compose.ui.platform.AbstractComposeView.onLayout(Unknown Source:0)
        at android.view.View.layout(View.java:23700)
        at android.view.ViewGroup.layout(ViewGroup.java:6413)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at android.view.View.layout(View.java:23700)
        at android.view.ViewGroup.layout(ViewGroup.java:6413)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1891)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1729)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1638)
        at android.view.View.layout(View.java:23700)
        at android.view.ViewGroup.layout(ViewGroup.java:6413)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at android.view.View.layout(View.java:23700)
        at android.view.ViewGroup.layout(ViewGroup.java:6413)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1891)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1729)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1638)
        at android.view.View.layout(View.java:23700)
        at android.view.ViewGroup.layout(ViewGroup.java:6413)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at com.android.internal.policy.DecorView.onLayout(DecorView.java:799)
        at android.view.View.layout(View.java:23700)
        at android.view.ViewGroup.layout(ViewGroup.java:6413)
        at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3999)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3372)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2328)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9084)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1231)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1239)
        at android.view.Choreographer.doCallbacks(Choreographer.java:899)
        at android.view.Choreographer.doFrame(Choreographer.java:832)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1214)
        at android.os.Handler.handleCallback(Handler.java:942)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7872)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
2023-01-31 10:31:47 +08:00
tarsin 2f3b6df529 Support manually refresh page 2023-01-31 10:01:28 +08:00
tarsin fa2cbbb0cc Revert "Remove paging-recyclerview dependency"
This reverts commit ccb48505bf.
2023-01-31 09:50:35 +08:00
tarsin d1ab9e77de Revert "Move to new HistoryScreen"
This reverts commit b1cda242df.
2023-01-31 09:49:32 +08:00
tarsin 3484261b50 Adjust webtoon viewer image default height
As suggested by @yinzhefuyun, the default height should be width * 1.5, fix it!
2023-01-31 09:47:50 +08:00
tarsin 7a03fb53ea ReImplement imageSearch max size restrict 2023-01-30 15:06:22 +08:00
tarsin d7f050efc4 Remove sucking streampipe totally 2023-01-30 14:55:54 +08:00
tarsin 2d11a29a79 Convert another BitmapFactory usage to ImageDecoder 2023-01-30 14:52:57 +08:00
tarsin 0b5aecfdb5 Convert a BitmapFactory usage to ImageDecoder 2023-01-30 14:43:12 +08:00
tarsin 487925b1f2 Refactor SpiderDen 2023-01-30 14:32:43 +08:00
tarsin 54c8183666 Add dummy cancellation support 2023-01-30 13:23:30 +08:00
tarsin c878952237 Cleanup unused code 2023-01-30 13:16:32 +08:00
tarsin 26d6428836 Opt-in all Experimental Api 2023-01-30 13:11:59 +08:00
tarsin cf31a31430 Refactor SpiderDen 2023-01-30 13:08:10 +08:00
tarsin e10e1c665f Refactor SpiderDen 2023-01-30 11:48:07 +08:00
tarsin 0834ff26be Convert SpiderDen to kotlin 2023-01-30 11:11:57 +08:00
tarsin 014b8db20d Rename .java to .kt 2023-01-30 11:11:57 +08:00
tarsin 34445e6f92 Move spiderInfo compat to separate file 2023-01-30 10:58:05 +08:00
tarsin 7d00e01e00 Avoid unnecessary thread create 2023-01-30 10:46:23 +08:00
tarsin 300fabca21 Avoid unnecessary diskcache edit 2023-01-30 10:33:31 +08:00
tarsin e17dc78aeb Handle cache internally 2023-01-29 12:38:44 +08:00
tarsin 1b9e00efe1 Fix a NPE 2023-01-29 12:20:07 +08:00
tarsin 152b4d3765 Fixup! New SpiderInfo serialization format
Don't serialize thousands of nulls
2023-01-29 11:36:29 +08:00
tarsin e0b95ab2ae Resolve #737 2023-01-29 10:13:50 +08:00
tarsin e0520edccc Resolve #735 2023-01-29 10:11:00 +08:00
tarsin 581b32e8a2 No need to use data class 2023-01-28 17:14:14 +08:00
tarsin e71d61ef37 Cleanup JvmStatic usage 2023-01-28 17:11:42 +08:00
tarsin 0a51d48703 Improve immutability 2023-01-28 17:04:49 +08:00
tarsin 30c7f9e020 New SpiderInfo serialization format 2023-01-28 15:58:45 +08:00
tarsin 71f7771a97 Rename .java to .kt 2023-01-28 15:58:45 +08:00
tarsin b36e5b7906 Resolve #734 2023-01-27 18:07:46 +08:00
FooIbar bb25449b35 Hide bypass VPN when domain fronting is disabled 2023-01-27 16:58:53 +08:00
tarsin dc8053dfcd No need to cache the pagingflow 2023-01-27 10:33:53 +08:00
tarsin 20be8e23ba Add lazylist item delete & add animation bugtracker 2023-01-27 10:04:05 +08:00
tarsin d83f476439 Fixup! Make LongList, IntList actually a parcelable mutableListOf() 2023-01-27 09:29:12 +08:00
renovate[bot] 29352b8838 Update dependency androidx.compose.material3:material3 to v1.1.0-alpha05 2023-01-27 09:06:03 +08:00
FooIbar c0390c3c5d Resolve #728 2023-01-26 22:00:55 +08:00
FooIbar 81dd2eeb47 fixup! Support filter comments
Resolve #726
2023-01-26 21:05:55 +08:00
tarsin 3beedf4dfe Make LongList, IntList actually a parcelable mutableListOf() 2023-01-26 18:56:48 +08:00
tarsin f5be71ecbd Rename .java to .kt 2023-01-26 18:56:47 +08:00
tarsin 58ba5dfedc Convert CommonOperations to kotlin 2023-01-26 18:36:25 +08:00
tarsin 040d265f3c Rename .java to .kt 2023-01-26 18:36:25 +08:00
tarsin fccf4db163 Migrate to androidx collection, step 3 2023-01-26 17:56:05 +08:00
tarsin 1122498b54 Migrate to androidx collection, step 2 2023-01-26 17:51:32 +08:00
tarsin 6a98aea209 Explicitly depend latest collections for kotlin compatibility 2023-01-26 17:48:25 +08:00
tarsin ae4d589c8e Move to androidx collection LongSparseArray 2023-01-26 17:46:37 +08:00
tarsin 6ccb59957d Revert "Update README.md"
Hello Tarsin,

Thank you for your request, but I can’t approve it. Sexually explicit material is not permitted on Hosted Weblate, including Hentai.

Kind regards,
This reverts commit 5dc9b83831.
2023-01-26 11:40:24 +08:00
renovate[bot] edd4e865b3 Update plugin com.google.devtools.ksp to v1.8.0-1.0.9 2023-01-26 11:08:59 +08:00
tarsin 6ff7614e94 Implement compose version FavSelector 2023-01-26 10:48:14 +08:00
FooIbar 065a14952a Supports page turning when holding volume keys 2023-01-26 08:44:14 +08:00
renovate[bot] 1e620931a2 Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha06 2023-01-25 22:25:45 +00:00
tarsin a46a531a89 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (615 of 615 strings)

Translation: Ehviewer-Overhauled/Android strings
Translate-URL: https://hosted.weblate.org/projects/ehviewer-overhauled/android-strings/zh_Hans/
2023-01-25 15:29:05 +08:00
tarsin 06c95b8d36 Tweak galleryInfoCard compose caching 2023-01-25 10:55:21 +08:00
tarsin 39967a9211 Implement remove download 2023-01-25 10:35:59 +08:00
tarsin a18e6f4b63 Fixup! Implement remove favourite 2023-01-25 10:17:42 +08:00
tarsin ef475000c3 Implement remove favourite 2023-01-25 10:14:13 +08:00
renovate[bot] f08a2444be Update dependency com.google.android.material:material to v1.8.0 2023-01-25 08:19:44 +08:00
FooIbar c746d58210 Refactor move download (#721)
* Rename .java to .kt

* Convert DownloadManager to kotlin

* Refactor move download
2023-01-25 00:41:12 +08:00
tarsin ccb48505bf Remove paging-recyclerview dependency 2023-01-24 18:53:01 +08:00
txyyh c9ddd25e99 Use fork in Action 2023-01-24 02:16:16 +00:00
tarsin 42234d9163 Fioxup! Move GalleryList card longclick dialog to separate file 2023-01-24 09:55:17 +08:00
tarsin e058885c7c Move GalleryList card longclick dialog to separate file 2023-01-24 09:51:21 +08:00
tarsin 1e3556cc8e Tweak swipeToDismiss logic 2023-01-24 08:58:48 +08:00
tarsin b1cda242df Move to new HistoryScreen 2023-01-23 19:51:52 +08:00
tarsin b593fce745 Fixup! Move some widget to separate file 2023-01-23 19:27:56 +08:00
tarsin 282cdeaad0 Move some widget to separate file 2023-01-23 19:24:15 +08:00
tarsin 6e54c7d672 Add background layer for HistoryScreen item swipeToDismiss 2023-01-23 18:51:25 +08:00
tarsin 51a12ee5cb Fixup HistoryScreen item swipeToDismiss 2023-01-23 18:47:04 +08:00
tarsin fcea40685c HistoryScreen item navToReader 2023-01-23 18:36:37 +08:00
tarsin 07450a7a51 HistoryScreen item SwipeToDismiss 2023-01-23 18:34:10 +08:00
tarsin 19fa8d15a6 HistoryScreen item longclick layout skeleton 2023-01-23 17:58:47 +08:00
FooIbar db2980f598 Call popBackStack() before navigating to GalleryDetailScene 2023-01-23 15:50:10 +08:00
tarsin b3e4fd65aa HistoryScreen item longclick impl 2023-01-23 15:44:09 +08:00
tarsin d46eca750c HistoryScreen Appbar liftonScroll 2023-01-23 14:25:20 +08:00
tarsin d441e92adc Refine InfoCard 2023-01-23 14:19:08 +08:00
tarsin 5f70d71203 Refine InfoCard 2023-01-23 14:15:06 +08:00
tarsin 59ffb2ea8e Fix ComposeSimpleRatingView 2023-01-23 14:00:17 +08:00
tarsin ce59b8037f Implement ComposeSimpleRatingView 2023-01-23 13:56:18 +08:00
tarsin 56e67ba70a Fix download icon 2023-01-23 13:24:03 +08:00
tarsin 108e9e216e Fix category text 2023-01-23 13:15:26 +08:00
tarsin b219b4e832 HistoryScreen Card Impl 2023-01-23 12:48:45 +08:00
tarsin ef4e0d76ec History Screen clear all dialog 2023-01-23 10:51:41 +08:00
tarsin 61c75c06a9 History Screen no item tips 2023-01-23 10:36:44 +08:00
tarsin 091705b3f2 History Screen lazy items 2023-01-23 10:27:02 +08:00
tarsin 6aa5b06619 History Screen TopAppBar Layout 2023-01-23 10:06:46 +08:00
tarsin df8fc0b229 Introduce HistoryComposeScreenFragmentBridge 2023-01-23 09:49:23 +08:00
515 changed files with 18455 additions and 28877 deletions

No files matched your search

+13 -2
View File
@@ -18,7 +18,9 @@ body:
label: 复现步骤 / Step to reproduce
description: |
请从全新安装应用开始,在此处写下复现的方式。请尽量附带录屏
如果不能稳定复现,请在下方上传应用日志
Describe how to reproduce here with fresh installation, with screen recording if possible.
If you can't reproduce it consistently, please upload app logs below
placeholder: |
0. 重新安装应用 / Fresh install
1.
@@ -55,6 +57,15 @@ body:
validations:
required: true
- type: textarea
id: log
attributes:
label: 应用日志 / App logs
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
placeholder: 06-15 17:44:53.704 23382 23382 E ...
validations:
required: false
- type: textarea
id: more
attributes:
@@ -81,8 +92,8 @@ body:
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
description: 位于设置 - 关于中的版本号后 / Next to the version code in Settings - About
placeholder: 26f05a0
validations:
required: true
+2 -2
View File
@@ -65,8 +65,8 @@ body:
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
description: 位于设置 - 关于中的版本号后 / Next to the version code in Settings - About
placeholder: 26f05a0
validations:
required: true
+6 -3
View File
@@ -17,15 +17,18 @@ jobs:
with:
submodules: 'recursive'
- name: Setup Java 19
- name: Setup Java 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 19
java-version: 17
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Ktlint Check
run: ./gradlew ktlintCheck
- name: Gradle Build
uses: gradle/gradle-build-action@v2
+6 -3
View File
@@ -12,15 +12,18 @@ jobs:
with:
submodules: 'recursive'
- name: Setup Java 19
- name: Setup Java 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 19
java-version: 17
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Ktlint Check
run: ./gradlew ktlintCheck
- name: Gradle Build
uses: gradle/gradle-build-action@v2
+5 -4
View File
@@ -15,15 +15,15 @@ jobs:
with:
submodules: 'recursive'
- name: Setup Java 19
- name: Setup Java 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 19
java-version: 17
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.2.9519653" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
@@ -58,7 +58,7 @@ jobs:
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
- name: Push Apks
uses: cpina/github-action-push-to-another-repository@v1.5
uses: Ehviewer-Overhauled/github-action-push-to-another-repository@main
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
@@ -68,5 +68,6 @@ jobs:
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
-6
View File
@@ -1,9 +1,3 @@
[submodule "app/src/main/cpp/libarchive"]
path = app/src/main/cpp/libarchive
url = https://github.com/Ehviewer-Overhauled/libarchive
[submodule "app/src/main/cpp/liblzma"]
path = app/src/main/cpp/liblzma
url = https://github.com/Ehviewer-Overhauled/xz
[submodule "app/src/main/cpp/nettle/nettle"]
path = app/src/main/cpp/nettle/nettle
url = https://github.com/Ehviewer-Overhauled/nettle
+16 -18
View File
@@ -1,10 +1,10 @@
<p align="right">
<strong>English<strong>
<strong>English</strong>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
简体中文
</a>
<span> | <span>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
正體中文
</a>
@@ -25,6 +25,9 @@
<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>
<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>
<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>
@@ -57,44 +60,39 @@
</h3>
</div>
# Description
EhViewer fork dedicated to lightweight and high-performance
with Material Design 3 and Dynamic Color Support
with [Material Design 3](https://m3.material.io/)
and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview) Support
# 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="Git it on F-Droid" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
<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>
# Screenshot
![screenshot-01](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-01.png)
![screenshot-02](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-02.png)
# Localization
Thank you to each of the **Ehviewer Overhauled** translators, and if you would like to contribute, please submit a translation via [Weblate](https://hosted.weblate.org/engage/ehviewer-overhauled/).
[<img src="https://hosted.weblate.org/widgets/ehviewer-overhauled/-/horizontal-auto.svg" alt="" />](https://hosted.weblate.org/engage/ehviewer-overhauled/)
# Thanks
Here is the libraries
- [Arrow](https://arrow-kt.io/)
- [AOSP & AndroidX](http://source.android.com/)
- [Coil](https://coil-kt.github.io/coil/)
- [Kotlin & KotlinX](https://kotlinlang.org/)
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
- [GnuTLS](https://gnutls.org/)
- [material-components-android](https://github.com/material-components/material-components-android)
- [MCA](https://github.com/material-components/material-components-android)
- [material-design-icons](https://github.com/google/material-design-icons)
- [jsoup](https://github.com/jhy/jsoup)
- [okhttp](https://github.com/square/okhttp)
- [Okhttp](https://square.github.io/okhttp/)
- [RikkaX](https://github.com/RikkaApps/RikkaX)
- [libarchive](http://www.libarchive.org/)
- [XZ Utils](https://tukaani.org/xz/)
- [Libarchive](http://www.libarchive.org/)
- [Coil](https://coil-kt.github.io/coil/)
- [Jsoup](https://jsoup.org/)
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
# License
-169
View File
@@ -1,169 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
id 'com.google.devtools.ksp'
}
android {
compileSdkVersion 33
buildToolsVersion "33.0.1"
ndkVersion "25.1.8937393"
splits {
abi {
enable = true
reset()
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
universalApk = true
}
}
signingConfigs {
sign {
storeFile file(signStoreFile)
storePassword signStorePassword
keyAlias signkeyAlias
keyPassword signkeyPassword
}
}
defaultConfig {
applicationId "moe.tarsin.ehviewer"
minSdkVersion 28
targetSdkVersion 33
versionCode 180031
versionName "1.8.5.3"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th', 'nb-rNO'
}
externalNativeBuild {
cmake {
path 'src/main/cpp/CMakeLists.txt'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_19
targetCompatibility JavaVersion.VERSION_19
}
kotlinOptions {
jvmTarget = '19'
freeCompilerArgs += [
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive"
]
}
lint {
abortOnError true
checkReleaseBuilds false
disable 'MissingTranslation'
}
packagingOptions {
resources {
excludes += "/META-INF/CHANGES"
excludes += "/META-INF/README.md"
excludes += "/kotlin/**"
excludes += "**.txt"
excludes += "**.bin"
}
}
dependenciesInfo.includeInApk false
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles 'proguard-rules.pro'
signingConfig signingConfigs.sign
}
debug {
applicationIdSuffix ".debug"
signingConfig signingConfigs.debug
}
}
buildFeatures {
buildConfig true
compose true
viewBinding true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0"
}
namespace 'com.hippo.ehviewer'
}
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation "androidx.activity:activity-compose:1.6.1"
implementation "androidx.appcompat:appcompat:1.6.0"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
implementation "androidx.browser:browser:1.4.0"
// https://developer.android.com/jetpack/androidx/releases/compose-material3
implementation platform("androidx.compose:compose-bom:2023.01.00")
implementation "androidx.compose.material:material-icons-extended"
implementation "androidx.compose.material3:material3"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha05"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation "androidx.lifecycle:lifecycle-process:2.5.1"
// https://developer.android.com/jetpack/androidx/releases/navigation
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-compose:$nav_version"
// https://developer.android.com/jetpack/androidx/releases/paging
implementation "androidx.paging:paging-runtime-ktx:3.2.0-alpha03"
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
// https://developer.android.com/jetpack/androidx/releases/room
def room_version = "2.5.0"
ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
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
implementation "com.google.accompanist:accompanist-themeadapter-material3:0.28.0"
implementation 'com.google.android.material:material:1.8.0-rc01'
// https://square.github.io/okhttp/changelogs/changelog/
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp-coroutines'
implementation 'com.squareup.okio:okio-jvm:3.3.0'
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.2.0'
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
// https://coil-kt.github.io/coil/changelog/
// implementation 'io.coil-kt:coil:2.2.2'
implementation 'com.github.Ehviewer-Overhauled.coil:coil:d65881d'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jsoup:jsoup:1.15.3'
}
configurations.all {
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
exclude group: 'dev.rikka.rikkax.material', module: 'material'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-android-extensions-runtime'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
}
+243
View File
@@ -0,0 +1,243 @@
import java.io.ByteArrayOutputStream
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
plugins {
id("com.android.application")
kotlin("android")
id("kotlin-parcelize")
kotlin("plugin.serialization")
id("com.google.devtools.ksp")
}
android {
compileSdk = 33
buildToolsVersion = "33.0.2"
ndkVersion = "25.2.9519653"
androidResources.generateLocaleConfig = true
splits {
abi {
isEnable = true
reset()
if (gradle.startParameter.taskNames.any { it.contains("Release") }) {
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
isUniversalApk = true
} else {
include("arm64-v8a")
}
}
}
val signConfig = signingConfigs.create("release") {
storeFile = File(projectDir.path + "/keystore/androidkey.jks")
storePassword = "000000"
keyAlias = "key0"
keyPassword = "000000"
enableV3Signing = true
enableV4Signing = true
}
val commitSha by lazy {
val stdout = ByteArrayOutputStream()
exec {
commandLine = "git rev-parse --short=7 HEAD".split(' ')
standardOutput = stdout
}
stdout.toString().trim()
}
val buildTime by lazy {
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
formatter.format(Instant.now())
}
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
targetSdk = 33
versionCode = 180042
versionName = "1.8.8.3"
resourceConfigurations.addAll(
listOf(
"zh",
"zh-rCN",
"zh-rHK",
"zh-rTW",
"es",
"ja",
"ko",
"fr",
"de",
"th",
"tr",
"nb-rNO",
),
)
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
}
externalNativeBuild {
cmake {
path = File("src/main/cpp/CMakeLists.txt")
}
}
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",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.paging.ExperimentalPagingApi",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
lint {
abortOnError = true
checkReleaseBuilds = false
disable.add("MissingTranslation")
}
packaging {
resources {
excludes += "/META-INF/**"
excludes += "/kotlin/**"
excludes += "**.txt"
excludes += "**.bin"
}
}
dependenciesInfo.includeInApk = false
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles("proguard-rules.pro")
signingConfig = signConfig
}
debug {
applicationIdSuffix = ".debug"
}
}
buildFeatures {
buildConfig = true
compose = true
viewBinding = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.7"
}
namespace = "com.hippo.ehviewer"
}
dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity
implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
implementation("androidx.browser:browser:1.5.0")
implementation("androidx.collection:collection-ktx:1.3.0-alpha04")
// 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.04.00-beta01"))
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material3:material3-window-size-class")
implementation("androidx.core:core-ktx:1.11.0-beta01")
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha10")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.drawerlayout:drawerlayout:1.2.0")
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation("androidx.lifecycle:lifecycle-process:2.6.1")
// https://developer.android.com/jetpack/androidx/releases/navigation
val nav_version = "2.5.3"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
implementation("androidx.navigation:navigation-compose:$nav_version")
// https://developer.android.com/jetpack/androidx/releases/paging
implementation("androidx.paging:paging-common-ktx:3.2.0-alpha06")
implementation("androidx.paging:paging-compose:1.0.0-alpha20")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.recyclerview:recyclerview:1.3.0")
// https://developer.android.com/jetpack/androidx/releases/room
val room_version = "2.6.0-alpha01"
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-paging:$room_version")
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
implementation("com.github.nanihadesuka:LazyColumnScrollbar:1.6.3")
// https://github.com/google/accompanist/releases
implementation("com.google.accompanist:accompanist-themeadapter-material3:0.30.1")
implementation("com.google.android.material:material:1.9.0")
// https://square.github.io/okhttp/changelogs/changelog/
implementation(platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11"))
implementation("com.squareup.okhttp3:okhttp-coroutines")
implementation("com.squareup.okio:okio-jvm:3.3.0")
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("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3")
implementation("dev.rikka.rikkax.material:material-preference:2.0.0")
implementation(platform("io.arrow-kt:arrow-stack:1.2.0-RC"))
implementation("io.arrow-kt: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("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.1")
implementation("org.jsoup:jsoup:1.16.1")
}
configurations.all {
exclude("dev.rikka.rikkax.appcompat", "appcompat")
exclude("dev.rikka.rikkax.material", "material")
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.generateKotlin", "true")
}
+5
View File
@@ -6,6 +6,11 @@
native <methods>;
}
# https://issuetracker.google.com/issues/190382641
-keepclassmembers class kotlin.SafePublicationLazyImpl {
java.lang.Object _value;
}
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
-keepattributes LineNumberTable,SourceFile
@@ -0,0 +1,88 @@
{
"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')"
]
}
}
@@ -0,0 +1,88 @@
{
"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')"
]
}
}
+1 -2
View File
@@ -27,7 +27,6 @@
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
@@ -36,7 +35,7 @@
<activity
android:name=".ui.login.LoginActivity"
android:exported="false"
android:theme="@style/AppTheme"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".ui.MainActivity"
@@ -0,0 +1,34 @@
From fc9fb0ea245259e346ab46c334f03b557fe63b6b Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Tue, 22 Nov 2022 12:37:12 +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 01c73cd..b461e68 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -592,7 +592,7 @@ check_symbol_exists(getopt_long getopt.h HAVE_GETOPT_LONG)
# xzdec
#############################################################################
-if(HAVE_GETOPT_LONG)
+if(0)
add_executable(xzdec
src/common/sysdefs.h
src/common/tuklib_common.h
@@ -630,7 +630,7 @@ endif()
# xz
#############################################################################
-if(NOT MSVC AND HAVE_GETOPT_LONG)
+if(0)
add_executable(xz
src/common/mythread.h
src/common/sysdefs.h
--
2.39.2
@@ -0,0 +1,38 @@
From 2a67861e6f06794f3671c4988bac12330d8422e3 Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Fri, 27 May 2022 23:21:20 +0800
Subject: [PATCH 1/2] Don't build shared lib
---
libarchive/CMakeLists.txt | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/libarchive/CMakeLists.txt b/libarchive/CMakeLists.txt
index ff7ade00..00c2e170 100644
--- a/libarchive/CMakeLists.txt
+++ b/libarchive/CMakeLists.txt
@@ -242,12 +242,6 @@ ELSEIF(ARCHIVE_ACL_SUNOS)
LIST(APPEND libarchive_SOURCES archive_disk_acl_sunos.c)
ENDIF()
-# Libarchive is a shared library
-ADD_LIBRARY(archive SHARED ${libarchive_SOURCES} ${include_HEADERS})
-TARGET_INCLUDE_DIRECTORIES(archive PUBLIC .)
-TARGET_LINK_LIBRARIES(archive ${ADDITIONAL_LIBS})
-SET_TARGET_PROPERTIES(archive PROPERTIES SOVERSION ${SOVERSION})
-
# archive_static is a static library
ADD_LIBRARY(archive_static STATIC ${libarchive_SOURCES} ${include_HEADERS})
TARGET_LINK_LIBRARIES(archive_static ${ADDITIONAL_LIBS})
@@ -260,7 +254,7 @@ ENDIF(NOT WIN32 OR CYGWIN)
IF(ENABLE_INSTALL)
# How to install the libraries
- INSTALL(TARGETS archive archive_static
+ INSTALL(TARGETS archive_static
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
--
2.39.2
@@ -0,0 +1,25 @@
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
+30 -13
View File
@@ -1,7 +1,9 @@
project(ehviewer)
cmake_minimum_required(VERSION 3.4.1)
cmake_minimum_required(VERSION 3.14)
project(ehviewer C ASM)
include(FetchContent)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -funroll-loops -flto=thin \
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto=thin \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
@@ -12,6 +14,20 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inli
-mllvm -polly-vectorizer=stripmine \
-mllvm -polly-detect-profitability-min-per-loop-insts=40 \
-mllvm -polly-invariant-load-hoisting")
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
# Change the default path of fetch content to avoid downloading the same dependencies per architecture
get_filename_component(deps "../_deps" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
set(FETCHCONTENT_BASE_DIR ${deps})
FetchContent_Declare(liblzma
GIT_REPOSITORY https://git.tukaani.org/xz.git
GIT_TAG v5.4.1
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binarys.patch || cd .
)
FetchContent_MakeAvailable(liblzma)
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
# Build GNUTLS libnettle
add_subdirectory(nettle)
@@ -27,11 +43,7 @@ set(HAVE_NETTLE_PBKDF2_H 1)
set(HAVE_NETTLE_RIPEMD160_H 1)
set(HAVE_NETTLE_SHA_H 1)
# Build liblzma
add_subdirectory(liblzma)
# Configure lzma support for libarchive
include_directories(liblzma/src/liblzma/api)
SET(HAVE_LIBLZMA 1)
SET(HAVE_LZMA_H 1)
SET(HAVE_LZMA_STREAM_ENCODER_MT 1)
@@ -39,12 +51,17 @@ SET(HAVE_LZMADEC_H 1)
SET(HAVE_LIBLZMADEC 1)
# Configure libarchive link's static lib
SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
# Build libarchive
add_subdirectory(libarchive)
FetchContent_Declare(libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive
GIT_TAG v3.6.2
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-shared-lib.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || cd .
)
FetchContent_MakeAvailable(libarchive)
include_directories(${libarchive_SOURCE_DIR}/libarchive)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c)
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
add_library(${PROJECT_NAME} SHARED archive.c gifutils.c natsort/strnatcmp.c)
target_link_libraries(${PROJECT_NAME} archive_static log)
+122 -55
View File
@@ -17,14 +17,12 @@
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <syscall.h>
#include <jni.h>
#include <android/log.h>
@@ -34,7 +32,9 @@
#define LOG_TAG "libarchive_wrapper"
#include "natsort/strnatcmp.h"
#include "ehviewer.h"
#include "spinlock.h"
typedef struct {
int using;
@@ -46,12 +46,25 @@ typedef struct {
typedef struct {
const char *filename;
int index;
size_t size;
} entry;
pthread_mutex_t ctx_lock;
_Atomic mcs_lock ctx_lock;
static archive_ctx **ctx_pool;
#define CTX_POOL_SIZE 20
static void *mempool = NULL;
static size_t *mempoolofs = NULL;
#define PAGE_ALIGN(x) ((x + ~PAGE_MASK) & PAGE_MASK)
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
#define MEMPOOL_ENTITY_SIZE(x) PAGE_ALIGN(entries[x].size)
#define PROT_RW (PROT_WRITE | PROT_READ)
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
static bool need_encrypt = false;
static char *passwd = NULL;
static void *archiveAddr = NULL;
@@ -59,9 +72,7 @@ static size_t archiveSize = 0;
static entry *entries = NULL;
static size_t entryCount = 0;
#define POPULATE_THRESHOLD (512 * 1024 * 1024) // 512 MiB
const char supportExt[9][6] = {
const char supportExt[10][6] = {
"jpeg",
"jpg",
"png",
@@ -70,7 +81,8 @@ const char supportExt[9][6] = {
"bmp",
"ico",
"wbmp",
"heif"
"heif",
"avif"
};
static inline int filename_is_playable_file(const char *name) {
@@ -78,7 +90,7 @@ static inline int filename_is_playable_file(const char *name) {
if (!dotptr++)
return false;
int i;
for (i = 0; i < 9; i++)
for (i = 0; i < 10; i++)
if (strcmp(dotptr, supportExt[i]) == 0)
return true;
return false;
@@ -87,9 +99,7 @@ static inline int filename_is_playable_file(const char *name) {
static inline int compare_entries(const void *a, const void *b) {
const char *fa = ((entry *) a)->filename;
const char *fb = ((entry *) b)->filename;
int a_len = (int) strlen(fa);
int b_len = (int) strlen(fb);
return a_len == b_len ? strcmp(fa, fb) : a_len - b_len;
return strnatcmp(fa, fb);
}
static long archive_map_entries_index(archive_ctx *ctx) {
@@ -99,6 +109,7 @@ static long archive_map_entries_index(archive_ctx *ctx) {
if (filename_is_playable_file(name)) {
entries[count].filename = strdup(name);
entries[count].index = count;
entries[count].size = archive_entry_size(ctx->entry);
count++;
}
}
@@ -108,6 +119,48 @@ static long archive_map_entries_index(archive_ctx *ctx) {
return count;
}
static bool archive_prealloc_mempool() {
mempoolofs = calloc(entryCount, sizeof(size_t));
for (int i = 0; i < entryCount; ++i) {
if (!i)
mempoolofs[i] = PAGE_ALIGN(entries[i].size);
else
mempoolofs[i] = PAGE_ALIGN(entries[i].size) + mempoolofs[i - 1];
}
mempool = mmap(0, MEMPOOL_SIZE, PROT_RW, MAP_ANON_POOL, -1, 0);
if (mempool == MAP_FAILED) {
LOGE("%s%s", "mmap failed with error ", strerror(errno));
return false;
}
return true;
}
// Mincore does not support anon mapping yet
// Now that we use MADV_FREE, how can we detect whether pages is reclaimed?
static bool is_mempool_pages_present_and_lock(int index) {
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
size_t size = MEMPOOL_ENTITY_SIZE(index);
mlock(addr, size);
#if 0
unsigned char vec[size / PAGE_SIZE];
unsigned char r = 1;
mincore(addr, size, vec);
for (int i = 0; i < size / PAGE_SIZE; ++i) r &= vec[i];
return r;
#endif
return false;
}
#define MEMPOOL_IN_CORE_AND_LOCK(x) is_mempool_pages_present_and_lock(x)
static void mempool_release_pages(void *addr, size_t size) {
size = PAGE_ALIGN(size);
munlock(addr, size);
int ret = madvise(addr, size, MADV_FREE);
if (ret == -EINVAL) madvise(addr, size, MADV_DONTNEED);
}
static long archive_list_all_entries(archive_ctx *ctx) {
long count = 0;
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
@@ -158,7 +211,6 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
}
static int archive_skip_to_index(archive_ctx *ctx, int index) {
assert(!(ctx->next_index > index));
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
continue;
@@ -172,7 +224,8 @@ static int archive_skip_to_index(archive_ctx *ctx, int index) {
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
int ret;
archive_ctx *ctx = NULL;
pthread_mutex_lock(&ctx_lock);
mcs_lock_t local_lock;
lock_mcs(&ctx_lock, &local_lock);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i])
continue;
@@ -187,7 +240,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
}
if (ctx)
ctx->using = 1;
pthread_mutex_unlock(&ctx_lock);
unlock_mcs(&ctx_lock, &local_lock);
if (!ctx) {
archive_ctx *victimCtx = NULL;
@@ -196,7 +249,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
ret = archive_alloc_ctx(&ctx);
if (ret)
return ret;
pthread_mutex_lock(&ctx_lock);
lock_mcs(&ctx_lock, &local_lock);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i]) {
ctx_pool[i] = ctx;
@@ -210,11 +263,9 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
victimIdx = i;
}
}
if (replace) {
archive_release_ctx(victimCtx);
ctx_pool[victimIdx] = ctx;
}
pthread_mutex_unlock(&ctx_lock);
if (replace) ctx_pool[victimIdx] = ctx;
unlock_mcs(&ctx_lock, &local_lock);
if (replace) archive_release_ctx(victimCtx);
}
ret = archive_skip_to_index(ctx, idx);
if (ret != idx) {
@@ -228,13 +279,12 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
}
JNIEXPORT jint JNICALL
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd, jlong size) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
jlong size) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int mmap_flags = MAP_PRIVATE;
if (size <= POPULATE_THRESHOLD)
mmap_flags |= MAP_POPULATE;
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
if (archiveAddr == MAP_FAILED) {
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
@@ -242,7 +292,6 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
}
archiveSize = size;
madvise_log_if_error(archiveAddr, archiveSize, MADV_WILLNEED);
pthread_mutex_init(&ctx_lock, 0);
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
if (!ctx_pool) {
LOGE("Allocate archive ctx pool failed:ENOMEM");
@@ -255,7 +304,7 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
} else {
entryCount = archive_list_all_entries(ctx);
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
// We must read through the file|vm then we can know whether it is encrypted
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
@@ -292,53 +341,66 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
} else {
entries = calloc(entryCount, sizeof(entry));
r = archive_map_entries_index(ctx);
if (!archive_prealloc_mempool()) {
r = 0;
for (int i = 0; i < entryCount; ++i) {
free((void *) entries[i].filename);
}
free(entries);
}
}
archive_release_ctx(ctx);
return r;
}
#pragma clang diagnostic push
#pragma ide diagnostic ignored "ConstantConditionsOC"
#pragma ide diagnostic ignored "UnreachableCode"
JNIEXPORT jobject JNICALL
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
index = entries[index].index;
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret)
return 0;
long long size = archive_entry_size(ctx->entry);
void *buffer = malloc(size);
if (!buffer) {
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
size_t size = entries[index].size;
if (!MEMPOOL_IN_CORE_AND_LOCK(index)) {
index = entries[index].index;
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret) return 0;
ret = archive_read_data(ctx->arc, addr, size);
ctx->using = 0;
LOGE("Allocate buffer for decompression failed:ENOMEM");
return 0;
if (ret == size)
return (*env)->NewDirectByteBuffer(env, addr, size);
if (ret != size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
mempool_release_pages(addr, size);
} else {
return (*env)->NewDirectByteBuffer(env, addr, size);
}
ret = archive_read_data(ctx->arc, buffer, size);
ctx->using = 0;
if (ret == size)
return (*env)->NewDirectByteBuffer(env, buffer, size);
if (ret != size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
free(buffer);
return 0;
}
#pragma clang diagnostic pop
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_closeArchive(JNIEnv *env, jclass thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
for (int i = 0; i < CTX_POOL_SIZE; i++)
archive_release_ctx(ctx_pool[i]);
free(passwd);
free(ctx_pool);
pthread_mutex_destroy(&ctx_lock);
passwd = NULL;
need_encrypt = false;
munmap(archiveAddr, archiveSize);
munmap(mempool, MEMPOOL_SIZE);
free(mempoolofs);
for (int i = 0; i < entryCount; ++i) {
free((void *) entries[i].filename);
}
@@ -346,14 +408,15 @@ Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_needPassword(JNIEnv *env, jobject thiz) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_needPassword(JNIEnv *env, jclass thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
return need_encrypt;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *env, jobject thiz, jstring str) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_providePassword(JNIEnv *env, jclass thiz,
jstring str) {
EH_UNUSED(thiz);
struct archive_entry *entry;
archive_ctx *ctx;
@@ -383,7 +446,8 @@ Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *env, jobject thiz, jst
}
JNIEXPORT jstring JNICALL
Java_com_hippo_UriArchiveAccessor_getFilename(JNIEnv *env, jobject thiz, jint index) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_getFilename(JNIEnv *env, jclass thiz,
jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
index = entries[index].index;
@@ -398,7 +462,8 @@ Java_com_hippo_UriArchiveAccessor_getFilename(JNIEnv *env, jobject thiz, jint in
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint index, jint fd) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_extractToFd(JNIEnv *env, jclass thiz,
jint index, jint fd) {
EH_UNUSED(env);
EH_UNUSED(thiz);
index = entries[index].index;
@@ -412,8 +477,10 @@ Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint in
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
Java_com_hippo_ehviewer_gallery_ArchivePageLoaderKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
jobject buffer) {
EH_UNUSED(thiz);
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
free(addr);
}
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
mempool_release_pages(addr, size);
}
-43
View File
@@ -1,43 +0,0 @@
#include <jni.h>
#include <sys/mman.h>
#include <android/log.h>
#include <string.h>
#include <errno.h>
#define LOG_TAG "mmap_utils"
#include "ehviewer.h"
JNIEXPORT jint JNICALL
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
jint fd = -1;
jclass fdClass = (*env)->FindClass(env, "java/io/FileDescriptor");
if (fdClass != NULL) {
jfieldID fdClassDescriptorFieldID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
if (fdClassDescriptorFieldID != NULL && fileDescriptor != NULL) {
fd = (*env)->GetIntField(env, fileDescriptor, fdClassDescriptorFieldID);
}
}
return fd;
}
JNIEXPORT jobject JNICALL
Java_com_hippo_Native_mapFd(JNIEnv *env, jclass clazz, jint fd, jlong capability) {
void *addr = mmap(0, capability, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr != MAP_FAILED) {
return (*env)->NewDirectByteBuffer(env, addr, capability);
} else {
LOGE("%s%s", "mmap failed with error ", strerror(errno));
return NULL;
}
}
JNIEXPORT void JNICALL
Java_com_hippo_Native_unmapDirectByteBuffer(JNIEnv *env, jclass clazz, jobject buffer) {
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
if (munmap(addr, size)) LOGE("munmap addr:%p, size%zu failed!", addr, size);
}
+21 -4
View File
@@ -2,6 +2,8 @@
#include <string.h>
#include <stdbool.h>
#include <android/log.h>
#include <sys/stat.h>
#include <sys/mman.h>
#define LOG_TAG "gifUtils"
@@ -22,10 +24,7 @@ static inline bool isGif(void *addr) {
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
static void doRewrite(byte *addr, size_t size) {
if (size < 7 || !isGif(addr)) return;
for (size_t i = 0; i < size - 8; i++) {
// TODO: Optimize this hex find with SIMD?
@@ -42,3 +41,21 @@ Java_com_hippo_image_Image_rewriteGifSource(JNIEnv *env, jclass clazz, jobject b
}
}
}
JNIEXPORT void JNICALL
Java_com_hippo_ehviewer_image_ImageKt_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) {
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);
munmap(addr, size);
}
+177
View File
@@ -0,0 +1,177 @@
/* -*- mode: c; c-file-style: "k&r" -*-
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
/* partial change history:
*
* 2004-10-10 mbp: Lift out character type dependencies into macros.
*
* Eric Sosman pointed out that ctype functions take a parameter whose
* value must be that of an unsigned int, even on platforms that have
* negative chars in their default char type.
*/
#include <stddef.h> /* size_t */
#include <ctype.h>
#include "strnatcmp.h"
/* These are defined as macros to make it easier to adapt this code to
* different characters types or comparison functions. */
static inline int
nat_isdigit(nat_char a)
{
return isdigit((unsigned char) a);
}
static inline int
nat_isspace(nat_char a)
{
return isspace((unsigned char) a);
}
static inline nat_char
nat_toupper(nat_char a)
{
return toupper((unsigned char) a);
}
static int
compare_right(nat_char const *a, nat_char const *b)
{
int bias = 0;
/* The longest run of digits wins. That aside, the greatest
value wins, but we can't know that it will until we've scanned
both numbers to know that they have the same magnitude, so we
remember it in BIAS. */
for (;; a++, b++) {
if (!nat_isdigit(*a) && !nat_isdigit(*b))
return bias;
if (!nat_isdigit(*a))
return -1;
if (!nat_isdigit(*b))
return +1;
if (*a < *b) {
if (!bias)
bias = -1;
} else if (*a > *b) {
if (!bias)
bias = +1;
} else if (!*a && !*b)
return bias;
}
return 0;
}
static int
compare_left(nat_char const *a, nat_char const *b)
{
/* Compare two left-aligned numbers: the first to have a
different value wins. */
for (;; a++, b++) {
if (!nat_isdigit(*a) && !nat_isdigit(*b))
return 0;
if (!nat_isdigit(*a))
return -1;
if (!nat_isdigit(*b))
return +1;
if (*a < *b)
return -1;
if (*a > *b)
return +1;
}
return 0;
}
static int
strnatcmp0(nat_char const *a, nat_char const *b, int fold_case)
{
int ai, bi;
nat_char ca, cb;
int fractional, result;
ai = bi = 0;
while (1) {
ca = a[ai]; cb = b[bi];
/* skip over leading spaces or zeros */
while (nat_isspace(ca))
ca = a[++ai];
while (nat_isspace(cb))
cb = b[++bi];
/* process run of digits */
if (nat_isdigit(ca) && nat_isdigit(cb)) {
fractional = (ca == '0' || cb == '0');
if (fractional) {
if ((result = compare_left(a+ai, b+bi)) != 0)
return result;
} else {
if ((result = compare_right(a+ai, b+bi)) != 0)
return result;
}
}
if (!ca && !cb) {
/* The strings compare the same. Perhaps the caller
will want to call strcmp to break the tie. */
return 0;
}
if (fold_case) {
ca = nat_toupper(ca);
cb = nat_toupper(cb);
}
if (ca < cb)
return -1;
if (ca > cb)
return +1;
++ai; ++bi;
}
}
int
strnatcmp(nat_char const *a, nat_char const *b) {
return strnatcmp0(a, b, 0);
}
/* Compare, recognizing numeric string and ignoring case. */
int
strnatcasecmp(nat_char const *a, nat_char const *b) {
return strnatcmp0(a, b, 1);
}
+38
View File
@@ -0,0 +1,38 @@
/* -*- mode: c; c-file-style: "k&r" -*-
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#ifdef __cplusplus
extern "C" {
#endif
/* CUSTOMIZATION SECTION
*
* You can change this typedef, but must then also change the inline
* functions in strnatcmp.c */
typedef char nat_char;
int strnatcmp(nat_char const *a, nat_char const *b);
int strnatcasecmp(nat_char const *a, nat_char const *b);
#ifdef __cplusplus
}
#endif
+44
View File
@@ -0,0 +1,44 @@
#ifndef EHVIEWER_SPINLOCK_H
#define EHVIEWER_SPINLOCK_H
#include <stddef.h>
#include <stdatomic.h>
#define cmpxchg(P, O, N) __sync_val_compare_and_swap((P), (O), (N))
#define barrier() __asm__ __volatile__("":::"memory")
#if defined(__aarch64__) || defined(__arm__)
#define cpu_relax() __asm__ volatile ("wfe");
#else
#define cpu_relax() __asm__ volatile ("pause" ::: "memory");
#endif
typedef struct mcs_lock_t mcs_lock_t;
struct mcs_lock_t {
mcs_lock_t *next;
int spin;
};
typedef struct mcs_lock_t *mcs_lock;
static inline void lock_mcs(_Atomic mcs_lock *m, mcs_lock_t *me) {
mcs_lock_t *tail;
me->next = NULL;
me->spin = 0;
tail = atomic_exchange(m, me);
if (!tail) return;
tail->next = me;
barrier();
while (!me->spin) cpu_relax();
return;
}
static inline void unlock_mcs(mcs_lock *m, mcs_lock_t *me) {
if (!me->next) {
if (cmpxchg(m, me, NULL) == me) return;
while (!me->next) cpu_relax();
}
me->next->spin = 1;
}
#endif //EHVIEWER_SPINLOCK_H
@@ -1,21 +1,19 @@
package androidx.navigation.ui
import android.view.Menu
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.NavGraph.Companion.findStartDestination
import androidx.navigation.NavOptions
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
if (navController.currentDestination?.matchDestination(item.itemId) == true) return false
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
is ActivityNavigator.Destination
is ActivityNavigator.Destination
) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
@@ -27,13 +25,6 @@ fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boo
.setPopEnterAnim(com.hippo.ehviewer.R.anim.scene_close_enter)
.setPopExitAnim(com.hippo.ehviewer.R.anim.scene_close_exit)
}
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
builder.setPopUpTo(
navController.graph.findStartDestination().id,
inclusive = false,
saveState = false
)
}
val options = builder.build()
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
@@ -46,4 +37,4 @@ fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boo
}
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
hierarchy.any { it.id == destId }
@@ -7,10 +7,10 @@ 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
context: Context,
attrs: AttributeSet? = null,
) : SwipeRefreshLayout(context, attrs) {
init {
@@ -22,4 +22,4 @@ class ThemedSwipeRefreshLayout @JvmOverloads constructor(
(mCircleView.background as ShapeDrawable).paint.color = backgroundColor
setColorSchemeColors(context.theme.resolveColor(com.google.android.material.R.attr.colorAccent))
}
}
}
@@ -1,15 +1,8 @@
package com.davemorrissey.labs.subscaleview
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import com.davemorrissey.labs.subscaleview.provider.AssetInputProvider
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.davemorrissey.labs.subscaleview.provider.OpenStreamProvider
import com.davemorrissey.labs.subscaleview.provider.ResourceInputProvider
import com.davemorrissey.labs.subscaleview.provider.UriInputProvider
import java.io.InputStream
/**
* Helper class used to set the source and additional attributes from a variety of sources. Supports
@@ -83,38 +76,6 @@ class ImageSource {
}
companion object {
/**
* Create an instance from a resource. The correct resource for the device screen resolution will be used.
*
* @param resId resource ID.
* @return an [ImageSource] instance.
*/
@JvmStatic
fun resource(context: Context, resId: Int): ImageSource {
return ImageSource(ResourceInputProvider(context, resId))
}
/**
* Create an instance from an asset name.
*
* @param assetName asset name.
* @return an [ImageSource] instance.
*/
@JvmStatic
fun asset(context: Context, assetName: String): ImageSource {
return ImageSource(AssetInputProvider(context, assetName))
}
/**
* Create an instance from a URI.
*
* @param uri image URI.
* @return an [ImageSource] instance.
*/
fun uri(context: Context, uri: Uri): ImageSource {
return ImageSource(UriInputProvider(context, uri))
}
/**
* Create an instance from an input provider.
*
@@ -125,16 +86,6 @@ class ImageSource {
return ImageSource(provider)
}
/**
* Create an instance from an input stream.
*
* @param stream open input stream.
* @return an [ImageSource] instance.
*/
fun inputStream(stream: InputStream): ImageSource {
return ImageSource(OpenStreamProvider(stream))
}
/**
* Provide a loaded bitmap for display.
*
@@ -19,12 +19,10 @@ import android.view.MotionEvent
import android.view.View
import androidx.annotation.AnyThread
import androidx.annotation.IntDef
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.asset
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.resource
import com.davemorrissey.labs.subscaleview.decoder.Decoder
import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.hippo.ehviewer.R
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -65,7 +63,7 @@ import kotlin.math.sqrt
@Suppress("unused")
open class SubsamplingScaleImageView @JvmOverloads constructor(
context: Context,
attr: AttributeSet? = null
attr: AttributeSet? = null,
) : View(context, attr) {
private val decoderLock: ReadWriteLock = ReentrantReadWriteLock(true)
@@ -81,7 +79,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
private val srcArray = FloatArray(8)
private val dstArray = FloatArray(8)
//The logical density of the display
// The logical density of the display
private val density: Float = resources.displayMetrics.density
// Bitmap (preview or full image)
@@ -142,6 +140,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
// Double tap zoom behaviour
private var doubleTapZoomScale = 1F
@ZoomStyle private var doubleTapZoomStyle = ZOOM_FOCUS_FIXED
private var doubleTapZoomDuration = 500
@@ -303,42 +302,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
setMinimumDpi(160)
setDoubleTapZoomDpi(160)
setGestureDetector(context)
// Handle XML attributes
if (attr != null) {
val typedAttr = getContext().obtainStyledAttributes(attr, R.styleable.SubsamplingScaleImageView)
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_assetName)) {
val assetName = typedAttr.getString(R.styleable.SubsamplingScaleImageView_assetName)
if (assetName != null && assetName.isNotEmpty()) {
setImage(asset(context, assetName))
}
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_src)) {
val resId = typedAttr.getResourceId(R.styleable.SubsamplingScaleImageView_src, 0)
if (resId > 0) {
setImage(resource(context, resId))
}
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_panEnabled)) {
setPanEnabled(typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_panEnabled, true))
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_zoomEnabled)) {
isZoomEnabled = typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_zoomEnabled, true)
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_quickScaleEnabled)) {
isQuickScaleEnabled =
typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_quickScaleEnabled, true)
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_tileBackgroundColor)) {
setTileBackgroundColor(
typedAttr.getColor(
R.styleable.SubsamplingScaleImageView_tileBackgroundColor,
Color.argb(0, 0, 0, 0)
)
)
}
typedAttr.recycle()
}
quickScaleThreshold =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
}
@@ -370,8 +333,9 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
imageSource.sRegion.left,
imageSource.sRegion.top,
imageSource.sRegion.width(),
imageSource.sRegion.height()
), false
imageSource.sRegion.height(),
),
false,
)
} else if (imageSource.bitmap != null) {
onImageLoaded(imageSource.bitmap, imageSource.isCached)
@@ -475,64 +439,71 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
}
private fun setGestureDetector(context: Context) {
detector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
if (panEnabled && isReady && vTranslate != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
val vTranslateEnd = PointF(vTranslate!!.x + velocityX * 0.25F, vTranslate!!.y + velocityY * 0.25F)
val sCenterXEnd = (width / 2 - vTranslateEnd.x) / scale
val sCenterYEnd = (height / 2 - vTranslateEnd.y) / scale
AnimationBuilder(PointF(sCenterXEnd, sCenterYEnd))
.withEasing(EASE_OUT_QUAD)
.withPanLimited(false)
.withOrigin(ORIGIN_FLING)
.start()
detector = GestureDetector(
context,
object : GestureDetector.SimpleOnGestureListener() {
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
if (panEnabled && isReady && vTranslate != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
val vTranslateEnd = PointF(vTranslate!!.x + velocityX * 0.25F, vTranslate!!.y + velocityY * 0.25F)
val sCenterXEnd = (width / 2 - vTranslateEnd.x) / scale
val sCenterYEnd = (height / 2 - vTranslateEnd.y) / scale
AnimationBuilder(PointF(sCenterXEnd, sCenterYEnd))
.withEasing(EASE_OUT_QUAD)
.withPanLimited(false)
.withOrigin(ORIGIN_FLING)
.start()
return true
}
return super.onFling(e1, e2, velocityX, velocityY)
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
performClick()
return true
}
return super.onFling(e1, e2, velocityX, velocityY)
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
performClick()
return true
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (isZoomEnabled && isReady && vTranslate != null) {
// Hacky solution for #15 - after a double tap the GestureDetector gets in a state
// where the next fling is ignored, so here we replace it with a new one.
setGestureDetector(context)
return if (isQuickScaleEnabled) {
// Store quick scale params. This will become either a double tap zoom or a
// quick scale depending on whether the user swipes.
vCenterStart = PointF(e.x, e.y)
vTranslateStart = PointF(vTranslate!!.x, vTranslate!!.y)
scaleStart = scale
isQuickScaling = true
isZooming = true
quickScaleLastDistance = -1F
quickScaleSCenter = viewToSourceCoord(vCenterStart!!)
quickScaleVStart = PointF(e.x, e.y)
quickScaleVLastPoint = PointF(quickScaleSCenter!!.x, quickScaleSCenter!!.y)
quickScaleMoved = false
// We need to get events in onTouchEvent after this.
false
} else {
// Start double tap zoom animation.
viewToSourceCoord(PointF(e.x, e.y))?.let { center ->
doubleTapZoom(center, PointF(e.x, e.y))
override fun onDoubleTap(e: MotionEvent): Boolean {
if (!ReaderActivity.readerPreferences.doubleTapToZoom().get()) return false
if (isZoomEnabled && isReady && vTranslate != null) {
// Hacky solution for #15 - after a double tap the GestureDetector gets in a state
// where the next fling is ignored, so here we replace it with a new one.
setGestureDetector(context)
return if (isQuickScaleEnabled) {
// Store quick scale params. This will become either a double tap zoom or a
// quick scale depending on whether the user swipes.
vCenterStart = PointF(e.x, e.y)
vTranslateStart = PointF(vTranslate!!.x, vTranslate!!.y)
scaleStart = scale
isQuickScaling = true
isZooming = true
quickScaleLastDistance = -1F
quickScaleSCenter = viewToSourceCoord(vCenterStart!!)
quickScaleVStart = PointF(e.x, e.y)
quickScaleVLastPoint = PointF(quickScaleSCenter!!.x, quickScaleSCenter!!.y)
quickScaleMoved = false
// We need to get events in onTouchEvent after this.
false
} else {
// Start double tap zoom animation.
viewToSourceCoord(PointF(e.x, e.y))?.let { center ->
doubleTapZoom(center, PointF(e.x, e.y))
}
true
}
true
}
return super.onDoubleTapEvent(e)
}
return super.onDoubleTapEvent(e)
}
})
singleDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
performClick()
return true
}
})
},
)
singleDetector = GestureDetector(
context,
object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
performClick()
return true
}
},
)
}
override fun onDetachedFromWindow() {
@@ -769,7 +740,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
val dx = abs(event.x - vCenterStart!!.x)
val dy = abs(event.y - vCenterStart!!.y)
//On the Samsung S6 long click event does not work, because the dx > 5 usually true
// On the Samsung S6 long click event does not work, because the dx > 5 usually true
val offset = density * 5
if (dx > offset || dy > offset || isPanning) {
consumed = true
@@ -933,7 +904,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
scaleElapsed,
anim!!.scaleStart,
anim!!.scaleEnd - anim!!.scaleStart,
anim!!.duration
anim!!.duration,
)
// Apply required animation to the focal point
@@ -942,14 +913,14 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
scaleElapsed,
anim!!.vFocusStart!!.x,
anim!!.vFocusEnd.x - anim!!.vFocusStart!!.x,
anim!!.duration
anim!!.duration,
)
val vFocusNowY = ease(
anim!!.easing,
scaleElapsed,
anim!!.vFocusStart!!.y,
anim!!.vFocusEnd.y - anim!!.vFocusStart!!.y,
anim!!.duration
anim!!.duration,
)
// Find out where the focal point is at this scale and adjust its position to follow the animation path
vTranslate!!.x -= sourceToViewX(anim!!.sCenterEnd.x) - vFocusNowX
@@ -1006,7 +977,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
tileWidth,
tileHeight,
0F,
tileHeight
tileHeight,
)
setMatrixArray(
dstArray,
@@ -1017,7 +988,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
tile.vRect.right.toFloat(),
tile.vRect.bottom.toFloat(),
tile.vRect.left.toFloat(),
tile.vRect.bottom.toFloat()
tile.vRect.bottom.toFloat(),
)
mtrx.setPolyToPoly(srcArray, 0, dstArray, 0, 4)
canvas.drawBitmap(tile.bitmap!!, mtrx, bitmapPaint)
@@ -1029,7 +1000,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
"LOADING",
(tile.vRect.left + px(5)).toFloat(),
(tile.vRect.top + px(35)).toFloat(),
debugTextPaint
debugTextPaint,
)
}
if (tile.visible && debug) {
@@ -1037,7 +1008,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
"ISS " + tile.sampleSize + " RECT " + tile.sRect.top + "," + tile.sRect.left + "," + tile.sRect.bottom + "," + tile.sRect.right,
(tile.vRect.left + px(5)).toFloat(),
(tile.vRect.top + px(15)).toFloat(),
debugTextPaint
debugTextPaint,
)
}
}
@@ -1062,21 +1033,36 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
"Scale: " + String.format(Locale.ENGLISH, "%.2f", scale) + " (" + String.format(
Locale.ENGLISH,
"%.2f",
minScale()
minScale(),
) + " - " + String.format(
Locale.ENGLISH, "%.2f", maxScale
) + ")", px(5).toFloat(), px(15).toFloat(), debugTextPaint
Locale.ENGLISH,
"%.2f",
maxScale,
) + ")",
px(5).toFloat(),
px(15).toFloat(),
debugTextPaint,
)
canvas.drawText(
"Translate: " + String.format(Locale.ENGLISH, "%.2f", vTranslate!!.x) + ":" + String.format(
Locale.ENGLISH, "%.2f", vTranslate!!.y
), px(5).toFloat(), px(30).toFloat(), debugTextPaint
Locale.ENGLISH,
"%.2f",
vTranslate!!.y,
),
px(5).toFloat(),
px(30).toFloat(),
debugTextPaint,
)
val center = center
canvas.drawText(
"Source center: " + String.format(Locale.ENGLISH, "%.2f", center!!.x) + ":" + String.format(
Locale.ENGLISH, "%.2f", center.y
), px(5).toFloat(), px(45).toFloat(), debugTextPaint
Locale.ENGLISH,
"%.2f",
center.y,
),
px(5).toFloat(),
px(45).toFloat(),
debugTextPaint,
)
if (anim != null) {
val vCenterStart = sourceToViewCoord(anim!!.sCenterStart)
@@ -1100,7 +1086,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
sourceToViewX(quickScaleSCenter!!.x),
sourceToViewY(quickScaleSCenter!!.y),
px(35).toFloat(),
debugLinePaint
debugLinePaint,
)
}
if (quickScaleVStart != null && isQuickScaling) {
@@ -1123,7 +1109,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
f4: Float,
f5: Float,
f6: Float,
f7: Float
f7: Float,
) {
array[0] = f0
array[1] = f1
@@ -1197,7 +1183,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
debug(
"loadTile, tile.sRect=%s, tile.sampleSize=%d",
tile.sRect,
tile.sampleSize
tile.sampleSize,
)
decoderLock.readLock().lock()
try {
@@ -1468,13 +1454,13 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
x * sTileWidth,
y * sTileHeight,
if (x == xTiles - 1) sWidth else (x + 1) * sTileWidth,
if (y == yTiles - 1) sHeight else (y + 1) * sTileHeight
if (y == yTiles - 1) sHeight else (y + 1) * sTileHeight,
)
val tile = Tile(
sRect = sRect,
sampleSize = sampleSize,
vRect = Rect(0, 0, 0, 0),
fileSRect = Rect(sRect)
fileSRect = Rect(sRect),
)
tileGrid.add(tile)
}
@@ -1603,7 +1589,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
private fun getMaxBitmapDimensions(canvas: Canvas): Point {
return Point(
canvas.maximumBitmapWidth.coerceAtMost(maxTileWidth),
canvas.maximumBitmapHeight.coerceAtMost(maxTileHeight)
canvas.maximumBitmapHeight.coerceAtMost(maxTileHeight),
)
}
@@ -1668,15 +1654,15 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
if (vTranslate == null || !isReady) return
fRect[
viewToSourceX(vRect.left.toFloat()).toInt(),
viewToSourceY(vRect.top.toFloat()).toInt(),
viewToSourceX(vRect.right.toFloat()).toInt()
viewToSourceX(vRect.left.toFloat()).toInt(),
viewToSourceY(vRect.top.toFloat()).toInt(),
viewToSourceX(vRect.right.toFloat()).toInt(),
] = viewToSourceY(vRect.bottom.toFloat()).toInt()
fileSRect(fRect, fRect)
fRect[
fRect.left.coerceAtLeast(0),
fRect.top.coerceAtLeast(0),
fRect.right.coerceAtLeast(sWidth)
fRect.left.coerceAtLeast(0),
fRect.top.coerceAtLeast(0),
fRect.right.coerceAtLeast(sWidth),
] = fRect.bottom.coerceAtLeast(sHeight)
if (!sRegion.isEmpty) {
fRect.offset(sRegion.left, sRegion.top)
@@ -1793,9 +1779,9 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
*/
private fun sourceToViewRect(sRect: Rect, vTarget: Rect) {
vTarget[
sourceToViewX(sRect.left.toFloat()).toInt(),
sourceToViewY(sRect.top.toFloat()).toInt(),
sourceToViewX(sRect.right.toFloat()).toInt()
sourceToViewX(sRect.left.toFloat()).toInt(),
sourceToViewY(sRect.top.toFloat()).toInt(),
sourceToViewX(sRect.right.toFloat()).toInt(),
] = sourceToViewY(sRect.bottom.toFloat()).toInt()
}
@@ -1842,11 +1828,11 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
return when (minimumScaleType) {
SCALE_TYPE_CENTER_INSIDE -> min(
(width - hPadding) / sWidth.toFloat(),
(height - vPadding) / sHeight.toFloat()
(height - vPadding) / sHeight.toFloat(),
)
SCALE_TYPE_CENTER_CROP -> max(
(width - hPadding) / sWidth.toFloat(),
(height - vPadding) / sHeight.toFloat()
(height - vPadding) / sHeight.toFloat(),
)
SCALE_TYPE_FIT_WIDTH -> (width - hPadding) / sWidth.toFloat()
SCALE_TYPE_FIT_HEIGHT -> (height - vPadding) / sHeight.toFloat()
@@ -1953,20 +1939,20 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
val scaleHeight = scale * sHeight
when (panLimit) {
PAN_LIMIT_CENTER -> {
vTarget.top = -(vTranslate!!.y - height / 2).coerceAtLeast(0F)
vTarget.left = -(vTranslate!!.x - width / 2).coerceAtLeast(0F)
vTarget.top = (-(vTranslate!!.y - height / 2)).coerceAtLeast(0F)
vTarget.left = (-(vTranslate!!.x - width / 2)).coerceAtLeast(0F)
vTarget.bottom = (vTranslate!!.y - (height / 2 - scaleHeight)).coerceAtLeast(0F)
vTarget.right = (vTranslate!!.x - (width / 2 - scaleWidth)).coerceAtLeast(0F)
}
PAN_LIMIT_OUTSIDE -> {
vTarget.top = -(vTranslate!!.y - height).coerceAtLeast(0F)
vTarget.left = -(vTranslate!!.x - width).coerceAtLeast(0F)
vTarget.top = (-(vTranslate!!.y - height)).coerceAtLeast(0F)
vTarget.left = (-(vTranslate!!.x - width)).coerceAtLeast(0F)
vTarget.bottom = (vTranslate!!.y + scaleHeight).coerceAtLeast(0F)
vTarget.right = (vTranslate!!.x + scaleWidth).coerceAtLeast(0F)
}
else -> {
vTarget.top = -vTranslate!!.y.coerceAtLeast(0F)
vTarget.left = -vTranslate!!.x.coerceAtLeast(0F)
vTarget.top = (-vTranslate!!.y).coerceAtLeast(0F)
vTarget.left = (-vTranslate!!.x).coerceAtLeast(0F)
vTarget.bottom = (scaleHeight + vTranslate!!.y - height).coerceAtLeast(0F)
vTarget.right = (scaleWidth + vTranslate!!.x - width).coerceAtLeast(0F)
}
@@ -2150,7 +2136,9 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
val state: ImageViewState?
get() = if (vTranslate != null && sWidth > 0 && sHeight > 0) {
ImageViewState(scale, center!!)
} else null
} else {
null
}
/**
* Returns true if pan gesture detection is enabled.
@@ -2436,7 +2424,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
val sRect: Rect,
val sampleSize: Int,
val vRect: Rect,
val fileSRect: Rect
val fileSRect: Rect,
) {
var bitmap: Bitmap? = null
var loading = false
@@ -2471,7 +2459,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
val easing: Int,
val origin: Int,
val time: Long = System.currentTimeMillis(),
val listener: OnAnimationEventListener?
val listener: OnAnimationEventListener?,
)
private data class ScaleAndTranslate(var scale: Float, val vTranslate: PointF)
@@ -2617,12 +2605,16 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
val vxCenter = paddingLeft + (width - paddingRight - paddingLeft) / 2
val vyCenter = paddingTop + (height - paddingBottom - paddingTop) / 2
val targetScale = limitedScale(targetScale)
val targetSCenter = if (panLimited) limitedSCenter(
targetSCenter!!.x,
targetSCenter.y,
targetScale,
PointF()
) else targetSCenter!!
val targetSCenter = if (panLimited) {
limitedSCenter(
targetSCenter!!.x,
targetSCenter.y,
targetScale,
PointF(),
)
} else {
targetSCenter!!
}
anim = Anim(
scaleStart = scale,
scaleEnd = targetScale,
@@ -2632,13 +2624,13 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
vFocusStart = sourceToViewCoord(targetSCenter),
vFocusEnd = PointF(
vxCenter.toFloat(),
vyCenter.toFloat()
vyCenter.toFloat(),
),
duration = duration,
interruptible = interruptible,
easing = easing,
origin = origin,
listener = listener
listener = listener,
)
if (vFocus != null) {
// Calculate where translation will be at the end of the anim
@@ -2650,7 +2642,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
// Adjust the position of the focus point at end so image will be in bounds
anim!!.vFocusEnd.set(
vFocus.x + (satEnd.vTranslate.x - vTranslateXEnd),
vFocus.y + (satEnd.vTranslate.y - vTranslateYEnd)
vFocus.y + (satEnd.vTranslate.y - vTranslateYEnd),
)
}
invalidate()
@@ -2662,7 +2654,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
@IntDef(
ZOOM_FOCUS_FIXED,
ZOOM_FOCUS_CENTER,
ZOOM_FOCUS_CENTER_IMMEDIATE
ZOOM_FOCUS_CENTER_IMMEDIATE,
)
annotation class ZoomStyle
@@ -2684,7 +2676,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
@Retention(AnnotationRetention.SOURCE)
@IntDef(
EASE_IN_OUT_QUAD,
EASE_OUT_QUAD
EASE_OUT_QUAD,
)
annotation class EasingStyle
@@ -2702,7 +2694,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
@IntDef(
PAN_LIMIT_INSIDE,
PAN_LIMIT_OUTSIDE,
PAN_LIMIT_CENTER
PAN_LIMIT_CENTER,
)
annotation class PanLimit
@@ -2729,7 +2721,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
SCALE_TYPE_FIT_HEIGHT,
SCALE_TYPE_ORIGINAL_SIZE,
SCALE_TYPE_SMART_FIT,
SCALE_TYPE_CUSTOM
SCALE_TYPE_CUSTOM,
)
annotation class ScaleType
@@ -4,9 +4,9 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import kotlin.Throws
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import java.lang.Exception
import kotlin.Throws
/**
* Interface for image decoding classes.
@@ -50,4 +50,4 @@ interface Decoder {
* resources still in use.
*/
fun recycle()
}
}
@@ -4,14 +4,12 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import kotlin.Throws
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import java.lang.Exception
import com.davemorrissey.labs.subscaleview.provider.InputProvider
class ImageDecoder @JvmOverloads constructor(
private val cropBorders: Boolean,
bitmapConfig: Bitmap.Config? = null
bitmapConfig: Bitmap.Config? = null,
) : Decoder {
init {
val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig
@@ -61,4 +59,4 @@ class ImageDecoder @JvmOverloads constructor(
override fun recycle() {
TODO()
}
}
}
@@ -1,11 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class AssetInputProvider(context: Context, val assetName: String) : InputProvider {
private val assets = context.assets
@Throws(IOException::class)
override fun openStream() = assets.open(assetName)
}
@@ -1,10 +1,3 @@
package com.davemorrissey.labs.subscaleview.provider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
interface InputProvider {
@Throws(IOException::class)
fun openStream(): InputStream?
}
interface InputProvider
@@ -1,16 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
/**
* Provider from an already open input stream.
*
*
* Please keep in mind this provider is not reusable.
*/
class OpenStreamProvider(private val stream: InputStream) : InputProvider {
@Throws(IOException::class)
override fun openStream() = stream
}
@@ -1,11 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class ResourceInputProvider(context: Context, val resource: Int) : InputProvider {
private val resources = context.resources
@Throws(IOException::class)
override fun openStream() = resources.openRawResource(resource)
}
@@ -1,13 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import android.net.Uri
import java.io.IOException
import kotlin.Throws
class UriInputProvider(context: Context, val uri: Uri) : InputProvider {
private val resolver = context.contentResolver
@Throws(IOException::class)
override fun openStream() = resolver.openInputStream(uri)
}
@@ -1,62 +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
import android.content.Context
import android.net.Uri
import com.hippo.image.Image
import java.nio.ByteBuffer
class UriArchiveAccessor(ctx: Context, uri: Uri) {
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
fun open(): Int {
return openArchive(pfd.fd, pfd.statSize)
}
private external fun openArchive(fd: Int, size: Long): Int
private external fun extractToByteBuffer(index: Int): ByteBuffer?
external fun extractToFd(index: Int, fd: Int)
external fun getFilename(index: Int): String
external fun needPassword(): Boolean
external fun providePassword(str: String): Boolean
private external fun closeArchive()
fun close() {
closeArchive()
pfd.close()
}
fun getImageSource(index: Int): Image.ByteBufferSource? {
val buffer = extractToByteBuffer(index)
buffer ?: return null
check(buffer.isDirect)
return object : Image.ByteBufferSource {
override fun getByteBuffer(): ByteBuffer {
return buffer
}
override fun close() {
releaseByteBuffer(buffer)
}
}
}
companion object {
@JvmStatic
private external fun releaseByteBuffer(buffer: ByteBuffer)
}
}
@@ -1,14 +0,0 @@
package com.hippo.app
import android.content.Context
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import com.google.android.material.slider.Slider
class HapticSlider @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : Slider(context, attrs) {
init {
addOnChangeListener { _, _, _ -> performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) }
}
}
@@ -1,190 +0,0 @@
/*
* Copyright 2018 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.drawable;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.core.graphics.drawable.DrawableCompat;
public class DrawableWrapper extends Drawable implements Drawable.Callback {
private Drawable mDrawable;
private Rect mBounds;
public DrawableWrapper(Drawable drawable) {
this.setWrappedDrawable(drawable);
}
public void draw(@NonNull Canvas canvas) {
if (mDrawable instanceof BitmapDrawable bitmapDrawable) {
canvas.drawBitmap(bitmapDrawable.getBitmap(), null, mBounds, bitmapDrawable.getPaint());
} else {
synchronized (mDrawable) {
mDrawable.setBounds(mBounds);
mDrawable.draw(canvas);
}
}
}
protected void onBoundsChange(Rect bounds) {
mBounds = bounds;
}
public int getChangingConfigurations() {
return this.mDrawable.getChangingConfigurations();
}
public void setChangingConfigurations(int configs) {
this.mDrawable.setChangingConfigurations(configs);
}
public void setDither(boolean dither) {
this.mDrawable.setDither(dither);
}
public void setFilterBitmap(boolean filter) {
this.mDrawable.setFilterBitmap(filter);
}
public void setAlpha(int alpha) {
this.mDrawable.setAlpha(alpha);
}
public void setColorFilter(ColorFilter cf) {
this.mDrawable.setColorFilter(cf);
}
public boolean isStateful() {
return this.mDrawable.isStateful();
}
public boolean setState(@NonNull int[] stateSet) {
return this.mDrawable.setState(stateSet);
}
@NonNull
public int[] getState() {
return this.mDrawable.getState();
}
public void jumpToCurrentState() {
this.mDrawable.jumpToCurrentState();
}
@NonNull
public Drawable getCurrent() {
return this.mDrawable.getCurrent();
}
public boolean setVisible(boolean visible, boolean restart) {
return super.setVisible(visible, restart) || this.mDrawable.setVisible(visible, restart);
}
public int getOpacity() {
return this.mDrawable.getOpacity();
}
public Region getTransparentRegion() {
return this.mDrawable.getTransparentRegion();
}
public int getIntrinsicWidth() {
return this.mDrawable.getIntrinsicWidth();
}
public int getIntrinsicHeight() {
return this.mDrawable.getIntrinsicHeight();
}
public int getMinimumWidth() {
return this.mDrawable.getMinimumWidth();
}
public int getMinimumHeight() {
return this.mDrawable.getMinimumHeight();
}
public boolean getPadding(@NonNull Rect padding) {
return this.mDrawable.getPadding(padding);
}
public void invalidateDrawable(@NonNull Drawable who) {
this.invalidateSelf();
}
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
this.scheduleSelf(what, when);
}
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
this.unscheduleSelf(what);
}
protected boolean onLevelChange(int level) {
return this.mDrawable.setLevel(level);
}
public boolean isAutoMirrored() {
return DrawableCompat.isAutoMirrored(this.mDrawable);
}
public void setAutoMirrored(boolean mirrored) {
DrawableCompat.setAutoMirrored(this.mDrawable, mirrored);
}
public void setTint(int tint) {
DrawableCompat.setTint(this.mDrawable, tint);
}
public void setTintList(ColorStateList tint) {
DrawableCompat.setTintList(this.mDrawable, tint);
}
public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
DrawableCompat.setTintMode(this.mDrawable, tintMode);
}
public void setHotspot(float x, float y) {
DrawableCompat.setHotspot(this.mDrawable, x, y);
}
public void setHotspotBounds(int left, int top, int right, int bottom) {
DrawableCompat.setHotspotBounds(this.mDrawable, left, top, right, bottom);
}
public Drawable getWrappedDrawable() {
return this.mDrawable;
}
public void setWrappedDrawable(Drawable drawable) {
if (this.mDrawable != null) {
this.mDrawable.setCallback(null);
}
this.mDrawable = drawable;
if (drawable != null) {
drawable.setCallback(this);
}
}
}
@@ -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.drawable;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import com.hippo.yorozuya.MathUtils;
/**
* Show a part of the original drawable
*/
public class PreciselyClipDrawable extends DrawableWrapper {
private final boolean mClip;
private RectF mScale;
private Rect mTemp;
public PreciselyClipDrawable(Drawable drawable, int offsetX, int offsetY, int width, int height) {
super(drawable);
float originWidth = drawable.getIntrinsicWidth();
float originHeight = drawable.getIntrinsicHeight();
if (originWidth <= 0 || originHeight <= 0) {
// Can not clip
mClip = false;
} else {
mClip = true;
mScale = new RectF();
mScale.set(MathUtils.clamp(offsetX / originWidth, 0.0f, 1.0f),
MathUtils.clamp(offsetY / originHeight, 0.0f, 1.0f),
MathUtils.clamp((offsetX + width) / originWidth, 0.0f, 1.0f),
MathUtils.clamp((offsetY + height) / originHeight, 0.0f, 1.0f));
mTemp = new Rect();
}
}
@Override
protected void onBoundsChange(Rect bounds) {
if (mClip) {
if (!mScale.isEmpty()) {
mTemp.left = (int) ((mScale.left * bounds.right - mScale.right * bounds.left) /
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
mTemp.right = (int) (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) /
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
mTemp.top = (int) ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) /
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
mTemp.bottom = (int) (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) /
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
super.onBoundsChange(mTemp);
}
} else {
super.onBoundsChange(bounds);
}
}
@Override
public int getIntrinsicWidth() {
if (mClip) {
return (int) (super.getIntrinsicWidth() * mScale.width());
} else {
return super.getIntrinsicWidth();
}
}
@Override
public int getIntrinsicHeight() {
if (mClip) {
return (int) (super.getIntrinsicHeight() * mScale.height());
} else {
return super.getIntrinsicHeight();
}
}
@Override
public void draw(Canvas canvas) {
if (mClip) {
if (!mScale.isEmpty()) {
int saveCount = canvas.save();
canvas.clipRect(getBounds());
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
} else {
super.draw(canvas);
}
}
}
@@ -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.drawable;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
public class TriangleDrawable extends Drawable {
private final Paint mPaint;
private final Path mPath;
public TriangleDrawable(int color) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(color);
mPath = new Path();
}
public void setColor(int color) {
mPaint.setColor(color);
invalidateSelf();
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mPath.reset();
mPath.moveTo(bounds.left, bounds.top);
mPath.lineTo(bounds.right, bounds.top);
mPath.lineTo(bounds.right, bounds.bottom);
mPath.close();
}
@Override
public void draw(Canvas canvas) {
canvas.drawPath(mPath, mPaint);
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
}
@@ -1,104 +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.drawable;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import com.hippo.widget.ObservedTextView;
import coil.Coil;
import coil.request.Disposable;
import coil.request.ImageRequest;
public class UnikeryDrawable extends WrapDrawable implements ObservedTextView.OnWindowAttachListener {
private static final String TAG = UnikeryDrawable.class.getSimpleName();
private final ObservedTextView mTextView;
private String mUrl;
private Disposable task;
public UnikeryDrawable(ObservedTextView textView) {
mTextView = textView;
mTextView.setOnWindowAttachListener(this);
}
@Override
public void onAttachedToWindow() {
load(mUrl);
}
@Override
public void onDetachedFromWindow() {
if (task != null && !task.isDisposed())
task.dispose();
clearDrawable();
}
public void load(String url) {
if (url != null) {
mUrl = url;
var imageLoader = Coil.imageLoader(mTextView.getContext());
var request = new ImageRequest.Builder(mTextView.getContext()).data(url).memoryCacheKey(url).diskCacheKey(url).target(
drawable -> null,
drawable -> null,
drawable -> {
onGetValue(drawable);
return null;
}
).build();
task = imageLoader.enqueue(request);
}
}
private void clearDrawable() {
setDrawable(null);
}
@Override
public void setDrawable(Drawable drawable) {
// Remove old callback
Drawable oldDrawable = getDrawable();
if (oldDrawable != null) {
oldDrawable.setCallback(null);
}
super.setDrawable(drawable);
if (drawable != null) {
drawable.setCallback(mTextView);
}
updateBounds();
if (drawable != null) {
invalidateSelf();
}
}
@Override
public void invalidateSelf() {
CharSequence cs = mTextView.getText();
mTextView.setText(cs);
}
public void onGetValue(Drawable drawable) {
clearDrawable();
setDrawable(drawable);
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable)
animatedImageDrawable.start();
}
}
@@ -20,12 +20,13 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.ehviewer.util.ReadableTime;
import com.hippo.ehviewer.yorozuya.FileUtils;
import com.hippo.ehviewer.yorozuya.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
@@ -134,7 +135,7 @@ public class AppConfig {
return FileUtils.createTempFile(getTempDir(), null);
}
public static void saveParseErrorBody(ParseException e) {
public static void saveParseErrorBody(@NonNull ParseException e) {
File dir = getExternalParseErrorDir();
if (null == dir) {
return;
@@ -150,9 +151,7 @@ public class AppConfig {
os.write(message.getBytes(StandardCharsets.UTF_8));
os.write('\n');
}
if (null != body) {
os.write(body.getBytes(StandardCharsets.UTF_8));
}
os.write(body.getBytes(StandardCharsets.UTF_8));
os.flush();
} catch (IOException e1) {
// Ignore
@@ -1,202 +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;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Debug;
import androidx.annotation.NonNull;
import com.hippo.util.PackageUtils;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.OSUtils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
public final class Crash {
private Crash() {
}
@NonNull
private static String avoidNull(String str) {
return null != str ? str : "null";
}
private static void collectInfo(Context context, FileWriter fw) throws IOException {
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
String versionName = pi.versionName == null ? "null" : pi.versionName;
String versionCode = String.valueOf(pi.versionCode);
fw.write("======== PackageInfo ========\r\n");
fw.write("PackageName=");
fw.write(pi.packageName);
fw.write("\r\n");
fw.write("VersionName=");
fw.write(versionName);
fw.write("\r\n");
fw.write("VersionCode=");
fw.write(versionCode);
fw.write("\r\n");
String signature = PackageUtils.getSignature(context, pi.packageName);
fw.write("Signature=");
fw.write(null != signature ? signature : "null");
fw.write("\r\n");
fw.write("\r\n");
}
} catch (PackageManager.NameNotFoundException e) {
fw.write("======== PackageInfo ========\r\n");
fw.write("Can't get package information\r\n");
fw.write("\r\n");
}
// Runtime
String topActivityClazzName = "null";
fw.write("======== Runtime ========\r\n");
fw.write("TopActivity=");
fw.write(avoidNull(topActivityClazzName));
fw.write("\r\n");
fw.write("\r\n");
fw.write("\r\n");
// Device info
fw.write("======== DeviceInfo ========\r\n");
fw.write("BOARD=");
fw.write(Build.BOARD);
fw.write("\r\n");
fw.write("BOOTLOADER=");
fw.write(Build.BOOTLOADER);
fw.write("\r\n");
fw.write("SUPPORTED_ABIS=");
fw.write(Arrays.toString(Build.SUPPORTED_ABIS));
fw.write("\r\n");
fw.write("DEVICE=");
fw.write(Build.DEVICE);
fw.write("\r\n");
fw.write("DISPLAY=");
fw.write(Build.DISPLAY);
fw.write("\r\n");
fw.write("FINGERPRINT=");
fw.write(Build.FINGERPRINT);
fw.write("\r\n");
fw.write("HARDWARE=");
fw.write(Build.HARDWARE);
fw.write("\r\n");
fw.write("HOST=");
fw.write(Build.HOST);
fw.write("\r\n");
fw.write("ID=");
fw.write(Build.ID);
fw.write("\r\n");
fw.write("MANUFACTURER=");
fw.write(Build.MANUFACTURER);
fw.write("\r\n");
fw.write("MODEL=");
fw.write(Build.MODEL);
fw.write("\r\n");
fw.write("PRODUCT=");
fw.write(Build.PRODUCT);
fw.write("\r\n");
fw.write("RADIO=");
fw.write(Build.getRadioVersion());
fw.write("\r\n");
fw.write("TAGS=");
fw.write(Build.TAGS);
fw.write("\r\n");
fw.write("TYPE=");
fw.write(Build.TYPE);
fw.write("\r\n");
fw.write("USER=");
fw.write(Build.USER);
fw.write("\r\n");
fw.write("CODENAME=");
fw.write(Build.VERSION.CODENAME);
fw.write("\r\n");
fw.write("INCREMENTAL=");
fw.write(Build.VERSION.INCREMENTAL);
fw.write("\r\n");
fw.write("RELEASE=");
fw.write(Build.VERSION.RELEASE);
fw.write("\r\n");
fw.write("SDK=");
fw.write(Integer.toString(Build.VERSION.SDK_INT));
fw.write("\r\n");
fw.write("MEMORY=");
fw.write(
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false));
fw.write("\r\n");
fw.write("MEMORY_NATIVE=");
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));
fw.write("\r\n");
fw.write("MEMORY_MAX=");
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false));
fw.write("\r\n");
fw.write("MEMORY_TOTAL=");
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false));
fw.write("\r\n");
fw.write("\r\n");
}
private static void getThrowableInfo(Throwable t, FileWriter fw) {
PrintWriter printWriter = new PrintWriter(fw);
t.printStackTrace(printWriter);
Throwable cause = t.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
}
public static void saveCrashLog(Context context, Throwable t) {
File dir = AppConfig.getExternalCrashDir();
if (dir == null) {
return;
}
String nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis());
String fileName = "crash-" + nowString + ".log";
File file = new File(dir, fileName);
FileWriter fw = null;
try {
fw = new FileWriter(file);
fw.write("TIME=");
fw.write(nowString);
fw.write("\r\n");
fw.write("\r\n");
collectInfo(context, fw);
fw.write("======== CrashInfo ========\r\n");
getThrowableInfo(t, fw);
fw.write("\r\n");
fw.flush();
} catch (Exception e) {
file.delete();
} finally {
IOUtils.closeQuietly(fw);
}
}
}
@@ -0,0 +1,96 @@
/*
* 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
import android.os.Build
import android.os.Debug
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.yorozuya.FileUtils
import com.hippo.ehviewer.yorozuya.OSUtils
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter
private fun joinIfStringArray(any: Any?): String {
return if (any is Array<*>) any.joinToString() else any.toString()
}
private fun collectClassStaticInfo(clazz: Class<*>): String {
return clazz.declaredFields.joinToString("\n") {
"${it.name}=${joinIfStringArray(it.get(null))}"
}
}
object Crash {
private fun collectInfo(fw: FileWriter) {
fw.write("======== PackageInfo ========\n")
fw.write("PackageName=${BuildConfig.APPLICATION_ID}\n")
fw.write("VersionName=${BuildConfig.VERSION_NAME}\n")
fw.write("VersionCode=${BuildConfig.VERSION_CODE}\n")
fw.write("CommitSha=${BuildConfig.COMMIT_SHA}\n")
fw.write("BuildTime=${BuildConfig.BUILD_TIME}\n")
fw.write("\n")
// Device info
fw.write("======== DeviceInfo ========\n")
fw.write("${collectClassStaticInfo(Build::class.java)}\n")
fw.write("${collectClassStaticInfo(Build.VERSION::class.java)}\n")
fw.write("MEMORY=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false))
fw.write("\n")
fw.write("MEMORY_NATIVE=")
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false))
fw.write("\n")
fw.write("MEMORY_MAX=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false))
fw.write("\n")
fw.write("MEMORY_TOTAL=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false))
fw.write("\n")
fw.write("\n")
}
private fun getThrowableInfo(t: Throwable, fw: FileWriter) {
val printWriter = PrintWriter(fw)
t.printStackTrace(printWriter)
var cause = t.cause
while (cause != null) {
cause.printStackTrace(printWriter)
cause = cause.cause
}
}
fun saveCrashLog(t: Throwable) {
val dir = AppConfig.getExternalCrashDir() ?: return
val nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis())
val fileName = "crash-$nowString.log"
val file = File(dir, fileName)
runCatching {
FileWriter(file).use { fw ->
fw.write("TIME=${nowString}\n")
fw.write("\n")
collectInfo(fw)
fw.write("======== CrashInfo ========\n")
getThrowableInfo(t, fw)
fw.write("\n")
fw.flush()
}
}.onFailure {
it.printStackTrace()
file.delete()
}
}
}
@@ -16,191 +16,97 @@
package com.hippo.ehviewer
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityManager
import android.app.Application
import android.content.ComponentCallbacks2
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.LruCache
import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import com.hippo.Native
import com.hippo.app.BaseDialogBuilder
import coil.decode.ImageDecoderDecoder
import coil.util.DebugLogger
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhX509TrustManager
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.parser.EventPaneParser
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.coil.diskCache
import com.hippo.ehviewer.dailycheck.checkDawn
import com.hippo.ehviewer.dao.buildMainDB
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.EhActivity
import com.hippo.util.BitmapUtils
import com.hippo.util.ExceptionUtils
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IntIdGenerator
import com.hippo.ehviewer.legacy.cleanObsoleteCache
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.yorozuya.FileUtils
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.io.File
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import java.net.Proxy
import java.security.KeyStore
import java.util.Arrays
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
private val mIdGenerator = IntIdGenerator()
private val mGlobalStuffMap = HashMap<Int, Any>()
private val mActivityList = ArrayList<Activity>()
val topActivity: EhActivity?
get() = if (mActivityList.isNotEmpty()) {
mActivityList[mActivityList.size - 1] as EhActivity
} else {
null
}
fun recreateAllActivity() {
mActivityList.forEach { it.recreate() }
}
@OptIn(DelicateCoroutinesApi::class)
@SuppressLint("StaticFieldLeak")
override fun onCreate() {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
application = this
val handler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
try {
if (Settings.getSaveCrashLog()) {
Crash.saveCrashLog(application, e)
if (Settings.saveCrashLog) {
Crash.saveCrashLog(e)
}
} catch (ignored: Throwable) {
}
handler?.uncaughtException(t, e)
}
super<Application>.onCreate()
Native.initialize()
GetText.initialize(this)
System.loadLibrary("ehviewer")
Settings.initialize()
ReadableTime.initialize(this)
AppConfig.initialize(this)
SpiderDen.initialize(this)
BitmapUtils.initialize(this)
EhTagDatabase.update()
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
AppCompatDelegate.setDefaultNightMode(Settings.theme)
launchIO {
launchIO {
EhTagDatabase.update()
}
launchIO {
ehDatabase
}
launchIO {
downloadManager
DownloadManager.isIdle
}
launchIO {
cleanupDownload()
}
launchIO {
theDawnOfNewDay()
if (Settings.requestNews) {
launchIO {
checkDawn()
}
}
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
cleanObsoleteCache(this)
}
private fun cleanupDownload() {
try {
val downloadLocation = Settings.getDownloadLocation()
if (Settings.getMediaScan()) {
CommonOperations.removeNoMediaFile(downloadLocation)
} else {
CommonOperations.ensureNoMediaFile(downloadLocation)
}
} catch (t: Throwable) {
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
private suspend fun cleanupDownload() {
runCatching {
keepNoMediaFileStatus()
}.onFailure {
it.printStackTrace()
}
// Clear temp files
try {
runCatching {
clearTempDir()
} catch (t: Throwable) {
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
}
}
private fun theDawnOfNewDay() {
if (!Settings.getRequestNews()) {
return
}
val store = ehCookieStore
val eh = EhUrl.HOST_E.toHttpUrl()
if (store.contains(eh, EhCookieStore.KEY_IPD_MEMBER_ID) || store.contains(
eh,
EhCookieStore.KEY_IPD_PASS_HASH
)
) {
val referer = EhUrl.REFERER_E
val request = EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build()
val call = okHttpClient.newCall(request)
try {
call.execute().use { response ->
val responseBody = response.body
val body = responseBody.string()
val html = EventPaneParser.parse(body)
if (html != null) {
showEventPane(html)
}
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
fun showEventPane(html: String) {
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
return
}
val activity = topActivity
activity?.runOnUiThread {
val dialog = BaseDialogBuilder(activity)
.setMessage(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton(android.R.string.ok, null)
.create()
dialog.setOnShowListener {
val messageView = dialog.findViewById<View>(android.R.id.message)
if (messageView is TextView) {
messageView.movementMethod = LinkMovementMethod.getInstance()
}
}
try {
dialog.show()
} catch (t: Throwable) {
// ignore
}
}.onFailure {
it.printStackTrace()
}
}
@@ -222,104 +128,64 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
}
fun putGlobalStuff(o: Any): Int {
val id = mIdGenerator.nextId()
mGlobalStuffMap[id] = o
Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId())
return id
}
fun containGlobalStuff(id: Int): Boolean {
return mGlobalStuffMap.containsKey(id)
}
fun getGlobalStuff(id: Int): Any? {
return mGlobalStuffMap[id]
}
fun removeGlobalStuff(id: Int): Any? {
return mGlobalStuffMap.remove(id)
}
fun removeGlobalStuff(o: Any) {
mGlobalStuffMap.values.removeAll(setOf(o))
}
fun registerActivity(activity: Activity) {
mActivityList.add(activity)
}
fun unregisterActivity(activity: Activity) {
mActivityList.remove(activity)
}
override fun onPause(owner: LifecycleOwner) {
if (!locked)
if (!locked) {
locked_last_leave_time = System.currentTimeMillis() / 1000
}
locked = true
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply {
okHttpClient(okHttpClient)
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
// Coil spawns a new thread for every image load by default
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
okHttpClient(nonCacheOkHttpClient)
components {
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
add(MergeInterceptor)
}
diskCache(imageCache)
crossfade(300)
error(R.drawable.image_failed)
if (BuildConfig.DEBUG) logger(DebugLogger())
}.build()
}
companion object {
private const val KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id"
var locked = true
var locked_last_leave_time: Long = 0
@JvmStatic
lateinit var application: EhApplication
private set
@JvmStatic
val ehCookieStore by lazy { EhCookieStore }
@JvmStatic
val ehProxySelector by lazy { EhProxySelector() }
@JvmStatic
val okHttpClient by lazy {
val builder = OkHttpClient.Builder()
.cookieJar(ehCookieStore)
.cache(Cache(File(application.cacheDir, "http_cache"), 50L * 1024L * 1024L))
.dns(EhDns)
.proxySelector(ehProxySelector)
if (Settings.getDF()) {
var trustManager: X509TrustManager
try {
val trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
)
trustManagerFactory.init(null as KeyStore?)
val trustManagers = trustManagerFactory.trustManagers
check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) {
"Unexpected default trust managers:" + Arrays.toString(
trustManagers
)
}
trustManager = trustManagers[0] as X509TrustManager
} catch (e: Exception) {
e.printStackTrace()
trustManager = EhX509TrustManager
val nonCacheOkHttpClient by lazy {
OkHttpClient.Builder().apply {
cookieJar(EhCookieStore)
dns(EhDns)
proxySelector(ehProxySelector)
if (Settings.dF) {
val factory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())!!
factory.init(null as KeyStore?)
val manager = factory.trustManagers!!
val trustManager = manager.filterIsInstance<X509TrustManager>().first()
sslSocketFactory(EhSSLSocketFactory, trustManager)
proxy(Proxy.NO_PROXY)
}
}.build()
}
builder.sslSocketFactory(EhSSLSocketFactory, trustManager)
builder.proxy(Proxy.NO_PROXY)
}
builder.build()
// Never use this okhttp client to download large blobs!!!
val okHttpClient by lazy {
nonCacheOkHttpClient.newBuilder().cache(
Cache(
application.cacheDir.toOkioPath() / "http_cache",
20L * 1024L * 1024L,
FileSystem.SYSTEM,
),
).build()
}
@JvmStatic
val galleryDetailCache by lazy {
LruCache<Long, GalleryDetail>(25).also {
favouriteStatusRouter.addListener { gid, slot ->
@@ -328,25 +194,19 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
}
@JvmStatic
val spiderInfoCache by lazy {
DiskCache.Builder().directory(File(application.cacheDir, "spider_info"))
.maxSizeBytes(20 * 1024 * 1024).build()
}
@JvmStatic
val downloadManager by lazy { DownloadManager() }
@JvmStatic
val hosts by lazy { Hosts(application, "hosts.db") }
@JvmStatic
val favouriteStatusRouter by lazy { FavouriteStatusRouter() }
@JvmStatic
val readerPreferences by lazy { ReaderPreferences(AndroidPreferenceStore(application)) }
@JvmStatic
val ehDatabase by lazy { buildMainDB(application) }
val imageCache by lazy {
diskCache {
directory(application.cacheDir.toOkioPath() / "image_cache")
maxSizeBytes(Settings.readCacheSize.coerceIn(320, 5120).toLong() * 1024 * 1024)
}
}
}
}
@@ -1,595 +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;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagingSource;
import androidx.room.Room;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.BasicDao;
import com.hippo.ehviewer.dao.DownloadDirname;
import com.hippo.ehviewer.dao.DownloadDirnameDao;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.dao.DownloadLabel;
import com.hippo.ehviewer.dao.DownloadLabelDao;
import com.hippo.ehviewer.dao.DownloadsDao;
import com.hippo.ehviewer.dao.EhDatabase;
import com.hippo.ehviewer.dao.Filter;
import com.hippo.ehviewer.dao.HistoryDao;
import com.hippo.ehviewer.dao.HistoryInfo;
import com.hippo.ehviewer.dao.LocalFavoriteInfo;
import com.hippo.ehviewer.dao.LocalFavoritesDao;
import com.hippo.ehviewer.dao.QuickSearch;
import com.hippo.ehviewer.dao.QuickSearchDao;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.util.ExceptionUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.ObjectUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class EhDB {
private static final int CUR_DB_VER = 4;
private static final EhDatabase db = EhApplication.getEhDatabase();
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
switch (oldVersion) {
case 1: // 1 to 2, add FILTER
db.execSQL("CREATE TABLE IF NOT EXISTS \"FILTER\" (" + //
"\"_id\" INTEGER PRIMARY KEY ," + // 0: id
"\"MODE\" INTEGER NOT NULL ," + // 1: mode
"\"TEXT\" TEXT," + // 2: text
"\"ENABLE\" INTEGER);"); // 3: enable
case 2: // 2 to 3, add ENABLE column to table FILTER
db.execSQL("CREATE TABLE " + "\"FILTER2\" (" +
"\"_id\" INTEGER PRIMARY KEY ," +
"\"MODE\" INTEGER NOT NULL ," +
"\"TEXT\" TEXT," +
"\"ENABLE\" INTEGER);");
db.execSQL("INSERT INTO \"FILTER2\" (" +
"_id, MODE, TEXT, ENABLE)" +
"SELECT _id, MODE, TEXT, 1 FROM FILTER;");
db.execSQL("DROP TABLE FILTER");
db.execSQL("ALTER TABLE FILTER2 RENAME TO FILTER");
case 3: // 3 to 4, add PAGE_FROM and PAGE_TO column to QUICK_SEARCH
db.execSQL("CREATE TABLE " + "\"QUICK_SEARCH2\" (" +
"\"_id\" INTEGER PRIMARY KEY ," +
"\"NAME\" TEXT," +
"\"MODE\" INTEGER NOT NULL ," +
"\"CATEGORY\" INTEGER NOT NULL ," +
"\"KEYWORD\" TEXT," +
"\"ADVANCE_SEARCH\" INTEGER NOT NULL ," +
"\"MIN_RATING\" INTEGER NOT NULL ," +
"\"PAGE_FROM\" INTEGER NOT NULL ," +
"\"PAGE_TO\" INTEGER NOT NULL ," +
"\"TIME\" INTEGER NOT NULL );");
db.execSQL("INSERT INTO \"QUICK_SEARCH2\" (" +
"_id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, PAGE_FROM, PAGE_TO, TIME)" +
"SELECT _id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, -1, -1, TIME FROM QUICK_SEARCH;");
db.execSQL("DROP TABLE QUICK_SEARCH");
db.execSQL("ALTER TABLE QUICK_SEARCH2 RENAME TO QUICK_SEARCH");
}
}
public static synchronized List<DownloadInfo> getAllDownloadInfo() {
DownloadsDao dao = db.downloadsDao();
List<DownloadInfo> list = dao.list();
// Fix state
for (DownloadInfo info : list) {
if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {
info.state = DownloadInfo.STATE_NONE;
}
}
return list;
}
public static synchronized void moveDownloadInfo(int fromPosition, int toPosition) {
if (fromPosition == toPosition) {
return;
}
boolean reverse = fromPosition > toPosition;
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
DownloadsDao dao = db.downloadsDao();
List<DownloadInfo> list = dao.list(offset, limit);
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
int end = reverse ? 0 : limit - 1;
long toTime = list.get(end).time;
for (int i = end; reverse ? i < start : i > start; i += step) {
list.get(i).time = list.get(i + step).time;
}
list.get(start).time = toTime;
dao.update(list);
}
// Insert or update
public static synchronized void putDownloadInfo(DownloadInfo downloadInfo) {
DownloadsDao dao = db.downloadsDao();
if (null != dao.load(downloadInfo.getGid())) {
// Update
dao.update(downloadInfo);
} else {
// Insert
dao.insert(downloadInfo);
}
}
public static synchronized void removeDownloadInfo(DownloadInfo downloadInfo) {
db.downloadsDao().delete(downloadInfo);
}
@Nullable
public static synchronized String getDownloadDirname(long gid) {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) {
return raw.getDirname();
} else {
return null;
}
}
/**
* Insert or update
*/
public static synchronized void putDownloadDirname(long gid, String dirname) {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) { // Update
raw.setDirname(dirname);
dao.update(raw);
} else { // Insert
raw = new DownloadDirname();
raw.setGid(gid);
raw.setDirname(dirname);
dao.insert(raw);
}
}
public static synchronized void removeDownloadDirname(long gid) {
DownloadDirnameDao dao = db.downloadDirnameDao();
dao.deleteByKey(gid);
}
public static synchronized void clearDownloadDirname() {
DownloadDirnameDao dao = db.downloadDirnameDao();
dao.deleteAll();
}
@NonNull
public static synchronized List<DownloadLabel> getAllDownloadLabelList() {
DownloadLabelDao dao = db.downloadLabelDao();
return dao.list();
}
public static synchronized DownloadLabel addDownloadLabel(String label) {
DownloadLabelDao dao = db.downloadLabelDao();
DownloadLabel raw = new DownloadLabel();
raw.setLabel(label);
raw.setTime(System.currentTimeMillis());
raw.setId(dao.insert(raw));
return raw;
}
public static synchronized DownloadLabel addDownloadLabel(DownloadLabel raw) {
// Reset id
raw.setId(null);
DownloadLabelDao dao = db.downloadLabelDao();
raw.setId(dao.insert(raw));
return raw;
}
public static synchronized void updateDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = db.downloadLabelDao();
dao.update(raw);
}
public static synchronized void moveDownloadLabel(int fromPosition, int toPosition) {
if (fromPosition == toPosition) {
return;
}
boolean reverse = fromPosition > toPosition;
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
DownloadLabelDao dao = db.downloadLabelDao();
List<DownloadLabel> list = dao.list(offset, limit);
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
int end = reverse ? 0 : limit - 1;
long toTime = list.get(end).getTime();
for (int i = end; reverse ? i < start : i > start; i += step) {
list.get(i).setTime(list.get(i + step).getTime());
}
list.get(start).setTime(toTime);
dao.update(list);
}
public static synchronized void removeDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = db.downloadLabelDao();
dao.delete(raw);
}
public static synchronized List<GalleryInfo> getAllLocalFavorites() {
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list();
return new ArrayList<>(list);
}
public static synchronized List<GalleryInfo> searchLocalFavorites(String query) {
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list("%" + query + "%");
return new ArrayList<>(list);
}
public static synchronized void removeLocalFavorites(long gid) {
db.localFavoritesDao().deleteByKey(gid);
}
public static synchronized void removeLocalFavorites(long[] gidArray) {
LocalFavoritesDao dao = db.localFavoritesDao();
for (long gid : gidArray) {
dao.deleteByKey(gid);
}
}
public static synchronized boolean containLocalFavorites(long gid) {
LocalFavoritesDao dao = db.localFavoritesDao();
return null != dao.load(gid);
}
public static synchronized void putLocalFavorites(GalleryInfo galleryInfo) {
LocalFavoritesDao dao = db.localFavoritesDao();
if (null == dao.load(galleryInfo.getGid())) {
LocalFavoriteInfo info;
if (galleryInfo instanceof LocalFavoriteInfo) {
info = (LocalFavoriteInfo) galleryInfo;
} else {
info = new LocalFavoriteInfo(galleryInfo);
info.time = System.currentTimeMillis();
}
dao.insert(info);
}
}
public static synchronized void putLocalFavorites(List<GalleryInfo> galleryInfoList) {
for (GalleryInfo gi : galleryInfoList) {
putLocalFavorites(gi);
}
}
public static synchronized List<QuickSearch> getAllQuickSearch() {
QuickSearchDao dao = db.quickSearchDao();
return dao.list();
}
public static synchronized void insertQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = db.quickSearchDao();
quickSearch.setId(null);
quickSearch.setTime(System.currentTimeMillis());
quickSearch.setId(dao.insert(quickSearch));
}
public static synchronized void importQuickSearch(List<QuickSearch> quickSearchList) {
QuickSearchDao dao = db.quickSearchDao();
for (QuickSearch quickSearch : quickSearchList) {
dao.insert(quickSearch);
}
}
public static synchronized void deleteQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = db.quickSearchDao();
dao.delete(quickSearch);
}
public static synchronized void moveQuickSearch(int fromPosition, int toPosition) {
if (fromPosition == toPosition) {
return;
}
boolean reverse = fromPosition > toPosition;
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
QuickSearchDao dao = db.quickSearchDao();
List<QuickSearch> list = dao.list(offset, limit);
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
int end = reverse ? 0 : limit - 1;
long toTime = list.get(end).getTime();
for (int i = end; reverse ? i < start : i > start; i += step) {
list.get(i).setTime(list.get(i + step).getTime());
}
list.get(start).setTime(toTime);
dao.update(list);
}
public static synchronized PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
return db.historyDao().listLazy();
}
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
HistoryDao dao = db.historyDao();
HistoryInfo info;
if (galleryInfo instanceof HistoryInfo) {
info = (HistoryInfo) galleryInfo;
} else {
info = new HistoryInfo(galleryInfo);
}
info.time = System.currentTimeMillis();
if (null != dao.load(info.getGid())) {
dao.update(info);
} else {
dao.insert(info);
}
}
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
HistoryDao dao = db.historyDao();
for (HistoryInfo info : historyInfoList) {
if (null == dao.load(info.getGid())) {
dao.insert(info);
}
}
}
public static synchronized void deleteHistoryInfo(HistoryInfo info) {
HistoryDao dao = db.historyDao();
dao.delete(info);
}
public static synchronized void clearHistoryInfo() {
HistoryDao dao = db.historyDao();
dao.deleteAll();
}
public static synchronized List<Filter> getAllFilter() {
return db.filterDao().list();
}
public static synchronized boolean addFilter(Filter filter) {
Filter existFilter;
try {
existFilter = db.filterDao().load(filter.text, filter.mode);
} catch (Exception e) {
existFilter = null;
}
if (existFilter == null) {
filter.setId(null);
filter.setId(db.filterDao().insert(filter));
return true;
} else {
return false;
}
}
public static synchronized void deleteFilter(Filter filter) {
db.filterDao().delete(filter);
}
public static synchronized void triggerFilter(Filter filter) {
filter.enable = !filter.enable;
db.filterDao().update(filter);
}
private static <T> void copyDao(BasicDao<T> from, BasicDao<T> to) {
List<T> list = from.list();
for (T item : list)
to.insert(item);
}
public static synchronized boolean exportDB(Context context, Uri uri) {
final String ehExportName = "eh.export.db";
// Delete old export db
context.deleteDatabase(ehExportName);
EhDatabase newDb = Room.databaseBuilder(context, EhDatabase.class, ehExportName).allowMainThreadQueries().build();
try {
// Copy data to a export db
copyDao(db.downloadsDao(), newDb.downloadsDao());
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao());
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao());
copyDao(db.historyDao(), newDb.historyDao());
copyDao(db.quickSearchDao(), newDb.quickSearchDao());
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao());
copyDao(db.filterDao(), newDb.filterDao());
// Close export db so we can copy it
if (newDb.isOpen()) {
newDb.close();
}
// Copy export db to data dir
File dbFile = context.getDatabasePath(ehExportName);
if (dbFile == null || !dbFile.isFile()) {
return false;
}
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(dbFile);
os = context.getContentResolver().openOutputStream(uri);
IOUtils.copy(is, os);
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
}
// Delete failed file
return false;
} finally {
context.deleteDatabase(ehExportName);
}
}
/**
* @return error string, null for no error
*/
public static synchronized String importDB(Context context, Uri uri) {
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
// Copy file to cache dir
File file = File.createTempFile("importDatabase", "");
FileOutputStream outputStream = new FileOutputStream(file);
byte[] buff = new byte[1024];
int read;
if (inputStream != null) {
while ((read = inputStream.read(buff, 0, buff.length)) > 0) {
outputStream.write(buff, 0, read);
}
} else {
return context.getString(R.string.cant_read_the_file);
}
inputStream.close();
outputStream.close();
// Check database version
SQLiteDatabase oldDB = SQLiteDatabase.openDatabase(
file.getPath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
int newVersion = CUR_DB_VER;
int oldVersion = oldDB.getVersion();
if (oldVersion < newVersion) {
upgradeDB(oldDB, oldVersion);
oldDB.setVersion(newVersion);
} else if (oldVersion > newVersion) {
return context.getString(R.string.db_version_too_high);
}
// Crete temp room database from cache file
String tmpDBName = "tmp.db";
context.deleteDatabase(tmpDBName);
EhDatabase oldRoomDatabase = Room.databaseBuilder(context, EhDatabase.class, tmpDBName)
.createFromFile(file).allowMainThreadQueries().build();
// Download label
DownloadManager manager = EhApplication.getDownloadManager();
try {
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
manager.addDownloadLabel(downloadLabelList);
} catch (Exception ignored) {
}
// Downloads
try {
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
manager.addDownload(downloadInfoList, false);
} catch (Exception ignored) {
}
// Download dirname
try {
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
for (DownloadDirname dirname : downloadDirnameList) {
putDownloadDirname(dirname.getGid(), dirname.getDirname());
}
} catch (Exception ignored) {
}
// History
try {
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
putHistoryInfo(historyInfoList);
} catch (Exception ignored) {
}
// QuickSearch
try {
List<QuickSearch> quickSearchList = oldRoomDatabase.quickSearchDao().list();
List<QuickSearch> currentQuickSearchList = db.quickSearchDao().list();
List<QuickSearch> importList = new ArrayList<>();
for (QuickSearch quickSearch : quickSearchList) {
String name = quickSearch.getName();
for (QuickSearch q : currentQuickSearchList) {
if (ObjectUtils.equal(q.getName(), name)) {
// The same name
name = null;
break;
}
}
if (null == name) {
continue;
}
importList.add(quickSearch);
}
importQuickSearch(importList);
} catch (Exception ignored) {
}
// LocalFavorites
try {
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
for (LocalFavoriteInfo info : localFavoriteInfoList) {
putLocalFavorites(info);
}
} catch (Exception ignored) {
}
// Filter
try {
List<Filter> filterList = oldRoomDatabase.filterDao().list();
List<Filter> currentFilterList = db.filterDao().list();
for (Filter filter : filterList) {
if (!currentFilterList.contains(filter)) {
addFilter(filter);
}
}
} catch (Exception ignored) {
}
// Delete temp database
if (oldRoomDatabase.isOpen()) {
oldRoomDatabase.close();
}
context.deleteDatabase(tmpDBName);
return null;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
return context.getString(R.string.cant_read_the_file);
}
}
}
@@ -0,0 +1,465 @@
/*
* 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
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import androidx.paging.PagingSource
import androidx.room.Room.databaseBuilder
import com.hippo.ehviewer.EhApplication.Companion.ehDatabase
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.BasicDao
import com.hippo.ehviewer.dao.DownloadDirname
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.dao.EHDB_MIGRATION_4_5
import com.hippo.ehviewer.dao.EhDatabase
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.HistoryInfo
import com.hippo.ehviewer.dao.LocalFavoriteInfo
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.util.sendTo
object EhDB {
private const val CUR_DB_VER = 4
private val db = ehDatabase
// Fix state
@get:Synchronized
val allDownloadInfo: List<DownloadInfo>
get() = db.downloadsDao().list().onEach {
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
it.state = DownloadInfo.STATE_NONE
}
}
@Synchronized
fun updateDownloadInfo(downloadInfos: List<DownloadInfo>) {
val dao = db.downloadsDao()
dao.update(downloadInfos)
}
@Synchronized
fun putDownloadInfo(downloadInfo: DownloadInfo) {
db.downloadsDao().run {
if (load(downloadInfo.gid) != null) {
update(downloadInfo)
} else {
insert(downloadInfo)
}
}
}
@Synchronized
fun removeDownloadInfo(downloadInfo: DownloadInfo) {
db.downloadsDao().delete(downloadInfo)
}
@Synchronized
fun getDownloadDirname(gid: Long): String? {
val dao = db.downloadDirnameDao()
val raw = dao.load(gid)
return raw?.dirname
}
@Synchronized
fun putDownloadDirname(gid: Long, dirname: String?) {
val dao = db.downloadDirnameDao()
var raw = dao.load(gid)
if (raw != null) {
raw.dirname = dirname
dao.update(raw)
} else {
raw = DownloadDirname(gid, dirname)
dao.insert(raw)
}
}
@Synchronized
fun removeDownloadDirname(gid: Long) {
val dao = db.downloadDirnameDao()
dao.deleteByKey(gid)
}
@Synchronized
fun clearDownloadDirname() {
val dao = db.downloadDirnameDao()
dao.deleteAll()
}
@get:Synchronized
val allDownloadLabelList: List<DownloadLabel>
get() = db.downloadLabelDao().list()
@Synchronized
fun addDownloadLabel(label: String): DownloadLabel {
val dao = db.downloadLabelDao()
val raw = DownloadLabel()
raw.label = label
raw.time = System.currentTimeMillis()
raw.id = dao.insert(raw)
return raw
}
@Synchronized
fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
// Reset id
raw.id = null
val dao = db.downloadLabelDao()
raw.id = dao.insert(raw)
return raw
}
@Synchronized
fun updateDownloadLabel(raw: DownloadLabel?) {
val dao = db.downloadLabelDao()
dao.update(raw!!)
}
@Synchronized
fun moveDownloadLabel(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) {
return
}
val reverse = fromPosition > toPosition
val offset = if (reverse) toPosition else fromPosition
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
val dao = db.downloadLabelDao()
val list = dao.list(offset, limit)
val step = if (reverse) 1 else -1
val start = if (reverse) limit - 1 else 0
val end = if (reverse) 0 else limit - 1
val toTime = list[end].time
var i = end
while (if (reverse) i < start else i > start) {
list[i].time = list[i + step].time
i += step
}
list[start].time = toTime
dao.update(list)
}
@Synchronized
fun removeDownloadLabel(raw: DownloadLabel?) {
val dao = db.downloadLabelDao()
dao.delete(raw!!)
}
@get:Synchronized
val allLocalFavorites: List<GalleryInfo>
get() {
val dao = db.localFavoritesDao()
val list = dao.list()
return ArrayList<GalleryInfo>(list)
}
@Synchronized
fun searchLocalFavorites(query: String): List<GalleryInfo> {
val dao = db.localFavoritesDao()
val list = dao.list("%$query%")
return ArrayList<GalleryInfo>(list)
}
@Synchronized
fun removeLocalFavorites(gid: Long) {
db.localFavoritesDao().deleteByKey(gid)
}
@Synchronized
fun removeLocalFavorites(gidArray: LongArray) {
val dao = db.localFavoritesDao()
for (gid in gidArray) {
dao.deleteByKey(gid)
}
}
@JvmStatic
@Synchronized
fun containLocalFavorites(gid: Long): Boolean {
val dao = db.localFavoritesDao()
return dao.contains(gid)
}
@Synchronized
fun putLocalFavorites(galleryInfo: GalleryInfo) {
val dao = db.localFavoritesDao()
if (null == dao.load(galleryInfo.gid)) {
val info: LocalFavoriteInfo
if (galleryInfo is LocalFavoriteInfo) {
info = galleryInfo
} else {
info = LocalFavoriteInfo(galleryInfo)
info.time = System.currentTimeMillis()
}
dao.insert(info)
}
}
@Synchronized
fun putLocalFavorites(galleryInfoList: List<GalleryInfo>) {
for (gi in galleryInfoList) {
putLocalFavorites(gi)
}
}
@get:Synchronized
val allQuickSearch: List<QuickSearch>
get() {
val dao = db.quickSearchDao()
return dao.list()
}
@Synchronized
fun insertQuickSearch(quickSearch: QuickSearch) {
val dao = db.quickSearchDao()
quickSearch.id = null
quickSearch.time = System.currentTimeMillis()
quickSearch.id = dao.insert(quickSearch)
}
@Synchronized
fun importQuickSearch(quickSearchList: List<QuickSearch?>) {
val dao = db.quickSearchDao()
for (quickSearch in quickSearchList) {
dao.insert(quickSearch!!)
}
}
@Synchronized
fun deleteQuickSearch(quickSearch: QuickSearch?) {
val dao = db.quickSearchDao()
if (quickSearch != null) {
dao.delete(quickSearch)
}
}
@Synchronized
fun moveQuickSearch(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) {
return
}
val reverse = fromPosition > toPosition
val offset = if (reverse) toPosition else fromPosition
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
val dao = db.quickSearchDao()
val list = dao.list(offset, limit)
val step = if (reverse) 1 else -1
val start = if (reverse) limit - 1 else 0
val end = if (reverse) 0 else limit - 1
val toTime = list[end].time
var i = end
while (if (reverse) i < start else i > start) {
list[i].time = list[i + step].time
i += step
}
list[start].time = toTime
dao.update(list)
}
@get:Synchronized
val historyLazyList: PagingSource<Int, HistoryInfo>
get() = db.historyDao().listLazy()
@Synchronized
fun putHistoryInfo(galleryInfo: GalleryInfo?) {
val dao = db.historyDao()
val info: HistoryInfo = if (galleryInfo is HistoryInfo) {
galleryInfo
} else {
HistoryInfo(galleryInfo!!)
}
info.time = System.currentTimeMillis()
if (null != dao.load(info.gid)) {
dao.update(info)
} else {
dao.insert(info)
}
}
@Synchronized
fun putHistoryInfoNonRefresh(info: GalleryInfo) {
val dao = db.historyDao()
val i = dao.load(info.gid)
if (null != i) {
val historyInfo: HistoryInfo
if (info is HistoryInfo) {
historyInfo = info
} else {
historyInfo = HistoryInfo(info)
historyInfo.time = i.time
}
dao.update(historyInfo)
}
}
@Synchronized
fun putHistoryInfo(historyInfoList: List<HistoryInfo>) {
val dao = db.historyDao()
for (info in historyInfoList) {
if (null == dao.load(info.gid)) {
dao.insert(info)
}
}
}
@Synchronized
fun deleteHistoryInfo(info: HistoryInfo?) {
val dao = db.historyDao()
dao.delete(info!!)
}
@Synchronized
fun clearHistoryInfo() {
val dao = db.historyDao()
dao.deleteAll()
}
@get:Synchronized
val allFilter: List<Filter>
get() = db.filterDao().list()
@Synchronized
fun addFilter(filter: Filter): Boolean {
val existFilter: Filter? = try {
db.filterDao().load(filter.text!!, filter.mode)
} catch (e: Exception) {
null
}
return if (existFilter == null) {
filter.id = null
filter.id = db.filterDao().insert(filter)
true
} else {
false
}
}
@Synchronized
fun deleteFilter(filter: Filter) {
db.filterDao().delete(filter)
}
@Synchronized
fun triggerFilter(filter: Filter) {
filter.enable = filter.enable?.not() ?: false
db.filterDao().update(filter)
}
private fun <T> copyDao(from: BasicDao<T>, to: BasicDao<T>) {
val list = from.list()
for (item in list) to.insert(item)
}
@Synchronized
fun exportDB(context: Context, uri: Uri): Boolean {
val ehExportName = "eh.export.db"
runCatching {
context.deleteDatabase(ehExportName)
val newDb = databaseBuilder(context, EhDatabase::class.java, ehExportName).build()
copyDao(db.downloadsDao(), newDb.downloadsDao())
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao())
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao())
copyDao(db.historyDao(), newDb.historyDao())
copyDao(db.quickSearchDao(), newDb.quickSearchDao())
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao())
copyDao(db.filterDao(), newDb.filterDao())
newDb.close()
val dbFile = context.getDatabasePath(ehExportName)
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
ParcelFileDescriptor.open(dbFile, MODE_READ_ONLY).use { fromFd ->
fromFd sendTo toFd
}
}
return true
}.onFailure {
it.printStackTrace()
}
return false
}
/**
* @return error string, null for no error
*/
@Synchronized
fun importDB(context: Context, uri: Uri): String? {
runCatching {
val oldDB = databaseBuilder(context, EhDatabase::class.java, "tmp.db")
.createFromInputStream { context.contentResolver.openInputStream(uri) }
.addMigrations(EHDB_MIGRATION_4_5)
.build()
// Download label
val manager = DownloadManager
runCatching {
val downloadLabelList = oldDB.downloadLabelDao().list()
manager.addDownloadLabel(downloadLabelList)
}
// Downloads
runCatching {
val downloadInfoList = oldDB.downloadsDao().list()
manager.addDownload(downloadInfoList, false)
}
// Download dirname
runCatching {
oldDB.downloadDirnameDao().list().forEach {
putDownloadDirname(it.gid, it.dirname)
}
}
// History
runCatching {
val historyInfoList = oldDB.historyDao().list()
putHistoryInfo(historyInfoList)
}
// QuickSearch
runCatching {
val quickSearchList = oldDB.quickSearchDao().list()
val currentQuickSearchList = db.quickSearchDao().list()
val importList = quickSearchList.mapNotNull { newQS ->
newQS.takeIf { currentQuickSearchList.find { it.name == newQS.name } == null }
}
importQuickSearch(importList)
}
// LocalFavorites
runCatching {
oldDB.localFavoritesDao().list().forEach {
putLocalFavorites(it)
}
}
// Filter
runCatching {
val filterList = oldDB.filterDao().list()
val currentFilterList = db.filterDao().list()
filterList.forEach {
if (it !in currentFilterList) addFilter(it)
}
}
oldDB.close()
}.onFailure {
it.printStackTrace()
return context.getString(R.string.cant_read_the_file)
}
return null
}
}
@@ -1,110 +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;
import android.text.TextUtils;
import com.hippo.network.InetValidator;
import com.hippo.util.ExceptionUtils;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.Collections;
import java.util.List;
public class EhProxySelector extends ProxySelector {
public static final int TYPE_DIRECT = 0;
public static final int TYPE_SYSTEM = 1;
public static final int TYPE_HTTP = 2;
public static final int TYPE_SOCKS = 3;
private ProxySelector delegation;
private ProxySelector alternative;
EhProxySelector() {
alternative = ProxySelector.getDefault();
if (alternative == null) {
alternative = new NullProxySelector();
}
updateProxy();
}
public void updateProxy() {
switch (Settings.getProxyType()) {
case TYPE_DIRECT:
delegation = new NullProxySelector();
break;
default:
case TYPE_SYSTEM:
delegation = alternative;
break;
case TYPE_HTTP:
case TYPE_SOCKS:
delegation = null;
break;
}
}
@Override
public List<Proxy> select(URI uri) {
int type = Settings.getProxyType();
if (type == TYPE_HTTP || type == TYPE_SOCKS) {
try {
String ip = Settings.getProxyIp();
int port = Settings.getProxyPort();
if (!TextUtils.isEmpty(ip) && InetValidator.isValidInetPort(port)) {
InetAddress inetAddress = InetAddress.getByName(ip);
SocketAddress socketAddress = new InetSocketAddress(inetAddress, port);
return Collections.singletonList(new Proxy(type == TYPE_HTTP ? Proxy.Type.HTTP : Proxy.Type.SOCKS, socketAddress));
}
} catch (Throwable t) {
ExceptionUtils.throwIfFatal(t);
}
}
if (delegation != null) {
return delegation.select(uri);
}
return alternative.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
if (delegation != null) {
delegation.select(uri);
}
}
private static class NullProxySelector extends ProxySelector {
@Override
public List<Proxy> select(URI uri) {
return Collections.singletonList(Proxy.NO_PROXY);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
}
}
}
@@ -0,0 +1,92 @@
/*
* 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
import android.text.TextUtils
import com.hippo.ehviewer.Settings.proxyIp
import com.hippo.ehviewer.Settings.proxyPort
import com.hippo.ehviewer.Settings.proxyType
import com.hippo.ehviewer.network.InetValidator.isValidInetPort
import com.hippo.ehviewer.util.ExceptionUtils
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.ProxySelector
import java.net.SocketAddress
import java.net.URI
class EhProxySelector internal constructor() : ProxySelector() {
private var delegation: ProxySelector? = null
private var alternative: ProxySelector?
init {
alternative = getDefault() ?: NullProxySelector()
updateProxy()
}
fun updateProxy() {
delegation = when (proxyType) {
TYPE_DIRECT -> NullProxySelector()
TYPE_SYSTEM -> alternative
TYPE_HTTP, TYPE_SOCKS -> null
else -> alternative
}
}
override fun select(uri: URI): List<Proxy> {
val type = proxyType
if (type == TYPE_HTTP || type == TYPE_SOCKS) {
runCatching {
val ip = proxyIp
val port = proxyPort
if (!TextUtils.isEmpty(ip) && isValidInetPort(port)) {
val inetAddress = InetAddress.getByName(ip)
val socketAddress = InetSocketAddress(inetAddress, port)
return listOf(
Proxy(
if (type == TYPE_HTTP) Proxy.Type.HTTP else Proxy.Type.SOCKS,
socketAddress,
),
)
}
}.onFailure {
ExceptionUtils.throwIfFatal(it)
it.printStackTrace()
}
}
return delegation?.select(uri) ?: alternative!!.select(uri)
}
override fun connectFailed(uri: URI, sa: SocketAddress, ioe: IOException) {
delegation?.select(uri)
}
private class NullProxySelector : ProxySelector() {
override fun select(uri: URI): List<Proxy> {
return listOf(Proxy.NO_PROXY)
}
override fun connectFailed(uri: URI, sa: SocketAddress, ioe: IOException) {}
}
companion object {
const val TYPE_DIRECT = 0
const val TYPE_SYSTEM = 1
const val TYPE_HTTP = 2
const val TYPE_SOCKS = 3
}
}
@@ -19,7 +19,7 @@ package com.hippo.ehviewer;
import android.annotation.SuppressLint;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.yorozuya.IntIdGenerator;
import com.hippo.ehviewer.yorozuya.IntIdGenerator;
import java.util.ArrayList;
import java.util.HashMap;
@@ -1,42 +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;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
public class GetText {
private static Resources sResources;
public static void initialize(Context context) {
sResources = context.getApplicationContext().getResources();
}
@NonNull
public static String getString(@StringRes int id) {
return sResources.getString(id);
}
@NonNull
public static String getString(@StringRes int id, Object... formatArgs) {
return sResources.getString(id, formatArgs);
}
}
@@ -13,6 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.exception
package com.hippo.ehviewer
class Image509Exception : EhException("509")
import androidx.annotation.StringRes
object GetText {
fun getString(@StringRes id: Int): String {
return EhApplication.application.getString(id)
}
fun getString(@StringRes id: Int, vararg formatArgs: Any?): String {
return EhApplication.application.getString(id, *formatArgs)
}
}
@@ -29,8 +29,8 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import com.hippo.database.MSQLiteBuilder;
import com.hippo.util.SqlUtils;
import com.hippo.ehviewer.database.MSQLiteBuilder;
import com.hippo.ehviewer.util.SqlUtils;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -1,739 +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;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import com.hippo.ehviewer.client.EhConfig;
import com.hippo.ehviewer.client.data.FavListUrlBuilder;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.ehviewer.ui.scene.GalleryListScene;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.NumberUtils;
import java.util.Locale;
import java.util.Set;
public class Settings {
/********************
****** Eh
********************/
public static final String KEY_THEME = "theme";
public static final String KEY_ACCOUNT = "account";
public static final String KEY_BLACK_DARK_THEME = "black_dark_theme";
public static final int THEME_SYSTEM = -1;
public static final String KEY_GALLERY_SITE = "gallery_site";
public static final String KEY_LIST_MODE = "list_mode";
public static final String KEY_DETAIL_SIZE = "detail_size";
public static final String KEY_THUMB_SIZE = "thumb_size_";
public static final String KEY_THUMB_RESOLUTION = "thumb_resolution";
public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations";
public static final String KEY_DEFAULT_CATEGORIES = "default_categories";
public static final int DEFAULT_DEFAULT_CATEGORIES = EhConfig.ALL_CATEGORY;
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
/********************
****** Privacy and Security
********************/
public static final String KEY_SEC_SECURITY = "enable_secure";
public static final boolean VALUE_SEC_SECURITY = false;
/********************
****** Download
********************/
public static final String KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme";
public static final String KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority";
public static final String KEY_DOWNLOAD_SAVE_PATH = "image_path";
public static final String KEY_NOTIFICATION_REQUIRED = "notification_required";
public static final String KEY_DOWNLOAD_SAVE_QUERY = "image_query";
public static final String KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment";
public static final String KEY_MEDIA_SCAN = "media_scan";
public static final String KEY_IMAGE_RESOLUTION = "image_size";
public static final String DEFAULT_IMAGE_RESOLUTION = EhConfig.IMAGE_SIZE_AUTO;
public static final int INVALID_DEFAULT_FAV_SLOT = -2;
/********************
****** Advanced
********************/
public static final String KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body";
public static final String KEY_SECURITY = "require_unlock";
public static final String KEY_SECURITY_DELAY = "require_unlock_delay";
public static final String KEY_READ_CACHE_SIZE = "read_cache_size";
public static final int DEFAULT_READ_CACHE_SIZE = 320;
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
public static final String KEY_BYPASS_VPN = "bypass_vpn";
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
private static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
private static final boolean DEFAULT_SHOW_COMMENTS = true;
private static final String TAG = Settings.class.getSimpleName();
private static final String KEY_DISPLAY_NAME = "display_name";
private static final String DEFAULT_DISPLAY_NAME = null;
private static final int DEFAULT_LIST_THUMB_SIZE = 40;
private static final String KEY_AVATAR = "avatar";
private static final String KEY_REMOVE_IMAGE_FILES = "include_pic";
private static final boolean DEFAULT_REMOVE_IMAGE_FILES = true;
private static final String KEY_NEED_SIGN_IN = "need_sign_in";
private static final boolean DEFAULT_NEED_SIGN_IN = true;
private static final String KEY_SELECT_SITE = "select_site";
private static final boolean DEFAULT_SELECT_SITE = true;
private static final int DEFAULT_THEME = THEME_SYSTEM;
private static final int DEFAULT_GALLERY_SITE = 1;
private static final String KEY_LAUNCH_PAGE = "launch_page";
private static final int DEFAULT_LAUNCH_PAGE = 0;
private static final int DEFAULT_LIST_MODE = 0;
private static final int DEFAULT_DETAIL_SIZE = 0;
private static final int DEFAULT_THUMB_SIZE = 120;
private static final int DEFAULT_THUMB_RESOLUTION = 0;
private static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages";
private static final boolean DEFAULT_SHOW_GALLERY_PAGES = false;
private static final boolean DEFAULT_SHOW_TAG_TRANSLATIONS = false;
private static final int DEFAULT_EXCLUDED_TAG_NAMESPACES = 0;
private static final String DEFAULT_EXCLUDED_LANGUAGES = null;
private static final String KEY_METERED_NETWORK_WARNING = "cellular_network_warning";
private static final boolean DEFAULT_METERED_NETWORK_WARNING = false;
private static final String KEY_APP_LINK_VERIFY_TIP = "app_link_verify_tip";
private static final boolean DEFAULT_APP_LINK_VERIFY_TIP = false;
private static final boolean DEFAULT_MEDIA_SCAN = false;
private static final String KEY_RECENT_DOWNLOAD_LABEL = "recent_download_label";
private static final String DEFAULT_RECENT_DOWNLOAD_LABEL = null;
private static final String KEY_HAS_DEFAULT_DOWNLOAD_LABEL = "has_default_download_label";
private static final boolean DEFAULT_HAS_DOWNLOAD_LABEL = false;
private static final String KEY_DEFAULT_DOWNLOAD_LABEL = "default_download_label";
private static final String DEFAULT_DOWNLOAD_LABEL = null;
private static final String KEY_MULTI_THREAD_DOWNLOAD = "download_thread";
private static final int DEFAULT_MULTI_THREAD_DOWNLOAD = 3;
private static final String KEY_PRELOAD_IMAGE = "preload_image";
private static final int DEFAULT_PRELOAD_IMAGE = 5;
private static final String KEY_DOWNLOAD_ORIGIN_IMAGE = "download_origin_image";
private static final boolean DEFAULT_DOWNLOAD_ORIGIN_IMAGE = false;
/********************
****** Favorites
********************/
private static final String KEY_FAV_CAT_0 = "fav_cat_0";
private static final String KEY_FAV_CAT_1 = "fav_cat_1";
private static final String KEY_FAV_CAT_2 = "fav_cat_2";
private static final String KEY_FAV_CAT_3 = "fav_cat_3";
private static final String KEY_FAV_CAT_4 = "fav_cat_4";
private static final String KEY_FAV_CAT_5 = "fav_cat_5";
private static final String KEY_FAV_CAT_6 = "fav_cat_6";
private static final String KEY_FAV_CAT_7 = "fav_cat_7";
private static final String KEY_FAV_CAT_8 = "fav_cat_8";
private static final String KEY_FAV_CAT_9 = "fav_cat_9";
private static final String DEFAULT_FAV_CAT_0 = "Favorites 0";
private static final String DEFAULT_FAV_CAT_1 = "Favorites 1";
private static final String DEFAULT_FAV_CAT_2 = "Favorites 2";
private static final String DEFAULT_FAV_CAT_3 = "Favorites 3";
private static final String DEFAULT_FAV_CAT_4 = "Favorites 4";
private static final String DEFAULT_FAV_CAT_5 = "Favorites 5";
private static final String DEFAULT_FAV_CAT_6 = "Favorites 6";
private static final String DEFAULT_FAV_CAT_7 = "Favorites 7";
private static final String DEFAULT_FAV_CAT_8 = "Favorites 8";
private static final String DEFAULT_FAV_CAT_9 = "Favorites 9";
private static final String KEY_FAV_COUNT_0 = "fav_count_0";
private static final String KEY_FAV_COUNT_1 = "fav_count_1";
private static final String KEY_FAV_COUNT_2 = "fav_count_2";
private static final String KEY_FAV_COUNT_3 = "fav_count_3";
private static final String KEY_FAV_COUNT_4 = "fav_count_4";
private static final String KEY_FAV_COUNT_5 = "fav_count_5";
private static final String KEY_FAV_COUNT_6 = "fav_count_6";
private static final String KEY_FAV_COUNT_7 = "fav_count_7";
private static final String KEY_FAV_COUNT_8 = "fav_count_8";
private static final String KEY_FAV_COUNT_9 = "fav_count_9";
private static final String KEY_FAV_LOCAL = "fav_local";
private static final String KEY_FAV_CLOUD = "fav_cloud";
private static final int DEFAULT_FAV_COUNT = 0;
private static final String KEY_RECENT_FAV_CAT = "recent_fav_cat";
private static final int DEFAULT_RECENT_FAV_CAT = FavListUrlBuilder.FAV_CAT_ALL;
// -1 for local, 0 - 9 for cloud favorite, other for no default fav slot
private static final String KEY_DEFAULT_FAV_SLOT = "default_favorite_2";
private static final int DEFAULT_DEFAULT_FAV_SLOT = INVALID_DEFAULT_FAV_SLOT;
private static final boolean DEFAULT_SAVE_PARSE_ERROR_BODY = true;
private static final String KEY_SAVE_CRASH_LOG = "save_crash_log";
private static final boolean DEFAULT_SAVE_CRASH_LOG = true;
private static final boolean DEFAULT_BUILT_IN_HOSTS = false;
private static final boolean DEFAULT_FRONTING = false;
private static final boolean DEFAULT_BYPASS_VPN = true;
private static final String KEY_PROXY_TYPE = "proxy_type";
private static final int DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM;
private static final String KEY_PROXY_IP = "proxy_ip";
private static final String DEFAULT_PROXY_IP = null;
private static final String KEY_PROXY_PORT = "proxy_port";
private static final int DEFAULT_PROXY_PORT = -1;
private static final String KEY_CLIPBOARD_TEXT_HASH_CODE = "clipboard_text_hash_code";
private static final int DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0;
private static final String KEY_DOWNLOAD_DELAY = "download_delay";
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
private static final String KEY_REQUEST_NEWS = "request_news";
private static final boolean DEFAULT_REQUEST_NEWS = true;
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
private static final boolean DEFAULT_QS_SAVE_PROGRESS = true;
public static boolean LIST_THUMB_SIZE_INITED = false;
public static SharedPreferences sSettingsPre;
private static int LIST_THUMB_SIZE = 40;
private static EhConfig sEhConfig;
public static void initialize() {
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(EhApplication.getApplication());
sEhConfig = loadEhConfig();
fixDefaultValue();
}
private static void fixDefaultValue() {
if ("CN".equals(Locale.getDefault().getCountry())) {
// Enable domain fronting if the country is CN
if (!sSettingsPre.contains(KEY_BUILT_IN_HOSTS)) {
putBuiltInHosts(true);
}
if (!sSettingsPre.contains(KEY_DOMAIN_FRONTING)) {
putDF(true);
}
// Enable show tag translations if the country is CN
if (!sSettingsPre.contains(KEY_SHOW_TAG_TRANSLATIONS)) {
putShowTagTranslations(true);
}
}
}
private static EhConfig loadEhConfig() {
EhConfig ehConfig = new EhConfig();
ehConfig.imageSize = getImageResolution();
ehConfig.excludedLanguages = getExcludedLanguages();
ehConfig.defaultCategories = getDefaultCategories();
ehConfig.excludedNamespaces = getExcludedTagNamespaces();
ehConfig.setDirty();
return ehConfig;
}
public static boolean getBoolean(String key, boolean defValue) {
try {
return sSettingsPre.getBoolean(key, defValue);
} catch (ClassCastException e) {
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
return defValue;
}
}
public static void putBoolean(String key, boolean value) {
sSettingsPre.edit().putBoolean(key, value).apply();
}
public static int getInt(String key, int defValue) {
try {
return sSettingsPre.getInt(key, defValue);
} catch (ClassCastException e) {
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
return defValue;
}
}
public static void putInt(String key, int value) {
sSettingsPre.edit().putInt(key, value).apply();
}
public static String getString(String key, String defValue) {
try {
return sSettingsPre.getString(key, defValue);
} catch (ClassCastException e) {
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
return defValue;
}
}
public static void putString(String key, String value) {
sSettingsPre.edit().putString(key, value).apply();
}
public static Set<String> getStringSet(String key) {
return sSettingsPre.getStringSet(key, null);
}
public static void putStringToStringSet(String key, String value) {
Set<String> set = getStringSet(key);
if (set == null)
set = Set.of(value);
else if (set.contains(value))
return;
else
set.add(value);
sSettingsPre.edit().putStringSet(key, set).apply();
}
public static int getIntFromStr(String key, int defValue) {
try {
return NumberUtils.parseIntSafely(sSettingsPre.getString(key, Integer.toString(defValue)), defValue);
} catch (ClassCastException e) {
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
return defValue;
}
}
public static void putIntToStr(String key, int value) {
sSettingsPre.edit().putString(key, Integer.toString(value)).apply();
}
@Nullable
public static String getDisplayName() {
return getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME);
}
public static void putDisplayName(String value) {
putString(KEY_DISPLAY_NAME, value);
}
public static void putAvatar(String value) {
putString(KEY_AVATAR, value);
}
public static boolean getRemoveImageFiles() {
return getBoolean(KEY_REMOVE_IMAGE_FILES, DEFAULT_REMOVE_IMAGE_FILES);
}
public static void putRemoveImageFiles(boolean value) {
putBoolean(KEY_REMOVE_IMAGE_FILES, value);
}
public static EhConfig getEhConfig() {
return sEhConfig;
}
public static boolean getNeedSignIn() {
return getBoolean(KEY_NEED_SIGN_IN, DEFAULT_NEED_SIGN_IN);
}
public static void putNeedSignIn(boolean value) {
putBoolean(KEY_NEED_SIGN_IN, value);
}
public static boolean getSelectSite() {
return getBoolean(KEY_SELECT_SITE, DEFAULT_SELECT_SITE);
}
public static void putSelectSite(boolean value) {
putBoolean(KEY_SELECT_SITE, value);
}
public static int getTheme() {
return getIntFromStr(KEY_THEME, DEFAULT_THEME);
}
public static int getGallerySite() {
return getIntFromStr(KEY_GALLERY_SITE, DEFAULT_GALLERY_SITE);
}
public static void putGallerySite(int value) {
putIntToStr(KEY_GALLERY_SITE, value);
}
public static String getLaunchPageGalleryListSceneAction() {
int value = getIntFromStr(KEY_LAUNCH_PAGE, DEFAULT_LAUNCH_PAGE);
return switch (value) {
case 0 -> GalleryListScene.ACTION_HOMEPAGE;
case 1 -> GalleryListScene.ACTION_SUBSCRIPTION;
case 2 -> GalleryListScene.ACTION_WHATS_HOT;
case 3 -> GalleryListScene.ACTION_TOP_LIST;
default -> throw new IllegalStateException("Unexpected value: " + value);
};
}
public static int getListMode() {
return getIntFromStr(KEY_LIST_MODE, DEFAULT_LIST_MODE);
}
public static int getDetailSize() {
return getIntFromStr(KEY_DETAIL_SIZE, DEFAULT_DETAIL_SIZE);
}
@DimenRes
public static int getDetailSizeResId() {
return switch (getDetailSize()) {
case 0 -> R.dimen.gallery_list_column_width_long;
case 1 -> R.dimen.gallery_list_column_width_short;
default -> throw new IllegalStateException("Unexpected value: " + getDetailSize());
};
}
public static int getThumbSize() {
return dip2px(getInt(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE));
}
public static int dip2px(int dpValue) {
final float scale = EhApplication.getApplication().getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
public static int getThumbResolution() {
return getIntFromStr(KEY_THUMB_RESOLUTION, DEFAULT_THUMB_RESOLUTION);
}
public static boolean getShowComments() {
return getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS);
}
public static boolean getRequestNews() {
return getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS);
}
public static boolean getHideHvEvents() {
return getBoolean(KEY_HIDE_HV_EVENTS, DEFAULT_HIDE_HV_EVENTS);
}
public static boolean getShowJpnTitle() {
return getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE);
}
public static boolean getShowGalleryPages() {
return getBoolean(KEY_SHOW_GALLERY_PAGES, DEFAULT_SHOW_GALLERY_PAGES);
}
public static boolean getShowTagTranslations() {
return getBoolean(KEY_SHOW_TAG_TRANSLATIONS, DEFAULT_SHOW_TAG_TRANSLATIONS);
}
public static void putShowTagTranslations(boolean value) {
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value);
}
public static int getDefaultCategories() {
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
}
public static int getExcludedTagNamespaces() {
return getInt(KEY_EXCLUDED_TAG_NAMESPACES, DEFAULT_EXCLUDED_TAG_NAMESPACES);
}
public static String getExcludedLanguages() {
return getString(KEY_EXCLUDED_LANGUAGES, DEFAULT_EXCLUDED_LANGUAGES);
}
public static boolean getMeteredNetworkWarning() {
return getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING);
}
public static boolean getAppLinkVerifyTip() {
return getBoolean(KEY_APP_LINK_VERIFY_TIP, DEFAULT_APP_LINK_VERIFY_TIP);
}
public static void putAppLinkVerifyTip(boolean value) {
putBoolean(KEY_APP_LINK_VERIFY_TIP, value);
}
public static boolean getEnabledSecurity() {
return getBoolean(KEY_SEC_SECURITY, VALUE_SEC_SECURITY);
}
@Nullable
public static UniFile getDownloadLocation() {
UniFile dir;
Uri.Builder builder = new Uri.Builder();
builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null));
builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null));
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null));
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null));
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null));
dir = UniFile.fromUri(EhApplication.getApplication(), builder.build());
return dir != null ? dir : UniFile.fromFile(AppConfig.getDefaultDownloadDir());
}
public static void putDownloadLocation(@NonNull UniFile location) {
Uri uri = location.getUri();
putString(KEY_DOWNLOAD_SAVE_SCHEME, uri.getScheme());
putString(KEY_DOWNLOAD_SAVE_AUTHORITY, uri.getEncodedAuthority());
putString(KEY_DOWNLOAD_SAVE_PATH, uri.getEncodedPath());
putString(KEY_DOWNLOAD_SAVE_QUERY, uri.getEncodedQuery());
putString(KEY_DOWNLOAD_SAVE_FRAGMENT, uri.getEncodedFragment());
if (getMediaScan()) {
CommonOperations.removeNoMediaFile(location);
} else {
CommonOperations.ensureNoMediaFile(location);
}
}
public static boolean getMediaScan() {
return getBoolean(KEY_MEDIA_SCAN, DEFAULT_MEDIA_SCAN);
}
public static String getRecentDownloadLabel() {
return getString(KEY_RECENT_DOWNLOAD_LABEL, DEFAULT_RECENT_DOWNLOAD_LABEL);
}
public static void putRecentDownloadLabel(String value) {
putString(KEY_RECENT_DOWNLOAD_LABEL, value);
}
public static boolean getHasDefaultDownloadLabel() {
return getBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, DEFAULT_HAS_DOWNLOAD_LABEL);
}
public static void putHasDefaultDownloadLabel(boolean hasDefaultDownloadLabel) {
putBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, hasDefaultDownloadLabel);
}
public static String getDefaultDownloadLabel() {
return getString(KEY_DEFAULT_DOWNLOAD_LABEL, DEFAULT_DOWNLOAD_LABEL);
}
public static void putDefaultDownloadLabel(String value) {
putString(KEY_DEFAULT_DOWNLOAD_LABEL, value);
}
public static int getMultiThreadDownload() {
return getIntFromStr(KEY_MULTI_THREAD_DOWNLOAD, DEFAULT_MULTI_THREAD_DOWNLOAD);
}
public static int getDownloadDelay() {
return getIntFromStr(KEY_DOWNLOAD_DELAY, DEFAULT_DOWNLOAD_DELAY);
}
public static int getPreloadImage() {
return getIntFromStr(KEY_PRELOAD_IMAGE, DEFAULT_PRELOAD_IMAGE);
}
public static String getImageResolution() {
return getString(KEY_IMAGE_RESOLUTION, DEFAULT_IMAGE_RESOLUTION);
}
public static void putImageResolution(String value) {
sEhConfig.imageSize = value;
sEhConfig.setDirty();
putString(KEY_IMAGE_RESOLUTION, value);
}
public static boolean getDownloadOriginImage() {
return getBoolean(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE);
}
public static String[] getFavCat() {
String[] favCat = new String[10];
favCat[0] = sSettingsPre.getString(KEY_FAV_CAT_0, DEFAULT_FAV_CAT_0);
favCat[1] = sSettingsPre.getString(KEY_FAV_CAT_1, DEFAULT_FAV_CAT_1);
favCat[2] = sSettingsPre.getString(KEY_FAV_CAT_2, DEFAULT_FAV_CAT_2);
favCat[3] = sSettingsPre.getString(KEY_FAV_CAT_3, DEFAULT_FAV_CAT_3);
favCat[4] = sSettingsPre.getString(KEY_FAV_CAT_4, DEFAULT_FAV_CAT_4);
favCat[5] = sSettingsPre.getString(KEY_FAV_CAT_5, DEFAULT_FAV_CAT_5);
favCat[6] = sSettingsPre.getString(KEY_FAV_CAT_6, DEFAULT_FAV_CAT_6);
favCat[7] = sSettingsPre.getString(KEY_FAV_CAT_7, DEFAULT_FAV_CAT_7);
favCat[8] = sSettingsPre.getString(KEY_FAV_CAT_8, DEFAULT_FAV_CAT_8);
favCat[9] = sSettingsPre.getString(KEY_FAV_CAT_9, DEFAULT_FAV_CAT_9);
return favCat;
}
public static void putFavCat(String[] value) {
AssertUtils.assertEquals(10, value.length);
sSettingsPre.edit()
.putString(KEY_FAV_CAT_0, value[0])
.putString(KEY_FAV_CAT_1, value[1])
.putString(KEY_FAV_CAT_2, value[2])
.putString(KEY_FAV_CAT_3, value[3])
.putString(KEY_FAV_CAT_4, value[4])
.putString(KEY_FAV_CAT_5, value[5])
.putString(KEY_FAV_CAT_6, value[6])
.putString(KEY_FAV_CAT_7, value[7])
.putString(KEY_FAV_CAT_8, value[8])
.putString(KEY_FAV_CAT_9, value[9])
.apply();
}
public static int[] getFavCount() {
int[] favCount = new int[10];
favCount[0] = sSettingsPre.getInt(KEY_FAV_COUNT_0, DEFAULT_FAV_COUNT);
favCount[1] = sSettingsPre.getInt(KEY_FAV_COUNT_1, DEFAULT_FAV_COUNT);
favCount[2] = sSettingsPre.getInt(KEY_FAV_COUNT_2, DEFAULT_FAV_COUNT);
favCount[3] = sSettingsPre.getInt(KEY_FAV_COUNT_3, DEFAULT_FAV_COUNT);
favCount[4] = sSettingsPre.getInt(KEY_FAV_COUNT_4, DEFAULT_FAV_COUNT);
favCount[5] = sSettingsPre.getInt(KEY_FAV_COUNT_5, DEFAULT_FAV_COUNT);
favCount[6] = sSettingsPre.getInt(KEY_FAV_COUNT_6, DEFAULT_FAV_COUNT);
favCount[7] = sSettingsPre.getInt(KEY_FAV_COUNT_7, DEFAULT_FAV_COUNT);
favCount[8] = sSettingsPre.getInt(KEY_FAV_COUNT_8, DEFAULT_FAV_COUNT);
favCount[9] = sSettingsPre.getInt(KEY_FAV_COUNT_9, DEFAULT_FAV_COUNT);
return favCount;
}
public static void putFavCount(int[] count) {
AssertUtils.assertEquals(10, count.length);
sSettingsPre.edit()
.putInt(KEY_FAV_COUNT_0, count[0])
.putInt(KEY_FAV_COUNT_1, count[1])
.putInt(KEY_FAV_COUNT_2, count[2])
.putInt(KEY_FAV_COUNT_3, count[3])
.putInt(KEY_FAV_COUNT_4, count[4])
.putInt(KEY_FAV_COUNT_5, count[5])
.putInt(KEY_FAV_COUNT_6, count[6])
.putInt(KEY_FAV_COUNT_7, count[7])
.putInt(KEY_FAV_COUNT_8, count[8])
.putInt(KEY_FAV_COUNT_9, count[9])
.apply();
}
public static int getFavLocalCount() {
return sSettingsPre.getInt(KEY_FAV_LOCAL, DEFAULT_FAV_COUNT);
}
public static void putFavLocalCount(int count) {
sSettingsPre.edit().putInt(KEY_FAV_LOCAL, count).apply();
}
public static int getFavCloudCount() {
return sSettingsPre.getInt(KEY_FAV_CLOUD, DEFAULT_FAV_COUNT);
}
public static void putFavCloudCount(int count) {
sSettingsPre.edit().putInt(KEY_FAV_CLOUD, count).apply();
}
public static int getRecentFavCat() {
return getInt(KEY_RECENT_FAV_CAT, DEFAULT_RECENT_FAV_CAT);
}
public static void putRecentFavCat(int value) {
putInt(KEY_RECENT_FAV_CAT, value);
}
public static int getDefaultFavSlot() {
return getInt(KEY_DEFAULT_FAV_SLOT, DEFAULT_DEFAULT_FAV_SLOT);
}
public static void putDefaultFavSlot(int value) {
putInt(KEY_DEFAULT_FAV_SLOT, value);
}
public static boolean getQSSaveProgress() {
return getBoolean(KEY_QS_SAVE_PROGRESS, DEFAULT_QS_SAVE_PROGRESS);
}
public static void putQSSaveProgress(boolean value) {
putBoolean(KEY_QS_SAVE_PROGRESS, value);
}
public static boolean getSaveParseErrorBody() {
return getBoolean(KEY_SAVE_PARSE_ERROR_BODY, DEFAULT_SAVE_PARSE_ERROR_BODY);
}
public static boolean getSaveCrashLog() {
return getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG);
}
public static boolean getSecurity() {
return getBoolean(KEY_SECURITY, false);
}
public static void putSecurity(boolean value) {
putBoolean(KEY_SECURITY, value);
}
public static int getSecurityDelay() {
return getInt(KEY_SECURITY_DELAY, 0);
}
public static int getReadCacheSize() {
return getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE);
}
public static boolean getBuiltInHosts() {
return getBoolean(KEY_BUILT_IN_HOSTS, DEFAULT_BUILT_IN_HOSTS);
}
public static void putBuiltInHosts(boolean value) {
putBoolean(KEY_BUILT_IN_HOSTS, value);
}
public static boolean getDF() {
return getBoolean(KEY_DOMAIN_FRONTING, DEFAULT_FRONTING);
}
public static void putDF(boolean value) {
putBoolean(KEY_DOMAIN_FRONTING, value);
}
public static boolean getBypassVPN() {
return getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN);
}
public static int getProxyType() {
return getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE);
}
public static void putProxyType(int value) {
putInt(KEY_PROXY_TYPE, value);
}
public static String getProxyIp() {
return getString(KEY_PROXY_IP, DEFAULT_PROXY_IP);
}
public static void putProxyIp(String value) {
putString(KEY_PROXY_IP, value);
}
public static int getProxyPort() {
return getInt(KEY_PROXY_PORT, DEFAULT_PROXY_PORT);
}
public static void putProxyPort(int value) {
putInt(KEY_PROXY_PORT, value);
}
public static int getClipboardTextHashCode() {
return getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE);
}
public static void putClipboardTextHashCode(int value) {
putInt(KEY_CLIPBOARD_TEXT_HASH_CODE, value);
}
public static Set<String> getArchivePasswds() {
return getStringSet(KEY_ARCHIVE_PASSWDS);
}
public static void putPasswdToArchivePasswds(String value) {
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value);
}
public static int getListThumbSize() {
if (LIST_THUMB_SIZE_INITED) {
return LIST_THUMB_SIZE;
}
int size = 3 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE);
LIST_THUMB_SIZE = size;
LIST_THUMB_SIZE_INITED = true;
return size;
}
public static boolean getNotificationRequired() {
return getBoolean(KEY_NOTIFICATION_REQUIRED, false);
}
public static void putNotificationRequired() {
putBoolean(KEY_NOTIFICATION_REQUIRED, true);
}
}
+641
View File
@@ -0,0 +1,641 @@
/*
* 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
import android.content.SharedPreferences
import android.net.Uri
import android.util.Log
import androidx.annotation.DimenRes
import androidx.preference.PreferenceManager
import com.hippo.ehviewer.EhApplication.Companion.application
import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.ui.scene.GalleryListScene
import com.hippo.ehviewer.yorozuya.LayoutUtils.dp2pix
import com.hippo.ehviewer.yorozuya.NumberUtils
import com.hippo.unifile.UniFile
import java.util.Locale
object Settings {
/********************
* Eh
*/
const val KEY_THEME = "theme"
const val KEY_ACCOUNT = "account"
const val KEY_IMAGE_LIMITS = "image_limits"
const val KEY_U_CONFIG = "uconfig"
const val KEY_MY_TAGS = "mytags"
const val KEY_BLACK_DARK_THEME = "black_dark_theme"
const val DEFAULT_BLACK_DARK_THEME = false
const val THEME_SYSTEM = -1
const val KEY_GALLERY_SITE = "gallery_site"
const val KEY_LIST_MODE = "list_mode"
const val KEY_DETAIL_SIZE = "detail_size"
const val KEY_THUMB_SIZE = "thumb_size_"
const val KEY_THUMB_RESOLUTION = "thumb_resolution"
const val KEY_SHOW_JPN_TITLE = "show_jpn_title"
const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
const val KEY_TAG_TRANSLATIONS_SOURCE = "tag_translations_source"
const val KEY_REQUEST_NEWS = "request_news"
const val KEY_REQUEST_NEWS_TIMER = "request_news_timer"
const val KEY_REQUEST_NEWS_TIMER_HOUR = "request_news_timer_hour"
const val KEY_REQUEST_NEWS_TIMER_MINUTE = "request_news_timer_minute"
const val KEY_HIDE_HV_EVENTS = "hide_hv_events"
val SIGN_IN_REQUIRED = arrayOf(
KEY_IMAGE_LIMITS,
KEY_GALLERY_SITE,
KEY_U_CONFIG,
KEY_MY_TAGS,
KEY_SHOW_JPN_TITLE,
KEY_REQUEST_NEWS,
KEY_REQUEST_NEWS_TIMER,
KEY_HIDE_HV_EVENTS,
)
/********************
* Privacy and Security
*/
const val KEY_SEC_SECURITY = "enable_secure"
const val VALUE_SEC_SECURITY = false
/********************
* Download
*/
const val KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme"
const val KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority"
const val KEY_DOWNLOAD_SAVE_PATH = "image_path"
const val KEY_NOTIFICATION_REQUIRED = "notification_required"
const val KEY_DOWNLOAD_SAVE_QUERY = "image_query"
const val KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment"
const val KEY_MEDIA_SCAN = "media_scan"
const val INVALID_DEFAULT_FAV_SLOT = -2
/********************
* Advanced
*/
const val KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body"
const val KEY_SECURITY = "require_unlock"
const val KEY_SECURITY_DELAY = "require_unlock_delay"
const val KEY_READ_CACHE_SIZE = "read_cache_size"
const val DEFAULT_READ_CACHE_SIZE = 640
const val KEY_BUILT_IN_HOSTS = "built_in_hosts_2"
const val KEY_DOMAIN_FRONTING = "domain_fronting"
const val KEY_BYPASS_VPN = "bypass_vpn"
const val KEY_LIST_THUMB_SIZE = "list_tile_size"
private const val KEY_LAST_DAWN_DAY = "last_dawn_day"
private const val DEFAULT_HIDE_HV_EVENTS = false
private const val KEY_SHOW_COMMENTS = "show_gallery_comments"
private const val DEFAULT_SHOW_COMMENTS = true
private val TAG = Settings::class.java.simpleName
private const val KEY_DISPLAY_NAME = "display_name"
private val DEFAULT_DISPLAY_NAME: String? = null
private const val DEFAULT_LIST_THUMB_SIZE = 40
private const val KEY_AVATAR = "avatar"
private const val KEY_REMOVE_IMAGE_FILES = "include_pic"
private const val DEFAULT_REMOVE_IMAGE_FILES = true
private const val KEY_NEED_SIGN_IN = "need_sign_in"
private const val DEFAULT_NEED_SIGN_IN = true
private const val KEY_SELECT_SITE = "select_site"
private const val DEFAULT_SELECT_SITE = true
private const val DEFAULT_THEME = THEME_SYSTEM
private const val DEFAULT_GALLERY_SITE = 0
private const val KEY_LAUNCH_PAGE = "launch_page"
private const val DEFAULT_LAUNCH_PAGE = 0
private const val DEFAULT_LIST_MODE = 0
private const val DEFAULT_DETAIL_SIZE = 0
private const val DEFAULT_THUMB_SIZE = 120
private const val DEFAULT_THUMB_RESOLUTION = 0
private const val DEFAULT_SHOW_JPN_TITLE = false
private const val KEY_SHOW_GALLERY_PAGES = "show_gallery_pages"
private const val DEFAULT_SHOW_GALLERY_PAGES = false
private const val DEFAULT_SHOW_TAG_TRANSLATIONS = false
private const val KEY_METERED_NETWORK_WARNING = "cellular_network_warning"
private const val DEFAULT_METERED_NETWORK_WARNING = false
private const val KEY_APP_LINK_VERIFY_TIP = "app_link_verify_tip"
private const val DEFAULT_APP_LINK_VERIFY_TIP = false
private const val DEFAULT_MEDIA_SCAN = false
private const val KEY_RECENT_DOWNLOAD_LABEL = "recent_download_label"
private val DEFAULT_RECENT_DOWNLOAD_LABEL: String? = null
private const val KEY_HAS_DEFAULT_DOWNLOAD_LABEL = "has_default_download_label"
private const val DEFAULT_HAS_DOWNLOAD_LABEL = false
private const val KEY_DEFAULT_DOWNLOAD_LABEL = "default_download_label"
private val DEFAULT_DOWNLOAD_LABEL: String? = null
private const val KEY_MULTI_THREAD_DOWNLOAD = "download_thread"
private const val DEFAULT_MULTI_THREAD_DOWNLOAD = 3
private const val KEY_PRELOAD_IMAGE = "preload_image"
private const val DEFAULT_PRELOAD_IMAGE = 5
private const val KEY_DOWNLOAD_ORIGIN_IMAGE = "download_origin_image"
private const val DEFAULT_DOWNLOAD_ORIGIN_IMAGE = false
/********************
* Favorites
*/
private const val KEY_FAV_CAT_0 = "fav_cat_0"
private const val KEY_FAV_CAT_1 = "fav_cat_1"
private const val KEY_FAV_CAT_2 = "fav_cat_2"
private const val KEY_FAV_CAT_3 = "fav_cat_3"
private const val KEY_FAV_CAT_4 = "fav_cat_4"
private const val KEY_FAV_CAT_5 = "fav_cat_5"
private const val KEY_FAV_CAT_6 = "fav_cat_6"
private const val KEY_FAV_CAT_7 = "fav_cat_7"
private const val KEY_FAV_CAT_8 = "fav_cat_8"
private const val KEY_FAV_CAT_9 = "fav_cat_9"
private const val DEFAULT_FAV_CAT_0 = "Favorites 0"
private const val DEFAULT_FAV_CAT_1 = "Favorites 1"
private const val DEFAULT_FAV_CAT_2 = "Favorites 2"
private const val DEFAULT_FAV_CAT_3 = "Favorites 3"
private const val DEFAULT_FAV_CAT_4 = "Favorites 4"
private const val DEFAULT_FAV_CAT_5 = "Favorites 5"
private const val DEFAULT_FAV_CAT_6 = "Favorites 6"
private const val DEFAULT_FAV_CAT_7 = "Favorites 7"
private const val DEFAULT_FAV_CAT_8 = "Favorites 8"
private const val DEFAULT_FAV_CAT_9 = "Favorites 9"
private const val KEY_FAV_COUNT_0 = "fav_count_0"
private const val KEY_FAV_COUNT_1 = "fav_count_1"
private const val KEY_FAV_COUNT_2 = "fav_count_2"
private const val KEY_FAV_COUNT_3 = "fav_count_3"
private const val KEY_FAV_COUNT_4 = "fav_count_4"
private const val KEY_FAV_COUNT_5 = "fav_count_5"
private const val KEY_FAV_COUNT_6 = "fav_count_6"
private const val KEY_FAV_COUNT_7 = "fav_count_7"
private const val KEY_FAV_COUNT_8 = "fav_count_8"
private const val KEY_FAV_COUNT_9 = "fav_count_9"
private const val KEY_FAV_LOCAL = "fav_local"
private const val KEY_FAV_CLOUD = "fav_cloud"
private const val DEFAULT_FAV_COUNT = 0
private const val KEY_RECENT_FAV_CAT = "recent_fav_cat"
private const val DEFAULT_RECENT_FAV_CAT = FavListUrlBuilder.FAV_CAT_ALL
// -1 for local, 0 - 9 for cloud favorite, other for no default fav slot
private const val KEY_DEFAULT_FAV_SLOT = "default_favorite_2"
private const val DEFAULT_DEFAULT_FAV_SLOT = INVALID_DEFAULT_FAV_SLOT
private const val DEFAULT_SAVE_PARSE_ERROR_BODY = true
private const val KEY_SAVE_CRASH_LOG = "save_crash_log"
private const val DEFAULT_SAVE_CRASH_LOG = true
private const val DEFAULT_BUILT_IN_HOSTS = false
private const val DEFAULT_FRONTING = false
private const val DEFAULT_BYPASS_VPN = true
private const val KEY_PROXY_TYPE = "proxy_type"
private const val DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM
private const val KEY_PROXY_IP = "proxy_ip"
private val DEFAULT_PROXY_IP: String? = null
private const val KEY_PROXY_PORT = "proxy_port"
private const val DEFAULT_PROXY_PORT = -1
private const val KEY_CLIPBOARD_TEXT_HASH_CODE = "clipboard_text_hash_code"
private const val DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0
private const val KEY_DOWNLOAD_DELAY = "download_delay"
private const val DEFAULT_DOWNLOAD_DELAY = 0
private const val DEFAULT_REQUEST_NEWS = false
private const val KEY_ARCHIVE_PASSWDS = "archive_passwds"
private const val KEY_QS_SAVE_PROGRESS = "qs_save_progress"
private const val DEFAULT_QS_SAVE_PROGRESS = true
private const val KEY_PRELOAD_THUMB_AGGRESIVELY = "preload_thumb_aggressively"
private const val DEFAULT_PRELOAD_THUMB_AGGRESIVELY = false
var LIST_THUMB_SIZE_INITED = false
private lateinit var sSettingsPre: SharedPreferences
private var LIST_THUMB_SIZE = 40
fun initialize() {
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(application)
fixDefaultValue()
}
private fun fixDefaultValue() {
if ("CN" == Locale.getDefault().country) {
// Enable domain fronting if the country is CN
if (!sSettingsPre.contains(KEY_BUILT_IN_HOSTS)) {
putBuiltInHosts(true)
}
if (!sSettingsPre.contains(KEY_DOMAIN_FRONTING)) {
putDF(true)
}
// Enable show tag translations if the country is CN
if (!sSettingsPre.contains(KEY_SHOW_TAG_TRANSLATIONS)) {
putShowTagTranslations(true)
}
}
}
private fun getBoolean(key: String, defValue: Boolean): Boolean {
return try {
sSettingsPre.getBoolean(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
}
private fun putBoolean(key: String, value: Boolean) {
sSettingsPre.edit().putBoolean(key, value).apply()
}
@JvmStatic
fun getInt(key: String, defValue: Int): Int {
return try {
sSettingsPre.getInt(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
}
@JvmStatic
fun putInt(key: String, value: Int) {
sSettingsPre.edit().putInt(key, value).apply()
}
private fun getString(key: String, defValue: String?): String? {
return try {
sSettingsPre.getString(key, defValue)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
}
private fun putString(key: String, value: String?) {
sSettingsPre.edit().putString(key, value).apply()
}
private fun getStringSet(key: String): MutableSet<String>? {
return sSettingsPre.getStringSet(key, null)
}
private fun putStringToStringSet(key: String, value: String) {
var set = getStringSet(key)
if (set == null) {
set =
mutableSetOf(value)
} else if (set.contains(value)) return else set.add(value)
sSettingsPre.edit().putStringSet(key, set).apply()
}
private fun getIntFromStr(key: String, defValue: Int): Int {
return try {
NumberUtils.parseIntSafely(
sSettingsPre.getString(key, defValue.toString()),
defValue,
)
} catch (e: ClassCastException) {
Log.d(TAG, "Get ClassCastException when get $key value", e)
defValue
}
}
private fun putIntToStr(key: String, value: Int) {
sSettingsPre.edit().putString(key, value.toString()).apply()
}
var lastDawnDay: Long
get() = sSettingsPre.getLong(KEY_LAST_DAWN_DAY, 0)
set(value) = sSettingsPre.edit().putLong(KEY_LAST_DAWN_DAY, value).apply()
val displayName: String?
get() = getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME)
fun putDisplayName(value: String?) {
putString(KEY_DISPLAY_NAME, value)
}
fun putAvatar(value: String?) {
putString(KEY_AVATAR, value)
}
val removeImageFiles: Boolean
get() = getBoolean(KEY_REMOVE_IMAGE_FILES, DEFAULT_REMOVE_IMAGE_FILES)
fun putRemoveImageFiles(value: Boolean) {
putBoolean(KEY_REMOVE_IMAGE_FILES, value)
}
val needSignIn: Boolean
get() = getBoolean(KEY_NEED_SIGN_IN, DEFAULT_NEED_SIGN_IN)
fun putNeedSignIn(value: Boolean) {
putBoolean(KEY_NEED_SIGN_IN, value)
}
val selectSite: Boolean
get() = getBoolean(KEY_SELECT_SITE, DEFAULT_SELECT_SITE)
fun putSelectSite(value: Boolean) {
putBoolean(KEY_SELECT_SITE, value)
}
val theme: Int
get() = getIntFromStr(KEY_THEME, DEFAULT_THEME)
val blackDarkTheme
get() = getBoolean(KEY_BLACK_DARK_THEME, DEFAULT_BLACK_DARK_THEME)
val gallerySite: Int
get() = getIntFromStr(KEY_GALLERY_SITE, DEFAULT_GALLERY_SITE)
fun putGallerySite(value: Int) {
putIntToStr(KEY_GALLERY_SITE, value)
}
val launchPageGalleryListSceneAction: String
get() {
return when (val value = getIntFromStr(KEY_LAUNCH_PAGE, DEFAULT_LAUNCH_PAGE)) {
0 -> GalleryListScene.ACTION_HOMEPAGE
1 -> GalleryListScene.ACTION_SUBSCRIPTION
2 -> GalleryListScene.ACTION_WHATS_HOT
3 -> GalleryListScene.ACTION_TOP_LIST
else -> throw IllegalStateException("Unexpected value: $value")
}
}
val listMode: Int
get() = getIntFromStr(KEY_LIST_MODE, DEFAULT_LIST_MODE)
private val detailSize: Int
get() = getIntFromStr(KEY_DETAIL_SIZE, DEFAULT_DETAIL_SIZE)
@get:DimenRes
val detailSizeResId: Int
get() = when (detailSize) {
0 -> R.dimen.gallery_list_column_width_long
1 -> R.dimen.gallery_list_column_width_short
else -> throw IllegalStateException("Unexpected value: $detailSize")
}
val thumbSize: Int
get() = dp2pix(application, thumbSizeDp.toFloat())
val thumbSizeDp: Int
get() = getInt(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE)
val thumbResolution: Int
get() = getIntFromStr(KEY_THUMB_RESOLUTION, DEFAULT_THUMB_RESOLUTION)
val showComments: Boolean
get() = getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS)
val requestNews: Boolean
get() = getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS)
val hideHvEvents: Boolean
get() = getBoolean(KEY_HIDE_HV_EVENTS, DEFAULT_HIDE_HV_EVENTS)
val showJpnTitle: Boolean
get() = getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE)
val showGalleryPages: Boolean
get() = getBoolean(KEY_SHOW_GALLERY_PAGES, DEFAULT_SHOW_GALLERY_PAGES)
val showTagTranslations: Boolean
get() = getBoolean(KEY_SHOW_TAG_TRANSLATIONS, DEFAULT_SHOW_TAG_TRANSLATIONS)
private fun putShowTagTranslations(value: Boolean) {
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value)
}
val meteredNetworkWarning: Boolean
get() = getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING)
val appLinkVerifyTip: Boolean
get() = getBoolean(KEY_APP_LINK_VERIFY_TIP, DEFAULT_APP_LINK_VERIFY_TIP)
fun putAppLinkVerifyTip(value: Boolean) {
putBoolean(KEY_APP_LINK_VERIFY_TIP, value)
}
val enabledSecurity: Boolean
get() = getBoolean(KEY_SEC_SECURITY, VALUE_SEC_SECURITY)
val downloadLocation: UniFile?
get() {
val dir: UniFile?
val builder = Uri.Builder()
builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null))
builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null))
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null))
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null))
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null))
dir = UniFile.fromUri(application, builder.build())
return dir ?: UniFile.fromFile(AppConfig.getDefaultDownloadDir())
}
fun putDownloadLocation(location: UniFile) {
val uri = location.uri
putString(KEY_DOWNLOAD_SAVE_SCHEME, uri.scheme)
putString(KEY_DOWNLOAD_SAVE_AUTHORITY, uri.encodedAuthority)
putString(KEY_DOWNLOAD_SAVE_PATH, uri.encodedPath)
putString(KEY_DOWNLOAD_SAVE_QUERY, uri.encodedQuery)
putString(KEY_DOWNLOAD_SAVE_FRAGMENT, uri.encodedFragment)
}
val mediaScan: Boolean
get() = getBoolean(KEY_MEDIA_SCAN, DEFAULT_MEDIA_SCAN)
val recentDownloadLabel: String?
get() = getString(KEY_RECENT_DOWNLOAD_LABEL, DEFAULT_RECENT_DOWNLOAD_LABEL)
fun putRecentDownloadLabel(value: String?) {
putString(KEY_RECENT_DOWNLOAD_LABEL, value)
}
val hasDefaultDownloadLabel: Boolean
get() = getBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, DEFAULT_HAS_DOWNLOAD_LABEL)
fun putHasDefaultDownloadLabel(hasDefaultDownloadLabel: Boolean) {
putBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, hasDefaultDownloadLabel)
}
val defaultDownloadLabel: String?
get() = getString(KEY_DEFAULT_DOWNLOAD_LABEL, DEFAULT_DOWNLOAD_LABEL)
fun putDefaultDownloadLabel(value: String?) {
putString(KEY_DEFAULT_DOWNLOAD_LABEL, value)
}
val multiThreadDownload: Int
get() = getIntFromStr(KEY_MULTI_THREAD_DOWNLOAD, DEFAULT_MULTI_THREAD_DOWNLOAD)
val downloadDelay: Int
get() = getIntFromStr(KEY_DOWNLOAD_DELAY, DEFAULT_DOWNLOAD_DELAY)
val preloadImage: Int
get() = getIntFromStr(KEY_PRELOAD_IMAGE, DEFAULT_PRELOAD_IMAGE)
val downloadOriginImage: Boolean
get() = getBoolean(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE)
val favCat: Array<String>
get() = arrayOf(
sSettingsPre.getString(KEY_FAV_CAT_0, DEFAULT_FAV_CAT_0)!!,
sSettingsPre.getString(KEY_FAV_CAT_1, DEFAULT_FAV_CAT_1)!!,
sSettingsPre.getString(KEY_FAV_CAT_2, DEFAULT_FAV_CAT_2)!!,
sSettingsPre.getString(KEY_FAV_CAT_3, DEFAULT_FAV_CAT_3)!!,
sSettingsPre.getString(KEY_FAV_CAT_4, DEFAULT_FAV_CAT_4)!!,
sSettingsPre.getString(KEY_FAV_CAT_5, DEFAULT_FAV_CAT_5)!!,
sSettingsPre.getString(KEY_FAV_CAT_6, DEFAULT_FAV_CAT_6)!!,
sSettingsPre.getString(KEY_FAV_CAT_7, DEFAULT_FAV_CAT_7)!!,
sSettingsPre.getString(KEY_FAV_CAT_8, DEFAULT_FAV_CAT_8)!!,
sSettingsPre.getString(KEY_FAV_CAT_9, DEFAULT_FAV_CAT_9)!!,
)
fun putFavCat(value: Array<String?>) {
check(value.size == 10)
sSettingsPre.edit()
.putString(KEY_FAV_CAT_0, value[0])
.putString(KEY_FAV_CAT_1, value[1])
.putString(KEY_FAV_CAT_2, value[2])
.putString(KEY_FAV_CAT_3, value[3])
.putString(KEY_FAV_CAT_4, value[4])
.putString(KEY_FAV_CAT_5, value[5])
.putString(KEY_FAV_CAT_6, value[6])
.putString(KEY_FAV_CAT_7, value[7])
.putString(KEY_FAV_CAT_8, value[8])
.putString(KEY_FAV_CAT_9, value[9])
.apply()
}
val favCount: IntArray
get() = intArrayOf(
sSettingsPre.getInt(KEY_FAV_COUNT_0, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_1, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_2, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_3, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_4, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_5, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_6, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_7, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_8, DEFAULT_FAV_COUNT),
sSettingsPre.getInt(KEY_FAV_COUNT_9, DEFAULT_FAV_COUNT),
)
fun putFavCount(count: IntArray) {
check(count.size == 10)
sSettingsPre.edit()
.putInt(KEY_FAV_COUNT_0, count[0])
.putInt(KEY_FAV_COUNT_1, count[1])
.putInt(KEY_FAV_COUNT_2, count[2])
.putInt(KEY_FAV_COUNT_3, count[3])
.putInt(KEY_FAV_COUNT_4, count[4])
.putInt(KEY_FAV_COUNT_5, count[5])
.putInt(KEY_FAV_COUNT_6, count[6])
.putInt(KEY_FAV_COUNT_7, count[7])
.putInt(KEY_FAV_COUNT_8, count[8])
.putInt(KEY_FAV_COUNT_9, count[9])
.apply()
}
val favLocalCount: Int
get() = sSettingsPre.getInt(KEY_FAV_LOCAL, DEFAULT_FAV_COUNT)
fun putFavLocalCount(count: Int) {
sSettingsPre.edit().putInt(KEY_FAV_LOCAL, count).apply()
}
val favCloudCount: Int
get() = sSettingsPre.getInt(KEY_FAV_CLOUD, DEFAULT_FAV_COUNT)
fun putFavCloudCount(count: Int) {
sSettingsPre.edit().putInt(KEY_FAV_CLOUD, count).apply()
}
val recentFavCat: Int
get() = getInt(KEY_RECENT_FAV_CAT, DEFAULT_RECENT_FAV_CAT)
fun putRecentFavCat(value: Int) {
putInt(KEY_RECENT_FAV_CAT, value)
}
val defaultFavSlot: Int
get() = getInt(KEY_DEFAULT_FAV_SLOT, DEFAULT_DEFAULT_FAV_SLOT)
fun putDefaultFavSlot(value: Int) {
putInt(KEY_DEFAULT_FAV_SLOT, value)
}
val qSSaveProgress: Boolean
get() = getBoolean(KEY_QS_SAVE_PROGRESS, DEFAULT_QS_SAVE_PROGRESS)
fun putQSSaveProgress(value: Boolean) {
putBoolean(KEY_QS_SAVE_PROGRESS, value)
}
val saveParseErrorBody: Boolean
get() = getBoolean(KEY_SAVE_PARSE_ERROR_BODY, DEFAULT_SAVE_PARSE_ERROR_BODY)
val saveCrashLog: Boolean
get() = getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG)
val security: Boolean
get() = getBoolean(KEY_SECURITY, false)
fun putSecurity(value: Boolean) {
putBoolean(KEY_SECURITY, value)
}
val securityDelay: Int
get() = getInt(KEY_SECURITY_DELAY, 0)
val readCacheSize: Int
get() = getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE)
val builtInHosts: Boolean
get() = getBoolean(KEY_BUILT_IN_HOSTS, DEFAULT_BUILT_IN_HOSTS)
private fun putBuiltInHosts(value: Boolean) {
putBoolean(KEY_BUILT_IN_HOSTS, value)
}
val dF: Boolean
get() = getBoolean(KEY_DOMAIN_FRONTING, DEFAULT_FRONTING)
private fun putDF(value: Boolean) {
putBoolean(KEY_DOMAIN_FRONTING, value)
}
val bypassVpn: Boolean
get() = getBoolean(KEY_BYPASS_VPN, DEFAULT_BYPASS_VPN)
@JvmStatic
val proxyType: Int
get() = getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE)
fun putProxyType(value: Int) {
putInt(KEY_PROXY_TYPE, value)
}
@JvmStatic
val proxyIp: String?
get() = getString(KEY_PROXY_IP, DEFAULT_PROXY_IP)
fun putProxyIp(value: String?) {
putString(KEY_PROXY_IP, value)
}
@JvmStatic
val proxyPort: Int
get() = getInt(KEY_PROXY_PORT, DEFAULT_PROXY_PORT)
fun putProxyPort(value: Int) {
putInt(KEY_PROXY_PORT, value)
}
val clipboardTextHashCode: Int
get() = getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE)
fun putClipboardTextHashCode(value: Int) {
putInt(KEY_CLIPBOARD_TEXT_HASH_CODE, value)
}
val archivePasswds: Set<String>?
get() = getStringSet(KEY_ARCHIVE_PASSWDS)
fun putPasswdToArchivePasswds(value: String) {
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value)
}
val listThumbSize: Int
get() {
if (LIST_THUMB_SIZE_INITED) {
return LIST_THUMB_SIZE
}
val size = 3 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE)
LIST_THUMB_SIZE = size
LIST_THUMB_SIZE_INITED = true
return size
}
val notificationRequired: Boolean
get() = getBoolean(KEY_NOTIFICATION_REQUIRED, false)
fun putNotificationRequired() {
putBoolean(KEY_NOTIFICATION_REQUIRED, true)
}
val preloadThumbAggressively: Boolean
get() = getBoolean(KEY_PRELOAD_THUMB_AGGRESIVELY, DEFAULT_PRELOAD_THUMB_AGGRESIVELY)
}
@@ -1,86 +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;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.browser.customtabs.CustomTabsIntent;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
public final class UrlOpener {
private UrlOpener() {
}
public static void openUrl(@NonNull Context context, String url, boolean ehUrl) {
openUrl(context, url, ehUrl, null);
}
public static void openUrl(@NonNull Context context, String url, boolean ehUrl, GalleryDetail galleryDetail) {
if (TextUtils.isEmpty(url)) {
return;
}
Intent intent;
Uri uri = Uri.parse(url);
if (ehUrl) {
if (galleryDetail != null) {
GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(url);
if (result != null) {
if (result.gid == galleryDetail.getGid()) {
intent = new Intent(context, ReaderActivity.class);
intent.setAction(ReaderActivity.ACTION_EH);
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail);
intent.putExtra(ReaderActivity.KEY_PAGE, result.page);
context.startActivity(intent);
return;
}
} else if (url.startsWith("#c")) {
try {
intent = new Intent(context, ReaderActivity.class);
intent.setAction(ReaderActivity.ACTION_EH);
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail);
intent.putExtra(ReaderActivity.KEY_PAGE, Integer.parseInt(url.replace("#c", "")) - 1);
context.startActivity(intent);
return;
} catch (NumberFormatException e) {
//
}
}
}
}
CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder();
customTabsIntent.setShowTitle(true);
try {
customTabsIntent.build().launchUrl(context, uri);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, R.string.no_browser_installed, Toast.LENGTH_LONG).show();
}
}
}
@@ -1,40 +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;
public class EhCacheKeyFactory {
public static String getThumbKey(long gid) {
return "preview:large:" + gid + ":" + 0; // "thumb:" + gid;
}
public static String getNormalPreviewKey(long gid, int index) {
return "preview:normal:" + gid + ":" + index;
}
public static String getLargePreviewKey(long gid, int index) {
return "preview:large:" + gid + ":" + index;
}
public static String getLargePreviewSetKey(long gid, int index) {
return "large_preview_set:" + gid + ":" + index;
}
public static String getImageKey(long gid, int index) {
return "image:" + gid + ":" + index;
}
}
@@ -0,0 +1,46 @@
/*
* 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 com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.NormalGalleryPreview
// ExHentai Large Preview: https://s.exhentai.org/t/***
// ExHentai Normal Preview: https://s.exhentai.org/m/***
// EHentai Normal Preview: https://ehgt.org/m/***
// EHentai Large Preview: https://ehgt.org/***
private const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
private const val URL_PREFIX_THUMB_EX = "https://s.exhentai.org/"
fun getImageKey(gid: Long, index: Int): String {
return "image:$gid:$index"
}
fun getThumbKey(url: String): String {
return url.removePrefix(URL_PREFIX_THUMB_E).removePrefix(URL_PREFIX_THUMB_EX).removePrefix("t/")
}
val GalleryPreview.url
get() = if (this is NormalGalleryPreview) {
if (EhUtils.isExHentai) URL_PREFIX_THUMB_EX + imageKey else URL_PREFIX_THUMB_E + imageKey
} else {
if (EhUtils.isExHentai) URL_PREFIX_THUMB_EX + "t/" + imageKey else URL_PREFIX_THUMB_E + imageKey
}
val GalleryInfo.thumbUrl
get() = if (EhUtils.isExHentai) URL_PREFIX_THUMB_EX + "t/" + thumbKey!! else URL_PREFIX_THUMB_E + thumbKey!!
@@ -1,184 +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 eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import java.io.File
object EhClient {
internal fun enqueue(request: EhRequest, scope: CoroutineScope) {
check(!request.isActive) // Abort if attempt to execute an active request
request.job = scope.launchIO {
val callback: Callback<Any?>? = request.callback
try {
val result: Any? = request.run {
if (args == null)
execute(method)
else
execute(method, *args!!)
}
withUIContext { callback?.onSuccess(result) }
} catch (e: Exception) {
if (e is CancellationException)
throw e // Don't catch coroutine CancellationException
e.printStackTrace()
withUIContext { callback?.onFailure(e) }
}
}
}
suspend fun execute(method: Int, vararg params: Any?): Any? {
return when (method) {
METHOD_SIGN_IN -> EhEngine.signIn(
params[0] as String,
params[1] as String
)
METHOD_GET_GALLERY_LIST -> EhEngine.getGalleryList(
params[0] as String
)
METHOD_GET_GALLERY_DETAIL -> EhEngine.getGalleryDetail(
params[0] as String?
)
METHOD_GET_PREVIEW_SET -> EhEngine.getPreviewSet(
params[0] as String?
)
METHOD_GET_RATE_GALLERY -> EhEngine.rateGallery(
params[0] as Long,
params[1] as String?,
params[2] as Long,
params[3] as String?,
params[4] as Float
)
METHOD_GET_COMMENT_GALLERY -> EhEngine.commentGallery(
params[0] as String?,
params[1] as String,
params[2] as String?
)
METHOD_GET_GALLERY_TOKEN -> EhEngine.getGalleryToken(
params[0] as Long,
params[1] as String?,
params[2] as Int
)
METHOD_GET_FAVORITES -> EhEngine.getFavorites(
params[0] as String
)
METHOD_ADD_FAVORITES -> EhEngine.addFavorites(
params[0] as Long,
params[1] as String?,
params[2] as Int,
params[3] as String?
)
METHOD_ADD_FAVORITES_RANGE -> @Suppress("UNCHECKED_CAST") EhEngine.addFavoritesRange(
params[0] as LongArray,
params[1] as Array<String?>,
params[2] as Int
)
METHOD_MODIFY_FAVORITES -> EhEngine.modifyFavorites(
params[0] as String,
params[1] as LongArray,
params[2] as Int
)
METHOD_GET_TORRENT_LIST -> EhEngine.getTorrentList(
params[0] as String?,
params[1] as Long,
params[2] as String?
)
METHOD_GET_PROFILE -> EhEngine.getProfile()
METHOD_VOTE_COMMENT -> EhEngine.voteComment(
params[0] as Long,
params[1] as String?,
params[2] as Long,
params[3] as String?,
params[4] as Long,
params[5] as Int
)
METHOD_IMAGE_SEARCH -> EhEngine.imageSearch(
params[0] as File,
params[1] as Boolean,
params[2] as Boolean,
params[3] as Boolean
)
METHOD_ARCHIVE_LIST -> EhEngine.getArchiveList(
params[0] as String?,
params[1] as Long,
params[2] as String?
)
METHOD_DOWNLOAD_ARCHIVE -> EhEngine.downloadArchive(
params[0] as Long,
params[1] as String?,
params[2] as String?,
params[3] as String?,
params[4] as Boolean
)
METHOD_VOTE_TAG -> EhEngine.voteTag(
params[0] as Long,
params[1] as String?,
params[2] as Long,
params[3] as String?,
params[4] as String?,
params[5] as Int
)
METHOD_GET_UCONFIG -> EhEngine.getUConfig()
else -> throw IllegalStateException("Can't detect method $method")
}
}
interface Callback<E> {
fun onSuccess(result: E)
fun onFailure(e: Exception)
fun onCancel()
}
const val METHOD_SIGN_IN = 0
const val METHOD_GET_GALLERY_LIST = 1
const val METHOD_GET_GALLERY_DETAIL = 3
const val METHOD_GET_PREVIEW_SET = 4
const val METHOD_GET_RATE_GALLERY = 5
const val METHOD_GET_COMMENT_GALLERY = 6
const val METHOD_GET_GALLERY_TOKEN = 7
const val METHOD_GET_FAVORITES = 8
const val METHOD_ADD_FAVORITES = 9
const val METHOD_ADD_FAVORITES_RANGE = 10
const val METHOD_MODIFY_FAVORITES = 11
const val METHOD_GET_TORRENT_LIST = 12
const val METHOD_GET_PROFILE = 14
const val METHOD_VOTE_COMMENT = 15
const val METHOD_IMAGE_SEARCH = 16
const val METHOD_ARCHIVE_LIST = 17
const val METHOD_DOWNLOAD_ARCHIVE = 18
const val METHOD_VOTE_TAG = 19
const val METHOD_GET_UCONFIG = 20
}
@@ -1,806 +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.client;
import com.hippo.network.InetValidator;
/**
* Some configurable stuff about EH. It ends up cookie.
*/
public class EhConfig implements Cloneable {
/**
* The Cookie key of uconfig
*/
public static final String KEY_UCONFIG = "uconfig";
/**
* The Cookie key of lofi resolution
*/
public static final String KEY_LOFI_RESOLUTION = "xres";
/**
* The Cookie key of show warning
*/
public static final String KEY_CONTENT_WARNING = "nw";
/**
* load images through the Hentai@Home Network
*/
public static final String LOAD_FROM_HAH_YES = "y";
/**
* do not load images through the Hentai@Home Network
*/
public static final String LOAD_FROM_HAH_NO = "n";
/**
* Image Size Auto
*/
public static final String IMAGE_SIZE_AUTO = "a";
/**
* Image Size 780x
*/
public static final String IMAGE_SIZE_780X = "780";
/**
* Image Size 980x
*/
public static final String IMAGE_SIZE_980X = "980";
/**
* Image Size 1280x
*/
public static final String IMAGE_SIZE_1280X = "1280";
/**
* Image Size 1600x
*/
public static final String IMAGE_SIZE_1600X = "1600";
/**
* Image Size 2400x
*/
public static final String IMAGE_SIZE_2400X = "2400";
/**
* Manual Accept, Manual Start
*/
public static final String ARCHIVER_DOWNLOAD_MAMS = "0";
/**
* >Manual Accept, Auto Start
*/
public static final String ARCHIVER_DOWNLOAD_AAMS = "1";
/**
* Auto Accept, Manual Start
*/
public static final String ARCHIVER_DOWNLOAD_MAAS = "2";
/**
* Auto Accept, Auto Start
*/
public static final String ARCHIVER_DOWNLOAD_AAAS = "3";
/**
* List View on the front and search pages
*/
public static final String LAYOUT_MODE_LIST = "l";
/**
* Thumbnail View on the front and search pages
*/
public static final String LAYOUT_MODE_THUMB = "t";
public static final int MISC = 0x1;
public static final int DOUJINSHI = 0x2;
public static final int MANGA = 0x4;
public static final int ARTIST_CG = 0x8;
public static final int GAME_CG = 0x10;
public static final int IMAGE_SET = 0x20;
public static final int COSPLAY = 0x40;
public static final int ASIAN_PORN = 0x80;
public static final int NON_H = 0x100;
public static final int WESTERN = 0x200;
public static final int ALL_CATEGORY = 0x3ff;
public static final int NAMESPACES_RECLASS = 0x1;
public static final int NAMESPACES_LANGUAGE = 0x2;
public static final int NAMESPACES_PARODY = 0x4;
public static final int NAMESPACES_CHARACTER = 0x8;
public static final int NAMESPACES_GROUP = 0x10;
public static final int NAMESPACES_ARTIST = 0x20;
public static final int NAMESPACES_MALE = 0x40;
public static final int NAMESPACES_FEMALE = 0x80;
public static final String JAPANESE_ORIGINAL = "0";
public static final String JAPANESE_TRANSLATED = "1024";
public static final String JAPANESE_REWRITE = "2048";
public static final String ENGLISH_ORIGINAL = "1";
public static final String ENGLISH_TRANSLATED = "1025";
public static final String ENGLISH_REWRITE = "2049";
public static final String CHINESE_ORIGINAL = "10";
public static final String CHINESE_TRANSLATED = "1034";
public static final String CHINESE_REWRITE = "2058";
public static final String DUTCH_ORIGINAL = "20";
public static final String DUTCH_TRANSLATED = "1044";
public static final String DUTCH_REWRITE = "2068";
public static final String FRENCH_ORIGINAL = "30";
public static final String FRENCH_TRANSLATED = "1054";
public static final String FRENCH_REWRITE = "2078";
public static final String GERMAN_ORIGINAL = "40";
public static final String GERMAN_TRANSLATED = "1064";
public static final String GERMAN_REWRITE = "2088";
public static final String HUNGARIAN_ORIGINAL = "50";
public static final String HUNGARIAN_TRANSLATED = "1074";
public static final String HUNGARIAN_REWRITE = "2098";
public static final String ITALIAN_ORIGINAL = "60";
public static final String ITALIAN_TRANSLATED = "1084";
public static final String ITALIAN_REWRITE = "2108";
public static final String KOREAN_ORIGINAL = "70";
public static final String KOREAN_TRANSLATED = "1094";
public static final String KOREAN_REWRITE = "2118";
public static final String POLISH_ORIGINAL = "80";
public static final String POLISH_TRANSLATED = "1104";
public static final String POLISH_REWRITE = "2128";
public static final String PORTUGUESE_ORIGINAL = "90";
public static final String PORTUGUESE_TRANSLATED = "1114";
public static final String PORTUGUESE_REWRITE = "2138";
public static final String RUSSIAN_ORIGINAL = "100";
public static final String RUSSIAN_TRANSLATED = "1124";
public static final String RUSSIAN_REWRITE = "2148";
public static final String SPANISH_ORIGINAL = "110";
public static final String SPANISH_TRANSLATED = "1134";
public static final String SPANISH_REWRITE = "2158";
public static final String THAI_ORIGINAL = "120";
public static final String THAI_TRANSLATED = "1144";
public static final String THAI_REWRITE = "2168";
public static final String VIETNAMESE_ORIGINAL = "130";
public static final String VIETNAMESE_TRANSLATED = "1154";
public static final String VIETNAMESE_REWRITE = "2178";
public static final String NA_ORIGINAL = "254";
public static final String NA_TRANSLATED = "1278";
public static final String NA_REWRITE = "2302";
public static final String OTHER_ORIGINAL = "255";
public static final String OTHER_TRANSLATED = "1279";
public static final String OTHER_REWRITE = "2303";
/**
* 25 results per page for the index/search page and torrent search pages
*/
public static final String RESULT_COUNT_25 = "0";
/**
* 50 results per page for the index/search page and torrent search pages
*/
public static final String RESULT_COUNT_50 = "1";
/**
* 100 results per page for the index/search page and torrent search pages
*/
public static final String RESULT_COUNT_100 = "2";
/**
* 200 results per page for the index/search page and torrent search pages
*/
public static final String RESULT_COUNT_200 = "3";
/**
* On mouse-over
*/
public static final String MOUSE_OVER_YES = "m";
/**
* On page load
*/
public static final String MOUSE_OVER_NO = "p";
/**
* Preview normal size
*/
public static final String PREVIEW_SIZE_NORMAL = "m";
/**
* Preview large size
*/
public static final String PREVIEW_SIZE_LARGE = "l";
/**
* 4 row preview per page
*/
public static final String PREVIEW_ROW_4 = "2";
/**
* 10 row preview per page
*/
public static final String PREVIEW_ROW_10 = "5";
/**
* 20 row preview per page
*/
public static final String PREVIEW_ROW_20 = "10";
/**
* 40 row preview per page
*/
public static final String PREVIEW_ROW_40 = "20";
/**
* Oldest comments first
*/
public static final String COMMENTS_SORT_OLDEST_FIRST = "a";
/**
* Recent comments first
*/
public static final String COMMENTS_SORT_RECENT_FIRST = "d";
/**
* By highest score
*/
public static final String COMMENTS_SORT_HIGHEST_SCORE_FIRST = "s";
/**
* Show gallery comment votes On score hover or click
*/
public static final String COMMENTS_VOTES_POP = "0";
/**
* Always show gallery comment votes
*/
public static final String COMMENTS_VOTES_ALWAYS = "1";
/**
* Sort order for gallery tags alphabetically
*/
public static final String TAGS_SORT_ALPHABETICAL = "a";
/**
* Sort order for gallery tags by tag power
*/
public static final String TAGS_SORT_POWER = "p";
/**
* Show gallery page numbers
*/
public static final String SHOW_GALLERY_INDEX_YES = "1";
/**
* Do not show gallery page numbers
*/
public static final String SHOW_GALLERY_INDEX_NO = "0";
/**
* Enable Tag Flagging
*/
public static final String ENABLE_TAG_FLAGGING_YES = "y";
/**
* Do not enable Tag Flagging
*/
public static final String ENABLE_TAG_FLAGGING_NO = "n";
/**
* Always display the original images
*/
public static final String ALWAYS_ORIGINAL_YES = "y";
/**
* Do not Always display the original images
*/
public static final String ALWAYS_ORIGINAL_NO = "n";
/**
* Enable the Multi-Page Viewe
*/
public static final String MULTI_PAGE_YES = "y";
/**
* Do not enable the Multi-Page Viewe
*/
public static final String MULTI_PAGE_NO = "n";
/**
* Align left, only scale if image is larger than browser width
*/
public static final String MULTI_PAGE_STYLE_N = "n";
/**
* Align center, only scale if image is larger than browser width
*/
public static final String MULTI_PAGE_STYLE_C = "c";
/**
* Align center, Always scale images to fit browser width
*/
public static final String MULTI_PAGE_STYLE_Y = "y";
/**
* Show Multi-Page Viewer Thumbnail Pane
*/
public static final String MULTI_PAGE_THUMB_SHOW = "n";
/**
* Hide Multi-Page Viewer Thumbnail Pane
*/
public static final String MULTI_PAGE_THUMB_HIDE = "y";
/**
* 460x for lofi resolution
*/
public static final String LOFI_RESOLUTION_460X = "1";
/**
* 780X for lofi resolution
*/
public static final String LOFI_RESOLUTION_780X = "2";
/**
* 980X for lofi resolution
*/
public static final String LOFI_RESOLUTION_980X = "3";
/**
* show warning
*/
public static final String CONTENT_WARNING_SHOW = "0";
/**
* not show warning
*/
public static final String CONTENT_WARNING_NOT_SHOW = "1";
/**
* The key of load images through the Hentai@Home Network
*
* @see #LOAD_FROM_HAH_YES
* @see #LOAD_FROM_HAH_NO
*/
private static final String KEY_LOAD_FROM_HAH = "uh";
/**
* The key of Image Size Settings
*
* @see #IMAGE_SIZE_AUTO
* @see #IMAGE_SIZE_780X
* @see #IMAGE_SIZE_980X
* @see #IMAGE_SIZE_1280X
* @see #IMAGE_SIZE_1600X
* @see #IMAGE_SIZE_2400X
*/
private static final String KEY_IMAGE_SIZE = "xr";
/**
* The key of scale images width
*/
private static final String KEY_SCALE_WIDTH = "rx";
/**
* The key of scale images height
*/
private static final String KEY_SCALE_HEIGHT = "ry";
/**
* The key of Gallery Name Display
*
* @see #GALLERY_TITLE_DEFAULT
* @see #GALLERY_TITLE_JAPANESE
*/
private static final String KEY_GALLERY_TITLE = "tl";
/**
* The key of the behavior for downloading archiver
*
* @see #ARCHIVER_DOWNLOAD_MAMS
* @see #ARCHIVER_DOWNLOAD_MAAS
* @see #ARCHIVER_DOWNLOAD_AAMS
* @see #ARCHIVER_DOWNLOAD_AAAS
*/
private static final String KEY_ARCHIVER_DOWNLOAD = "ar";
/**
* The key of display mode would you like to use on the front and search pages
*
* @see #LAYOUT_MODE_LIST
* @see #LAYOUT_MODE_THUMB
*/
private static final String KEY_LAYOUT_MODE = "dm";
/**
* The key for show popular
*
* @see #POPULAR_YES
* @see #POPULAR_NO
*/
private static final String KEY_POPULAR = "prn";
/**
* The key of categories would you like to view as default on the front page
*/
private static final String KEY_DEFAULT_CATEGORIES = "cats";
/**
* The key for favorites sort
*
* @see #FAVORITES_SORT_GALLERY_UPDATE_TIME
* @see #FAVORITES_SORT_FAVORITED_TIME
*/
private static final String KEY_FAVORITES_SORT = "fs";
/**
* The key of exclude certain namespaces from a default tag search
*/
private static final String KEY_EXCLUDED_NAMESPACES = "xns";
/**
* The key of hide galleries in certain languages from the gallery list and searches
*/
private static final String KEY_EXCLUDED_LANGUAGES = "xl";
/**
* The key of how many results would you like per page for the index/search page and torrent search pages
*
* @see #RESULT_COUNT_25
* @see #RESULT_COUNT_50
* @see #RESULT_COUNT_100
* @see #RESULT_COUNT_200
*/
private static final String KEY_RESULT_COUNT = "rc";
/**
* The key of mouse-over thumb
*
* @see #MOUSE_OVER_YES
* @see #MOUSE_OVER_NO
*/
private static final String KEY_MOUSE_OVER = "lt";
/**
* The key of preview size
*
* @see #PREVIEW_SIZE_NORMAL
* @see #PREVIEW_SIZE_LARGE
*/
private static final String KEY_PREVIEW_SIZE = "ts";
/**
* The key of preview row per page
*
* @see #PREVIEW_ROW_4
* @see #PREVIEW_ROW_10
* @see #PREVIEW_ROW_20
* @see #PREVIEW_ROW_40
*/
private static final String KEY_PREVIEW_ROW = "tr";
/**
* The key of sort order for gallery comments
*
* @see #COMMENTS_SORT_OLDEST_FIRST
* @see #COMMENTS_SORT_RECENT_FIRST
* @see #COMMENTS_SORT_HIGHEST_SCORE_FIRST
*/
private static final String KEY_COMMENTS_SORT = "cs";
/**
* The key of show gallery comment votes
*
* @see #COMMENTS_VOTES_POP
* @see #COMMENTS_VOTES_ALWAYS
*/
private static final String KEY_COMMENTS_VOTES = "sc";
/**
* The key of sort order for gallery tags
*
* @see #TAGS_SORT_ALPHABETICAL
* @see #TAGS_SORT_POWER
*/
private static final String KEY_TAGS_SORT = "to";
/**
* The key of show gallery page numbers
*
* @see #SHOW_GALLERY_INDEX_NO
* @see #SHOW_GALLERY_INDEX_YES
*/
private static final String KEY_SHOW_GALLERY_INDEX = "pn";
/**
* The key of the IP:Port of a proxy-enabled Hentai@Home Client
* to load all images
*/
private static final String KEY_HAH_CLIENT_IP_PORT = "hp";
/**
* The key of the passkey of a proxy-enabled Hentai@Home Client
* to load all images
*/
private static final String KEY_HAH_CLIENT_PASSKEY = "hk";
/**
* The key of enable Tag Flagging
*
* @see #ENABLE_TAG_FLAGGING_NO
* @see #ENABLE_TAG_FLAGGING_YES
*/
private static final String KEY_ENABLE_TAG_FLAGGING = "tf";
/**
* The key of always display the original images instead of the resampled versions
*
* @see #ALWAYS_ORIGINAL_NO
* @see #ALWAYS_ORIGINAL_YES
*/
private static final String KEY_ALWAYS_ORIGINAL = "oi";
/**
* The key of enable the multi-Page Viewer
*
* @see #MULTI_PAGE_NO
* @see #MULTI_PAGE_YES
*/
private static final String KEY_MULTI_PAGE = "qb";
/**
* The key of multi-Page Viewer Display Style
*
* @see #MULTI_PAGE_STYLE_C
* @see #MULTI_PAGE_STYLE_N
* @see #MULTI_PAGE_STYLE_Y
*/
private static final String KEY_MULTI_PAGE_STYLE = "ms";
/**
* The key of multi-Page Viewer Thumbnail Pane
*
* @see #MULTI_PAGE_THUMB_HIDE
* @see #MULTI_PAGE_THUMB_SHOW
*/
private static final String KEY_MULTI_PAGE_THUMB = "mt";
/**
* Default gallery title
*/
private static final String GALLERY_TITLE_DEFAULT = "r";
/**
* Japanese gallery title
*/
private static final String GALLERY_TITLE_JAPANESE = "j";
/**
* Show popular
*/
private static final String POPULAR_YES = "y";
/**
* Don't show popular
*/
private static final String POPULAR_NO = "n";
/**
* Sort favorites by last gallery update time
*/
private static final String FAVORITES_SORT_GALLERY_UPDATE_TIME = "p";
/**
* Sort favorites by favorited time
*/
private static final String FAVORITES_SORT_FAVORITED_TIME = "f";
/**
* Load images through the Hentai@Home Network<br/>
* key: {@link #KEY_LOAD_FROM_HAH}<br/>
* value: {@link #LOAD_FROM_HAH_YES}, {@link #LOAD_FROM_HAH_NO}
*/
public String loadFromHAH = LOAD_FROM_HAH_YES;
/**
* Image Size<br/>
* key: {@link #KEY_IMAGE_SIZE}<br/>
* value: {@link #IMAGE_SIZE_AUTO}, {@link #IMAGE_SIZE_780X}, {@link #IMAGE_SIZE_980X},
* {@link #IMAGE_SIZE_1280X}, {@link #IMAGE_SIZE_1600X}, {@link #IMAGE_SIZE_2400X}
*/
public String imageSize = IMAGE_SIZE_AUTO;
/**
* Scale width<br/>
* key: {@link #KEY_SCALE_WIDTH}<br/>
* value: 0 for no limit
*/
public int scaleWidth = 0;
/**
* Scale height<br/>
* key: {@link #KEY_SCALE_HEIGHT}<br/>
* value: 0 for no limit
*/
public int scaleHeight = 0;
/**
* Gallery title<br/>
* key: {@link #KEY_GALLERY_TITLE}<br/>
* value: {@link #GALLERY_TITLE_DEFAULT}, {@link #GALLERY_TITLE_JAPANESE}
*/
public String galleryTitle = GALLERY_TITLE_DEFAULT;
/**
* The default behavior for downloading an archiver<br/>
* key: {@link #KEY_ARCHIVER_DOWNLOAD}<br/>
* value: {@link #ARCHIVER_DOWNLOAD_MAMS}, {@link #ARCHIVER_DOWNLOAD_AAMS},
* {@link #ARCHIVER_DOWNLOAD_MAAS}, {@link #ARCHIVER_DOWNLOAD_AAAS}
*/
public String archiverDownload = ARCHIVER_DOWNLOAD_MAMS;
/**
* Display mode used on the front and search pages<br/>
* false for list, true for thumb<br/>
* key: {@link #KEY_LAYOUT_MODE}<br/>
* value: {@link #LAYOUT_MODE_LIST}, {@link #LAYOUT_MODE_THUMB}
*/
public String layoutMode = LAYOUT_MODE_LIST;
/**
* Show popular or not<br/>
* key: {@link #KEY_POPULAR}<br/>
* value: {@link #POPULAR_YES}, {@link #POPULAR_NO}
*/
public String popular = POPULAR_YES;
/**
* Default categories on the front page<br/>
* key: {@link #KEY_DEFAULT_CATEGORIES}<br/>
* value: the value of categories, for multiple use & operation,
* 0 for none
*/
public int defaultCategories = 0;
/**
* <br/>
* key: {@link #KEY_FAVORITES_SORT}<br/>
* value: {@link #FAVORITES_SORT_GALLERY_UPDATE_TIME}, {@link #FAVORITES_SORT_FAVORITED_TIME}
*/
public String favoritesSort = FAVORITES_SORT_FAVORITED_TIME;
/**
* Certain namespaces excluded from a default tag search<br/>
* key: {@link #KEY_EXCLUDED_NAMESPACES}<br/>
* value: the value of namespaces, for multiple use & operation,
* 0 for none
*/
public int excludedNamespaces = 0;
/**
* Certain languages excluded from list and searches<br/>
* key: {@link #KEY_EXCLUDED_LANGUAGES}<br/>
* value: {@link #JAPANESE_TRANSLATED}, {@link #JAPANESE_REWRITE} ...
* For multiple languages, use <code>x<code/> to combine them, like 1x1024x2048
*/
public String excludedLanguages = "";
/**
* How many results would you like per page for the index/search page
* and torrent search pages<br/>
* key: {@link #KEY_RESULT_COUNT}<br/>
* value: {@link #RESULT_COUNT_25}, {@link #RESULT_COUNT_50},
* {@link #RESULT_COUNT_100}, {@link #RESULT_COUNT_200}<br/>
* Require <code>Hath Perk:Paging Enlargement</code>
*/
public String resultCount = RESULT_COUNT_25;
/**
* mouse-over thumb<br/>
* key: {@link #KEY_MOUSE_OVER}<br/>
* value: {@link #MOUSE_OVER_YES}, {@link #MOUSE_OVER_NO}
*/
public String mouseOver = MOUSE_OVER_YES;
/**
* Default preview mode<br/>
* key: {@link #KEY_PREVIEW_SIZE}<br/>
* value: {@link #PREVIEW_SIZE_NORMAL}, {@link #PREVIEW_SIZE_LARGE}
*/
public String previewSize = PREVIEW_SIZE_LARGE;
/**
* Preview row<br/>
* key: {@link #KEY_PREVIEW_ROW}<br/>
* value: {@link #PREVIEW_ROW_4}, {@link #PREVIEW_ROW_10},
* {@link #PREVIEW_ROW_20}, {@link #PREVIEW_ROW_40}
*/
public String previewRow = PREVIEW_ROW_4;
/**
* Sort order for gallery comments<br/>
* key: {@link #KEY_COMMENTS_SORT}<br/>
* value: {@link #COMMENTS_SORT_OLDEST_FIRST}, {@link #COMMENTS_SORT_RECENT_FIRST},
* {@link #COMMENTS_SORT_HIGHEST_SCORE_FIRST}
*/
public String commentSort = COMMENTS_SORT_OLDEST_FIRST;
/**
* Show gallery comment votes mode<br/>
* key: {@link #KEY_COMMENTS_VOTES}<br/>
* value: {@link #COMMENTS_VOTES_POP}, {@link #COMMENTS_VOTES_ALWAYS}
*/
public String commentVotes = COMMENTS_VOTES_POP;
/**
* Sort order for gallery tags<br/>
* key: {@link #KEY_TAGS_SORT}<br/>
* value: {@link #TAGS_SORT_ALPHABETICAL}, {@link #TAGS_SORT_POWER}
*/
public String tagSort = TAGS_SORT_ALPHABETICAL;
/**
* Show gallery page numbers<br/>
* key: {@link #KEY_SHOW_GALLERY_INDEX}<br/>
* value: {@link #SHOW_GALLERY_INDEX_YES}, {@link #SHOW_GALLERY_INDEX_NO}
*/
public String showGalleryIndex = SHOW_GALLERY_INDEX_YES;
/**
* The IP of a proxy-enabled Hentai@Home Client
* to load all images<br/>
* key: {@link #KEY_HAH_CLIENT_IP_PORT}<br/>
*/
public String hahClientIp = "";
/**
* The PORT of a proxy-enabled Hentai@Home Client
* to load all images<br/>
* key: {@link #KEY_HAH_CLIENT_IP_PORT}<br/>
*/
public int hahClientPort = -1;
/**
* The passkey of a proxy-enabled Hentai@Home Client
* to load all images<br/>
* key: {@link #KEY_HAH_CLIENT_PASSKEY}<br/>
*/
public String hahClientPasskey = "";
/**
* Enable tag flagging
* key: {@link #KEY_ENABLE_TAG_FLAGGING}<br/>
* value: {@link #ENABLE_TAG_FLAGGING_YES}, {@link #ENABLE_TAG_FLAGGING_NO}<br/>
* <code>Bronze Star</code> or <code>Hath Perk: Tag Flagging</code> Required
*/
public String enableTagFlagging = ENABLE_TAG_FLAGGING_NO;
/**
* Always display the original images instead of the resampled versions<br/>
* key: {@link #KEY_ALWAYS_ORIGINAL}<br/>
* value: {@link #ALWAYS_ORIGINAL_YES}, {@link #ALWAYS_ORIGINAL_NO}<br/>
* <code>Silver Star</code> or <code>Hath Perk: Source Nexus</code> Required
*/
public String alwaysOriginal = ALWAYS_ORIGINAL_NO;
/**
* Enable the multi-Page Viewer<br/>
* key: {@link #KEY_MULTI_PAGE}<br/>
* value: {@link #MULTI_PAGE_YES}, {@link #MULTI_PAGE_NO}<br/>
* <code>Gold Star</code> or <code>Hath Perk: Multi-Page Viewer</code> Required
*/
public String multiPage = MULTI_PAGE_NO;
/**
* Multi-Page Viewer Display Style<br/>
* key: {@link #KEY_MULTI_PAGE_STYLE}<br/>
* value: {@link #MULTI_PAGE_STYLE_N}, {@link #MULTI_PAGE_STYLE_C},
* {@link #MULTI_PAGE_STYLE_Y}<br/>
* <code>Gold Star</code> or <code>Hath Perk: Multi-Page Viewer</code> Required
*/
public String multiPageStyle = MULTI_PAGE_STYLE_N;
/**
* Multi-Page Viewer Thumbnail Pane<br/>
* key: {@link #KEY_MULTI_PAGE_THUMB}<br/>
* value: {@link #MULTI_PAGE_THUMB_SHOW}, {@link #MULTI_PAGE_THUMB_HIDE}<br/>
* <code>Gold Star</code> or <code>Hath Perk: Multi-Page Viewer</code> Required
*/
public String multiPageThumb = MULTI_PAGE_THUMB_SHOW;
/**
* Lofi resolution
* key: {@link #KEY_LOFI_RESOLUTION}<br/>
* value: {@link #LOFI_RESOLUTION_460X}, {@link #LOFI_RESOLUTION_780X},
* {@link #LOFI_RESOLUTION_980X}
*/
public String lofiResolution = LOFI_RESOLUTION_980X;
/**
* Show content warning
* key: {@link #KEY_CONTENT_WARNING}<br/>
* value: {@link #CONTENT_WARNING_SHOW}, {@link #CONTENT_WARNING_NOT_SHOW}
*/
public String contentWarning = CONTENT_WARNING_NOT_SHOW;
private String mUconfig;
private boolean mDirty = true;
@Override
public EhConfig clone() {
try {
return (EhConfig) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
public synchronized void setDirty() {
mDirty = true;
}
private void updateUconfig() {
String hahClientIpPort = (InetValidator.isValidInet4Address(hahClientIp) &&
InetValidator.isValidInetPort(hahClientPort)) ?
hahClientIp + "%3A" + hahClientPort : "";
String hahClientPasskey = null == this.hahClientPasskey ? "" : this.hahClientPasskey;
mUconfig = KEY_LOAD_FROM_HAH + "_" + loadFromHAH + "-" +
KEY_IMAGE_SIZE + "_" + imageSize + "-" +
KEY_SCALE_WIDTH + "_" + scaleWidth + "-" +
KEY_SCALE_HEIGHT + "_" + scaleHeight + "-" +
KEY_GALLERY_TITLE + "_" + galleryTitle + "-" +
KEY_ARCHIVER_DOWNLOAD + "_" + archiverDownload + "-" +
KEY_LAYOUT_MODE + "_" + layoutMode + "-" +
KEY_POPULAR + "_" + popular + "-" +
KEY_DEFAULT_CATEGORIES + "_" + defaultCategories + "-" +
KEY_FAVORITES_SORT + "_" + favoritesSort + "-" +
KEY_EXCLUDED_NAMESPACES + "_" + excludedNamespaces + "-" +
KEY_EXCLUDED_LANGUAGES + "_" + excludedLanguages + "-" +
KEY_RESULT_COUNT + "_" + resultCount + "-" +
KEY_MOUSE_OVER + "_" + mouseOver + "-" +
KEY_PREVIEW_SIZE + "_" + previewSize + "-" +
KEY_PREVIEW_ROW + "_" + previewRow + "-" +
KEY_COMMENTS_SORT + "_" + commentSort + "-" +
KEY_COMMENTS_VOTES + "_" + commentVotes + "-" +
KEY_TAGS_SORT + "_" + tagSort + "-" +
KEY_SHOW_GALLERY_INDEX + "_" + showGalleryIndex + "-" +
KEY_HAH_CLIENT_IP_PORT + "_" + hahClientIpPort + "-" +
KEY_HAH_CLIENT_PASSKEY + "_" + hahClientPasskey + "-" +
KEY_ENABLE_TAG_FLAGGING + "_" + enableTagFlagging + "-" +
KEY_ALWAYS_ORIGINAL + "_" + alwaysOriginal + "-" +
KEY_MULTI_PAGE + "_" + multiPage + "-" +
KEY_MULTI_PAGE_STYLE + "_" + multiPageStyle + "-" +
KEY_MULTI_PAGE_THUMB + "_" + multiPageThumb;
}
public synchronized String uconfig() {
if (mDirty) {
mDirty = false;
updateUconfig();
}
return mUconfig;
}
}
@@ -16,8 +16,9 @@
package com.hippo.ehviewer.client
import com.hippo.ehviewer.EhApplication
import com.hippo.network.CookieDatabase
import com.hippo.network.CookieSet
import com.hippo.ehviewer.network.CookieDatabase
import com.hippo.ehviewer.network.CookieSet
import eu.kanade.tachiyomi.util.lang.launchIO
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
@@ -26,7 +27,7 @@ import java.util.Collections
import java.util.regex.Pattern
object EhCookieStore : CookieJar {
private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie.db")
private val db: CookieDatabase = CookieDatabase(EhApplication.application)
private val map: MutableMap<String, CookieSet> = db.allCookies
fun signOut() {
@@ -35,24 +36,31 @@ object EhCookieStore : CookieJar {
fun hasSignedIn(): Boolean {
val url = EhUrl.HOST_E.toHttpUrl()
return contains(url, KEY_IPD_MEMBER_ID) &&
contains(url, KEY_IPD_PASS_HASH)
return contains(url, KEY_IPB_MEMBER_ID) &&
contains(url, KEY_IPB_PASS_HASH)
}
const val KEY_IPD_MEMBER_ID = "ipb_member_id"
const val KEY_IPD_PASS_HASH = "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"
const val KEY_SETTINGS_PROFILE = "sp"
const val KEY_STAR = "star"
private const val KEY_CONTENT_WARNING = "nw"
private const val CONTENT_WARNING_NOT_SHOW = "1"
private val sTipsCookie: Cookie = Cookie.Builder()
.name(EhConfig.KEY_CONTENT_WARNING)
.value(EhConfig.CONTENT_WARNING_NOT_SHOW)
.name(KEY_CONTENT_WARNING)
.value(CONTENT_WARNING_NOT_SHOW)
.domain(EhUrl.DOMAIN_E)
.path("/")
.expiresAt(Long.MAX_VALUE)
.build()
fun newCookie(
cookie: Cookie, newDomain: String, forcePersistent: Boolean,
forceLongLive: Boolean, forceNotHostOnly: Boolean
cookie: Cookie,
newDomain: String,
forcePersistent: Boolean = false,
forceLongLive: Boolean = false,
forceNotHostOnly: Boolean = false,
): Cookie {
val builder = Cookie.Builder()
builder.name(cookie.name)
@@ -79,6 +87,21 @@ object EhCookieStore : CookieJar {
return builder.build()
}
fun copyCookie(domain: String, newDomain: String, name: String, path: String = "/") {
val cookie = map[domain]?.get(name, domain, path)
cookie?.let { addCookie(newCookie(it, newDomain)) }
}
fun deleteCookie(url: HttpUrl, name: String) {
val deletedCookie = Cookie.Builder()
.name(name)
.value("deleted")
.domain(url.host)
.expiresAt(0)
.build()
addCookie(deletedCookie)
}
@Synchronized
fun addCookie(cookie: Cookie) {
// For cookie database
@@ -173,17 +196,17 @@ object EhCookieStore : CookieJar {
@Synchronized
fun clear() {
map.clear()
db.clear()
}
@Synchronized
fun close() {
db.close()
launchIO {
db.clear()
}
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
addCookie(cookie)
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
if (cookie.name != "__utmp") {
addCookie(cookie)
}
}
}
@@ -214,10 +237,12 @@ object EhCookieStore : CookieJar {
val urlHost = url.host
return if (urlHost == domain) {
true // As in 'example.com' matching 'example.com'.
} else urlHost.endsWith(domain!!)
&& urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(
urlHost
)
} else {
urlHost.endsWith(domain!!) &&
urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(
urlHost,
)
}
// As in 'example.com' matching 'www.example.com'.
}
@@ -229,10 +254,7 @@ object EhCookieStore : CookieJar {
// Add all but skip some
for (cookie in cookies) {
val name = cookie.name
if (EhConfig.KEY_CONTENT_WARNING == name) {
continue
}
if (EhConfig.KEY_UCONFIG == name) {
if (KEY_CONTENT_WARNING == name) {
continue
}
result.add(cookie)
@@ -244,4 +266,4 @@ object EhCookieStore : CookieJar {
cookies
}
}
}
}
@@ -27,7 +27,7 @@ object EhDns : Dns {
private val builtInHosts: MutableMap<String, List<InetAddress>> = mutableMapOf()
init {
/* Pair(ip: String!, blockedByCCP: Boolean!) */
// Pair(ip: String!, blockedByCCP: Boolean!)
val ehgtHosts = arrayOf(
Pair("37.48.89.44", false),
Pair("81.171.10.48", false),
@@ -56,6 +56,13 @@ object EhDns : Dns {
Pair("178.175.132.20", false),
Pair("178.175.132.22", false),
)
put(
builtInHosts,
"s.exhentai.org",
Pair("178.175.129.254", false),
Pair("178.175.128.254", false),
Pair("178.175.132.22", false),
)
put(
builtInHosts,
"repo.e-hentai.org",
@@ -70,27 +77,27 @@ object EhDns : Dns {
put(
builtInHosts,
"ehgt.org",
*ehgtHosts
*ehgtHosts,
)
put(
builtInHosts,
"gt0.ehgt.org",
*ehgtHosts
*ehgtHosts,
)
put(
builtInHosts,
"gt1.ehgt.org",
*ehgtHosts
*ehgtHosts,
)
put(
builtInHosts,
"gt2.ehgt.org",
*ehgtHosts
*ehgtHosts,
)
put(
builtInHosts,
"gt3.ehgt.org",
*ehgtHosts
*ehgtHosts,
)
put(
builtInHosts,
@@ -111,20 +118,20 @@ object EhDns : Dns {
private fun put(
map: MutableMap<String, List<InetAddress>>,
host: String,
vararg ips: Pair<String, Boolean>
vararg ips: Pair<String, Boolean>,
) {
map[host] = ips.mapNotNull { pair ->
Hosts.toInetAddress(host, pair.first).takeUnless { Settings.getDF() && pair.second }
Hosts.toInetAddress(host, pair.first).takeUnless { Settings.dF && pair.second }
}
}
@Throws(UnknownHostException::class)
override fun lookup(hostname: String): List<InetAddress> {
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.getBuiltInHosts() }
?: Dns.SYSTEM.lookup(hostname)
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.builtInHosts }
?: Dns.SYSTEM.lookup(hostname)
}
fun isInHosts(hostname: String): Boolean {
return hosts.contains(hostname) || (builtInHosts.contains(hostname) && Settings.getBuiltInHosts())
return hosts.contains(hostname) || (builtInHosts.contains(hostname) && Settings.builtInHosts)
}
}
File diff suppressed because it is too large. Load diff
@@ -16,19 +16,20 @@
package com.hippo.ehviewer.client
import android.util.Log
import arrow.core.memoize
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.Filter
import java.util.Locale
import java.util.regex.Pattern
private val regex = { p: Filter -> Regex(p.text!!) }.memoize()
object EhFilter {
private val mTitleFilterList: MutableList<Filter> = ArrayList()
private val mUploaderFilterList: MutableList<Filter> = ArrayList()
private val mTagFilterList: MutableList<Filter> = ArrayList()
private val mTagNamespaceFilterList: MutableList<Filter> = ArrayList()
private val mCommenterFilterList: MutableList<Filter> = ArrayList()
private val mCommentFilterList: MutableList<Filter> = ArrayList()
val titleFilterList = mutableListOf<Filter>()
val uploaderFilterList = mutableListOf<Filter>()
val tagFilterList = mutableListOf<Filter>()
val tagNamespaceFilterList = mutableListOf<Filter>()
val commenterFilterList = mutableListOf<Filter>()
val commentFilterList = mutableListOf<Filter>()
const val MODE_TITLE = 0
const val MODE_UPLOADER = 1
@@ -36,83 +37,44 @@ object EhFilter {
const val MODE_TAG_NAMESPACE = 3
const val MODE_COMMENTER = 4
const val MODE_COMMENT = 5
private val TAG = EhFilter::class.java.simpleName
private const val TAG = "EhFilter"
init {
val list = EhDB.getAllFilter()
var i = 0
val n = list.size
while (i < n) {
val filter = list[i]
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTitleFilterList.add(filter)
}
init { EhDB.allFilter.forEach(::memorizeFilter) }
MODE_TAG -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagFilterList.add(filter)
}
MODE_TAG_NAMESPACE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagNamespaceFilterList.add(filter)
}
MODE_UPLOADER -> mUploaderFilterList.add(filter)
MODE_COMMENTER -> mCommenterFilterList.add(filter)
MODE_COMMENT -> mCommentFilterList.add(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
i++
}
}
val titleFilterList: List<Filter>
get() = mTitleFilterList
val uploaderFilterList: List<Filter>
get() = mUploaderFilterList
val tagFilterList: List<Filter>
get() = mTagFilterList
val tagNamespaceFilterList: List<Filter>
get() = mTagNamespaceFilterList
val commenterFilterList: List<Filter>
get() = mCommenterFilterList
val commentFilterList: List<Filter>
get() = mCommentFilterList
@Synchronized
fun addFilter(filter: Filter): Boolean {
// enable filter by default before it is added to database
filter.enable = true
if (!EhDB.addFilter(filter)) return false
private fun memorizeFilter(filter: Filter) {
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTitleFilterList.add(filter)
filter.text = filter.text!!.lowercase()
titleFilterList.add(filter)
}
MODE_TAG -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagFilterList.add(filter)
filter.text = filter.text!!.lowercase()
tagFilterList.add(filter)
}
MODE_TAG_NAMESPACE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagNamespaceFilterList.add(filter)
filter.text = filter.text!!.lowercase()
tagNamespaceFilterList.add(filter)
}
MODE_UPLOADER -> mUploaderFilterList.add(filter)
MODE_COMMENTER -> mCommenterFilterList.add(filter)
MODE_COMMENT -> mCommentFilterList.add(filter)
MODE_UPLOADER -> uploaderFilterList.add(filter)
MODE_COMMENTER -> commenterFilterList.add(filter)
MODE_COMMENT -> commentFilterList.add(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
}
@Synchronized
fun addFilter(filter: Filter): Boolean {
filter.enable = true
if (!EhDB.addFilter(filter)) return false
memorizeFilter(filter)
return true
}
@Synchronized
fun triggerFilter(filter: Filter?) {
fun triggerFilter(filter: Filter) {
EhDB.triggerFilter(filter)
}
@@ -120,196 +82,72 @@ object EhFilter {
fun deleteFilter(filter: Filter) {
EhDB.deleteFilter(filter)
when (filter.mode) {
MODE_TITLE -> mTitleFilterList.remove(filter)
MODE_TAG -> mTagFilterList.remove(filter)
MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter)
MODE_UPLOADER -> mUploaderFilterList.remove(filter)
MODE_COMMENTER -> mCommenterFilterList.remove(filter)
MODE_COMMENT -> mCommentFilterList.remove(filter)
MODE_TITLE -> titleFilterList.remove(filter)
MODE_TAG -> tagFilterList.remove(filter)
MODE_TAG_NAMESPACE -> tagNamespaceFilterList.remove(filter)
MODE_UPLOADER -> uploaderFilterList.remove(filter)
MODE_COMMENTER -> commenterFilterList.remove(filter)
MODE_COMMENT -> commentFilterList.remove(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
}
@Synchronized
fun needTags(): Boolean {
return 0 != mTagFilterList.size || 0 != mTagNamespaceFilterList.size
return 0 != tagFilterList.size || 0 != tagNamespaceFilterList.size
}
@Synchronized
fun filterTitle(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Title
val title = info.title
val filters: List<Filter> = mTitleFilterList
if (null != title && filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && title.lowercase(Locale.getDefault()).contains(
filters[i].text!!
)
) {
return false
}
i++
}
}
return true
fun filterTitle(info: GalleryInfo): Boolean {
val title = info.title ?: return false
return titleFilterList.any { it.enable!! && it.text!! in title.lowercase() }
}
@Synchronized
fun filterUploader(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Uploader
val uploader = info.uploader
val filters: List<Filter> = mUploaderFilterList
if (null != uploader && filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && uploader == filters[i].text) {
return false
}
i++
}
}
return true
fun filterUploader(info: GalleryInfo): Boolean {
val uploader = info.uploader ?: return false
return uploaderFilterList.any { it.enable!! && it.text == uploader }
}
private fun matchTag(tag: String?, filter: String?): Boolean {
if (null == tag || null == filter) {
return false
}
val tagNamespace: String?
val tagName: String
val filterNamespace: String?
val filterName: String
var index = tag.indexOf(':')
if (index < 0) {
tagNamespace = null
tagName = tag
} else {
tagNamespace = tag.substring(0, index)
tagName = tag.substring(index + 1)
}
index = filter.indexOf(':')
if (index < 0) {
filterNamespace = null
filterName = filter
} else {
filterNamespace = filter.substring(0, index)
filterName = filter.substring(index + 1)
}
return if (null != tagNamespace && null != filterNamespace &&
tagNamespace != filterNamespace
) {
private fun spiltTag(tag: String) = tag.run {
val index = indexOf(':')
if (index < 0) null to this else substring(0, index) to substring(index + 1)
}
private fun matchTag(tag: String, filter: String): Boolean {
val (tagNamespace, tagName) = spiltTag(tag)
val (filterNamespace, filterName) = spiltTag(filter)
return if (null != tagNamespace && null != filterNamespace && tagNamespace != filterNamespace) {
false
} else tagName == filterName
}
@Synchronized
fun filterTag(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Tag
val tags = info.simpleTags
val filters: List<Filter> = mTagFilterList
if (null != tags && filters.isNotEmpty()) {
for (tag in tags) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && matchTag(tag, filters[i].text)) {
return false
}
i++
}
}
}
return true
}
private fun matchTagNamespace(tag: String?, filter: String?): Boolean {
if (null == tag || null == filter) {
return false
}
val tagNamespace: String
val index = tag.indexOf(':')
return if (index >= 0) {
tagNamespace = tag.substring(0, index)
tagNamespace == filter
} else {
false
tagName == filterName
}
}
@Synchronized
fun filterTagNamespace(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
val tags = info.simpleTags
val filters: List<Filter> = mTagNamespaceFilterList
if (null != tags && filters.isNotEmpty()) {
for (tag in tags) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && matchTagNamespace(tag, filters[i].text)) {
return false
}
i++
}
}
}
return true
fun filterTag(info: GalleryInfo): Boolean {
val tags = info.simpleTags ?: return false
return tags.any { tag -> tagFilterList.any { it.enable!! && matchTag(tag, it.text!!) } }
}
private fun matchTagNamespace(tag: String, filter: String): Boolean {
val (nameSpace, _) = spiltTag(tag)
return nameSpace == filter
}
@Synchronized
fun filterCommenter(commenter: String?): Boolean {
if (null == commenter) {
return false
}
val filters: List<Filter> = mCommenterFilterList
if (filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && commenter == filters[i].text) {
return false
}
i++
}
}
return true
fun filterTagNamespace(info: GalleryInfo): Boolean {
val tags = info.simpleTags ?: return false
return tags.any { tag -> tagNamespaceFilterList.any { it.enable!! && matchTagNamespace(tag, it.text!!) } }
}
@Synchronized
fun filterComment(comment: String?): Boolean {
if (null == comment) {
return false
}
val filters: List<Filter> = mCommentFilterList
if (filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!!) {
val p = Pattern.compile(filters[i].text!!)
val m = p.matcher(comment)
if (m.find()) return false
}
i++
}
}
return true
fun filterCommenter(commenter: String): Boolean {
return commenterFilterList.any { it.enable!! && it.text == commenter }
}
}
@Synchronized
fun filterComment(comment: String): Boolean {
return commentFilterList.any { it.enable!! && regex(it).containsMatchIn(comment) }
}
}
@@ -1,80 +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.app.Activity
import androidx.annotation.MainThread
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
class EhRequest {
internal var job: Job? = null
val isActive
get() = job?.isActive ?: false
var method = 0
private set
var args: Array<out Any?>? = null
private set
var callback: EhClient.Callback<Any?>? = null
private set
fun setMethod(method: Int): EhRequest {
this.method = method
return this
}
fun setArgs(vararg args: Any?): EhRequest {
this.args = args
return this
}
@Suppress("UNCHECKED_CAST")
fun setCallback(callback: EhClient.Callback<*>?): EhRequest {
this.callback = callback as EhClient.Callback<Any?>?
return this
}
fun enqueue(scope: CoroutineScope) {
EhClient.enqueue(this, scope)
}
@DelicateCoroutinesApi
fun enqueue() {
EhClient.enqueue(this, GlobalScope)
}
fun enqueue(fragment: Fragment) {
enqueue(fragment.viewLifecycleOwner.lifecycleScope)
}
fun enqueue(activity: Activity) {
check(activity is FragmentActivity)
enqueue(activity.lifecycleScope)
}
@MainThread
fun cancel() {
if (isActive) {
job?.cancel()
callback?.onCancel()
}
}
}
@@ -0,0 +1,69 @@
/*
* 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 com.hippo.ehviewer.EhApplication.Companion.nonCacheOkHttpClient
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
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 org.json.JSONArray
import org.json.JSONObject
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
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 JSONObject.array(name: String, builder: JSONArray.() -> Unit): JSONObject = put(name, JSONArray().apply(builder))
fun jsonArrayOf(vararg element: Any?) = JSONArray().apply { element.forEach { put(it) } } // Should ensure it is inlined
inline fun Request.Builder.jsonBody(builder: JSONObject.() -> Unit) = post(JSONObject().apply(builder).toString().toRequestBody(MEDIA_TYPE_JSON))
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(block: Response.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return nonCacheOkHttpClient.newCall(this).executeAsync().use(block)
}
const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 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"
@@ -23,6 +23,8 @@ import java.net.Socket
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
private const val EXCEPTIONAL_DOMAIN = "hath.network"
object EhSSLSocketFactory : SSLSocketFactory() {
private val sslSocketFactory = getDefault() as SSLSocketFactory
override fun getDefaultCipherSuites(): Array<String> {
@@ -34,12 +36,12 @@ object EhSSLSocketFactory : SSLSocketFactory() {
}
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
val address = s.inetAddress.hostAddress.takeIf { EhDns.isInHosts(host) }
val address = s.inetAddress.hostAddress.takeIf { EhDns.isInHosts(host) || EXCEPTIONAL_DOMAIN in host }
val socket = sslSocketFactory.createSocket(s, address ?: host, port, autoClose) as SSLSocket
val sslSession = socket.session
Log.d(
"EhSSLSocketFactory",
"Host: " + host + " Address: " + address + " Protocol:" + sslSession.protocol + " CipherSuite:" + sslSession.cipherSuite
"Host: " + host + " Address: " + address + " Protocol:" + sslSession.protocol + " CipherSuite:" + sslSession.cipherSuite,
)
return socket
}
@@ -52,7 +54,7 @@ object EhSSLSocketFactory : SSLSocketFactory() {
host: String,
port: Int,
localHost: InetAddress,
localPort: Int
localPort: Int,
): Socket {
return sslSocketFactory.createSocket(host, port, localHost, localPort)
}
@@ -65,7 +67,7 @@ object EhSSLSocketFactory : SSLSocketFactory() {
address: InetAddress,
port: Int,
localAddress: InetAddress,
localPort: Int
localPort: Int,
): Socket {
return sslSocketFactory.createSocket(address, port, localAddress, localPort)
}
@@ -18,94 +18,114 @@ package com.hippo.ehviewer.client
import android.content.Context
import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.EhApplication.Companion.nonCacheOkHttpClient
import com.hippo.ehviewer.R
import com.hippo.util.ExceptionUtils
import com.hippo.util.HashCodeUtils
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IOUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import com.hippo.ehviewer.yorozuya.FileUtils
import com.hippo.ehviewer.yorozuya.copyToFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.BufferedSource
import okhttp3.executeAsync
import okio.HashingSink.Companion.sha1
import okio.blackholeSink
import okio.buffer
import okio.source
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
object EhTagDatabase {
private lateinit var tags: JSONObject
private lateinit var tagList: ArrayList<Pair<String, String>>
private typealias TagGroup = Map<String, String>
private typealias TagGroups = Map<String, TagGroup>
object EhTagDatabase : CoroutineScope {
private const val NAMESPACE_PREFIX = "n"
private lateinit var tagGroups: TagGroups
private val updateLock = Mutex()
private val dbLock = Mutex()
override val coroutineContext = Dispatchers.IO + Job()
fun isInitialized(): Boolean {
return this::tags.isInitialized && this::tagList.isInitialized
return this::tagGroups.isInitialized
}
private fun updateData(source: BufferedSource) {
try {
val tmpTags = JSONObject(source.readString(StandardCharsets.UTF_8))
val tmpTagList = arrayListOf<Pair<String, String>>()
tmpTags.keys().forEachRemaining { prefix: String ->
val values = tmpTags.optJSONObject(prefix)
values?.let {
it.keys().forEachRemaining { tag: String ->
tmpTagList.add(Pair("$prefix:$tag", values.optString(tag)))
private fun JSONObject.toTagGroups(): TagGroups =
keys().asSequence().associateWith { getJSONObject(it).toTagGroup() }
private fun JSONObject.toTagGroup(): TagGroup =
keys().asSequence().associateWith { getString(it) }
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
}
private fun internalSuggestFlow(
tags: Map<String, String>,
keyword: String,
translate: Boolean,
exactly: Boolean,
): Flow<Pair<String?, String>> = flow {
if (exactly) {
tags[keyword]?.let {
emit(Pair(it.takeIf { translate }, keyword))
}
} else {
if (translate) {
tags.forEach { (tag, hint) ->
if (tag != keyword &&
(tag.containsIgnoreSpace(keyword) || hint.containsIgnoreSpace(keyword))
) {
emit(Pair(hint, tag))
}
}
} else {
tags.keys.forEach { tag ->
if (tag != keyword && tag.containsIgnoreSpace(keyword)) {
emit(Pair(null, tag))
}
}
}
tags = tmpTags
tagList = tmpTagList
} catch (e: JSONException) {
e.printStackTrace()
}
}
fun getTranslation(prefix: String?, tag: String?): String? {
return tags.optJSONObject(prefix)?.optString(tag)?.trim()?.takeIf { it.isNotEmpty() }
}
/* Construct a cold flow for tag database suggestions */
// Construct a cold flow for tag database suggestions
fun suggestFlow(
keyword: String,
translate: Boolean,
exactly: Boolean = false
exactly: Boolean = false,
): Flow<Pair<String?, String>> = flow {
tagList.forEach {
val tag = it.first
val hint = if (translate) it.second else null
val index = tag.indexOf(':')
if (tag.substring(index + 1).trim() == keyword) {
if (exactly)
emit(Pair(hint, tag))
return@forEach
val keywordPrefix = keyword.substringBefore(':')
val keywordTag = keyword.drop(keywordPrefix.length + 1)
val prefix = namespaceToPrefix(keywordPrefix) ?: keywordPrefix
val tags = tagGroups[prefix.takeIf { keywordTag.isNotEmpty() && it != NAMESPACE_PREFIX }]
tags?.let {
internalSuggestFlow(it, keywordTag, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$prefix:$tag"))
}
if (exactly)
return@forEach
val keywordMatches: Boolean =
if (index == -1 || index >= tag.length - 1 || keyword.length > 2) {
containsIgnoreSpace(tag, keyword)
} else {
containsIgnoreSpace(tag.substring(index + 1), keyword)
} ?: tagGroups.forEach { (prefix, tags) ->
if (prefix != NAMESPACE_PREFIX) {
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$prefix:$tag"))
}
} else {
internalSuggestFlow(tags, keyword, translate, exactly).collect { (hint, tag) ->
emit(Pair(hint, "$tag:"))
}
if (keywordMatches || containsIgnoreSpace(hint, keyword)) {
emit(Pair(hint, tag))
}
}
}
private fun containsIgnoreSpace(text: String?, key: String): Boolean {
return text != null && text.replace(" ", "").contains(key.replace(" ", ""))
}
private fun String.removeSpace(): String = replace(" ", "")
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
removeSpace().contains(other.removeSpace(), ignoreCase)
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
it["artist"] = "a"
@@ -136,68 +156,74 @@ object EhTagDatabase {
}
private fun getFileContent(file: File): String? {
try {
file.source().buffer().use { return it.readString(StandardCharsets.UTF_8) }
} catch (e: IOException) {
return null
}
return runCatching {
file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }
}.getOrNull()
}
private fun getFileSha1(file: File): String? {
try {
FileInputStream(file).use { stream ->
val digest = MessageDigest.getInstance("SHA-1")
var n: Int
val buffer = ByteArray(4 * 1024)
while (stream.read(buffer).also { n = it } != -1) {
digest.update(buffer, 0, n)
return runCatching {
file.source().buffer().use { source ->
sha1(blackholeSink()).use {
source.readAll(it)
it.hash.hex()
}
return HashCodeUtils.bytesToHexString(digest.digest())
}
} catch (e: IOException) {
return null
} catch (e: NoSuchAlgorithmException) {
return null
}
}.getOrNull()
}
private fun checkData(sha1File: File, dataFile: File): Boolean {
val s1 = getFileContent(sha1File) ?: return false
val s2 = getFileSha1(dataFile) ?: return false
return s1 == s2
private fun checkData(sha1: String?, data: File): Boolean {
return sha1 != null && sha1 == getFileSha1(data)
}
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
private suspend fun save(client: OkHttpClient, url: String, file: File): Boolean {
val request: Request = Request.Builder().url(url).build()
val call = client.newCall(request)
try {
call.execute().use { response ->
runCatching {
call.executeAsync().use { response ->
if (!response.isSuccessful) {
return false
}
val body = response.body
body.byteStream()
.use { ins -> FileOutputStream(file).use { os -> IOUtils.copy(ins, os) } }
response.body.use {
it.copyToFile(file)
}
return true
}
} catch (t: Throwable) {
FileUtils.delete(file)
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
return false
}.onFailure {
file.delete()
it.printStackTrace()
}
return false
}
@OptIn(DelicateCoroutinesApi::class)
@JvmStatic
fun update() {
launchIO {
suspend fun update() {
updateLock.withLock {
updateInternal()
}
}
@Synchronized
private fun updateInternal() {
private suspend fun issueUpdateInMemoryData() {
dbLock.withLock {
getMetadata(EhApplication.application)?.let { urls ->
val dataName = urls[2]
val dir = AppConfig.getFilesDir("tag-translations")
val dataFile = File(dir, dataName).takeIf { it.exists() } ?: return
runSuspendCatching {
tagGroups = JSONObject(dataFile.source().buffer().readString(StandardCharsets.UTF_8)).toTagGroups()
}.onFailure {
it.printStackTrace()
}
}
}
}
init {
launch {
issueUpdateInMemoryData()
}
}
private suspend fun updateInternal() {
val urls = getMetadata(EhApplication.application)
urls?.let {
val sha1Name = urls[0]
@@ -205,38 +231,31 @@ object EhTagDatabase {
val dataName = urls[2]
val dataUrl = urls[3]
try {
val dir = AppConfig.getFilesDir("tag-translations")
checkNotNull(dir)
val dir = AppConfig.getFilesDir("tag-translations")
checkNotNull(dir)
val sha1File = File(dir, sha1Name)
val dataFile = File(dir, dataName)
runCatching {
// Check current sha1 and current data
val sha1File = File(dir, sha1Name)
val dataFile = File(dir, dataName)
if (!checkData(sha1File, dataFile)) {
val sha1 = getFileContent(sha1File)
if (!checkData(sha1, dataFile)) {
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
}
// Read current EhTagDatabase
if (!isInitialized() && dataFile.exists()) {
try {
dataFile.source().buffer().use { updateData(it) }
} catch (e: IOException) {
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
}
}
val client = okHttpClient
val client = nonCacheOkHttpClient
// Save new sha1
val tempSha1File = File(dir, "$sha1Name.tmp")
check(save(client, sha1Url, tempSha1File))
val tempSha1 = getFileContent(tempSha1File)
// Check new sha1 and current data
if (checkData(tempSha1File, dataFile)) {
// Check new sha1 and current sha1
if (tempSha1 == sha1) {
// The data is the same
FileUtils.delete(tempSha1File)
return
return@runCatching
}
// Save new data
@@ -244,10 +263,10 @@ object EhTagDatabase {
check(save(client, dataUrl, tempDataFile))
// Check new sha1 and new data
if (!checkData(tempSha1File, tempDataFile)) {
if (!checkData(tempSha1, tempDataFile)) {
FileUtils.delete(tempSha1File)
FileUtils.delete(tempDataFile)
return
return@runCatching
}
// Replace current sha1 and current data with new sha1 and new data
@@ -257,13 +276,10 @@ object EhTagDatabase {
tempDataFile.renameTo(dataFile)
// Read new EhTagDatabase
try {
dataFile.source().buffer().use { updateData(it) }
} catch (_: IOException) {
}
} catch (e: Throwable) {
e.printStackTrace()
issueUpdateInMemoryData()
}.onFailure {
it.printStackTrace()
}
}
}
}
}
@@ -16,7 +16,7 @@
package com.hippo.ehviewer.client
import com.hippo.ehviewer.Settings
import com.hippo.network.UrlBuilder
import com.hippo.ehviewer.network.UrlBuilder
object EhUrl {
const val SITE_E = 0
@@ -44,30 +44,30 @@ object EhUrl {
const val URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login"
const val URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00"
const val URL_FORUMS = "https://forums.e-hentai.org/"
const val URL_FUNDS = "https://e-hentai.org/exchange.php?t=gp"
const val URL_FUNDS = HOST_E + "exchange.php?t=gp"
const val URL_HOME = HOST_E + "home.php"
const val URL_NEWS = HOST_E + "news.php"
const val REFERER_EX = "https://$DOMAIN_EX"
const val ORIGIN_EX = REFERER_EX
const val REFERER_E = "https://$DOMAIN_E"
const val ORIGIN_E = REFERER_E
const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
const val URL_PREFIX_THUMB_EX = "https://exhentai.org/t/"
val host: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> HOST_E
SITE_EX -> HOST_EX
else -> HOST_E
}
val favoritesUrl: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> URL_FAVORITES_E
SITE_EX -> URL_FAVORITES_EX
else -> URL_FAVORITES_E
}
val apiUrl: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> API_E
SITE_EX -> API_EX
else -> API_E
@@ -75,14 +75,14 @@ object EhUrl {
@JvmStatic
val referer: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> REFERER_E
SITE_EX -> REFERER_EX
else -> REFERER_E
}
val origin: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> ORIGIN_E
SITE_EX -> ORIGIN_EX
else -> ORIGIN_E
@@ -90,7 +90,7 @@ object EhUrl {
@JvmStatic
val uConfigUrl: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> URL_UCONFIG_E
SITE_EX -> URL_UCONFIG_EX
else -> URL_UCONFIG_E
@@ -98,28 +98,28 @@ object EhUrl {
@JvmStatic
val myTagsUrl: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> URL_MY_TAGS_E
SITE_EX -> URL_MY_TAGS_EX
else -> URL_MY_TAGS_E
}
val popularUrl: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> URL_POPULAR_E
SITE_EX -> URL_POPULAR_EX
else -> URL_POPULAR_E
}
val imageSearchUrl: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> URL_IMAGE_SEARCH_E
SITE_EX -> URL_IMAGE_SEARCH_EX
else -> URL_IMAGE_SEARCH_E
}
val watchedUrl: String
get() = when (Settings.getGallerySite()) {
get() = when (Settings.gallerySite) {
SITE_E -> URL_WATCHED_E
SITE_EX -> URL_WATCHED_EX
else -> URL_WATCHED_E
@@ -163,12 +163,4 @@ object EhUrl {
fun getTagDefinitionUrl(tag: String): String {
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
}
fun getThumbUrlPrefix(): String {
return when (Settings.getGallerySite()) {
SITE_E -> URL_PREFIX_THUMB_E
SITE_EX -> URL_PREFIX_THUMB_EX
else -> URL_PREFIX_THUMB_E
}
}
}
}
@@ -16,13 +16,23 @@
package com.hippo.ehviewer.client
import android.text.TextUtils
import com.hippo.ehviewer.EhApplication.Companion.ehCookieStore
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.GalleryInfo
import java.util.regex.Pattern
object EhUtils {
const val NONE = -1 // Use it for homepage
const val MISC = 0x1
const val DOUJINSHI = 0x2
const val MANGA = 0x4
const val ARTIST_CG = 0x8
const val GAME_CG = 0x10
const val IMAGE_SET = 0x20
const val COSPLAY = 0x40
const val ASIAN_PORN = 0x80
const val NON_H = 0x100
const val WESTERN = 0x200
const val ALL_CATEGORY = 0x3ff
const val PRIVATE = 0x400
const val UNKNOWN = 0x800
@@ -41,37 +51,41 @@ object EhUtils {
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
private val PATTERN_TITLE_PREFIX = Pattern.compile(
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*"
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*",
)
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
Pattern.CASE_INSENSITIVE
Pattern.CASE_INSENSITIVE,
)
private val CATEGORY_VALUES = hashMapOf(
EhConfig.MISC to arrayOf("misc"),
EhConfig.DOUJINSHI to arrayOf("doujinshi"),
EhConfig.MANGA to arrayOf("manga"),
EhConfig.ARTIST_CG to arrayOf("artistcg", "Artist CG Sets", "Artist CG"),
EhConfig.GAME_CG to arrayOf("gamecg", "Game CG Sets", "Game CG"),
EhConfig.IMAGE_SET to arrayOf("imageset", "Image Sets", "Image Set"),
EhConfig.COSPLAY to arrayOf("cosplay"),
EhConfig.ASIAN_PORN to arrayOf("asianporn", "Asian Porn"),
EhConfig.NON_H to arrayOf("non-h"),
EhConfig.WESTERN to arrayOf("western"),
MISC to arrayOf("misc"),
DOUJINSHI to arrayOf("doujinshi"),
MANGA to arrayOf("manga"),
ARTIST_CG to arrayOf("artistcg", "Artist CG Sets", "Artist CG"),
GAME_CG to arrayOf("gamecg", "Game CG Sets", "Game CG"),
IMAGE_SET to arrayOf("imageset", "Image Sets", "Image Set"),
COSPLAY to arrayOf("cosplay"),
ASIAN_PORN to arrayOf("asianporn", "Asian Porn"),
NON_H to arrayOf("non-h"),
WESTERN to arrayOf("western"),
PRIVATE to arrayOf("private"),
UNKNOWN to arrayOf("unknown")
UNKNOWN to arrayOf("unknown"),
)
private val CATEGORY_STRINGS = CATEGORY_VALUES.entries.map { (k, v) -> v to k }
val isExHentai: Boolean
get() = Settings.gallerySite == EhUrl.SITE_EX
@JvmStatic
fun getCategory(type: String?): Int {
for (entry in CATEGORY_STRINGS) {
for (str in entry.first)
if (str.equals(type, ignoreCase = true))
if (str.equals(type, ignoreCase = true)) {
return entry.second
}
}
return UNKNOWN
}
@@ -82,39 +96,40 @@ object EhUtils {
fun getCategoryColor(category: Int): Int {
return when (category) {
EhConfig.DOUJINSHI -> BG_COLOR_DOUJINSHI
EhConfig.MANGA -> BG_COLOR_MANGA
EhConfig.ARTIST_CG -> BG_COLOR_ARTIST_CG
EhConfig.GAME_CG -> BG_COLOR_GAME_CG
EhConfig.WESTERN -> BG_COLOR_WESTERN
EhConfig.NON_H -> BG_COLOR_NON_H
EhConfig.IMAGE_SET -> BG_COLOR_IMAGE_SET
EhConfig.COSPLAY -> BG_COLOR_COSPLAY
EhConfig.ASIAN_PORN -> BG_COLOR_ASIAN_PORN
EhConfig.MISC -> BG_COLOR_MISC
DOUJINSHI -> BG_COLOR_DOUJINSHI
MANGA -> BG_COLOR_MANGA
ARTIST_CG -> BG_COLOR_ARTIST_CG
GAME_CG -> BG_COLOR_GAME_CG
WESTERN -> BG_COLOR_WESTERN
NON_H -> BG_COLOR_NON_H
IMAGE_SET -> BG_COLOR_IMAGE_SET
COSPLAY -> BG_COLOR_COSPLAY
ASIAN_PORN -> BG_COLOR_ASIAN_PORN
MISC -> BG_COLOR_MISC
else -> BG_COLOR_UNKNOWN
}.toInt()
}
fun signOut() {
ehCookieStore.signOut()
EhCookieStore.signOut()
Settings.putAvatar(null)
Settings.putDisplayName(null)
Settings.putGallerySite(EhUrl.SITE_E)
Settings.putNeedSignIn(true)
Settings.putSelectSite(true)
}
fun needSignedIn(): Boolean {
return Settings.getNeedSignIn() && !ehCookieStore.hasSignedIn()
return Settings.needSignIn
}
@JvmStatic
fun getSuitableTitle(gi: GalleryInfo): String? {
return if (Settings.getShowJpnTitle()) {
fun getSuitableTitle(gi: GalleryInfo): String {
return if (Settings.showJpnTitle) {
if (TextUtils.isEmpty(gi.titleJpn)) gi.title else gi.titleJpn
} else {
if (TextUtils.isEmpty(gi.title)) gi.titleJpn else gi.title
}
}.orEmpty()
}
fun extractTitle(fullTitle: String?): String? {
@@ -131,17 +146,12 @@ object EhUtils {
return title.ifEmpty { null }
}
fun fixThumbUrl(url: String): String {
return EhUrl.getThumbUrlPrefix() +
url.removePrefix(EhUrl.URL_PREFIX_THUMB_E).removePrefix(EhUrl.URL_PREFIX_THUMB_EX)
}
@JvmStatic
fun handleThumbUrlResolution(url: String?): String? {
if (null == url) {
return null
}
val resolution = when (Settings.getThumbResolution()) {
val resolution = when (Settings.thumbResolution) {
0 -> return url
1 -> "250"
2 -> "300"
@@ -155,4 +165,4 @@ object EhUtils {
url
}
}
}
}
@@ -5,7 +5,7 @@ interface AbstractGalleryInfo {
var token: String?
var title: String?
var titleJpn: String?
var thumb: String?
var thumbKey: String?
var category: Int
var posted: String?
var uploader: String?
@@ -22,4 +22,4 @@ interface AbstractGalleryInfo {
var simpleLanguage: String?
var favoriteSlot: Int
var favoriteName: String?
}
}
@@ -36,7 +36,7 @@ open class BaseGalleryInfo(
override var titleJpn: String? = null,
@ColumnInfo(name = "THUMB")
override var thumb: String? = null,
override var thumbKey: String? = null,
@ColumnInfo(name = "CATEGORY")
override var category: Int = 0,
@@ -84,6 +84,6 @@ open class BaseGalleryInfo(
override var favoriteSlot: Int = -2,
@Ignore
override var favoriteName: String? = null
override var favoriteName: String? = null,
) : GalleryInfo
) : GalleryInfo
@@ -17,8 +17,8 @@ package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.ehviewer.client.EhUrl
import com.hippo.network.UrlBuilder
import com.hippo.util.encodeUTF8
import com.hippo.ehviewer.network.UrlBuilder
import com.hippo.ehviewer.util.encodeUTF8
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -55,4 +55,4 @@ class FavListUrlBuilder(
return favCat in 0..9
}
}
}
}
@@ -34,5 +34,5 @@ class GalleryApiInfo(
var expunged: Boolean = false,
var rating: Float = 0f,
var torrentcount: Int = 0,
var tags: Array<String>?
) : Parcelable
var tags: Array<String>?,
) : Parcelable
@@ -46,5 +46,5 @@ class GalleryComment(
@JvmField
var comment: String? = null,
@JvmField
var lastEdited: Long = 0
) : Parcelable
var lastEdited: Long = 0,
) : Parcelable
@@ -20,8 +20,6 @@ import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryCommentList(
@JvmField
var comments: Array<GalleryComment>?,
@JvmField
var hasMore: Boolean
) : Parcelable
var comments: Array<GalleryComment>,
val hasMore: Boolean,
) : Parcelable
@@ -15,61 +15,49 @@
*/
package com.hippo.ehviewer.client.data
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANGS
import kotlinx.parcelize.Parcelize
private val LANGUAGES = arrayOf(
"English",
"Chinese",
"Spanish",
"Korean",
"Russian",
"French",
"Portuguese",
"Thai",
"German",
"Italian",
"Vietnamese",
"Polish",
"Hungarian",
"Dutch",
)
@Parcelize
class GalleryDetail(
@JvmField
val galleryInfo: GalleryInfo = BaseGalleryInfo(),
@JvmField
var apiUid: Long = -1L,
@JvmField
var apiKey: String? = null,
@JvmField
var torrentCount: Int = 0,
@JvmField
var torrentUrl: String? = null,
@JvmField
var archiveUrl: String? = null,
@JvmField
var parent: String? = null,
@JvmField
var newerVersions: ArrayList<GalleryInfo> = ArrayList(),
@JvmField
var visible: String? = null,
@JvmField
var language: String? = null,
@JvmField
var size: String? = null,
@JvmField
var favoriteCount: Int = 0,
@JvmField
var isFavorited: Boolean = false,
@JvmField
var ratingCount: Int = 0,
@JvmField
var tags: Array<GalleryTagGroup>? = null,
@JvmField
var comments: GalleryCommentList? = null,
@JvmField
var previewPages: Int = 0,
@JvmField
var previewSet: PreviewSet? = null,
) : AbstractGalleryInfo by galleryInfo, GalleryInfo
val tags: Array<GalleryTagGroup>,
var comments: GalleryCommentList,
val previewPages: Int,
val previewList: List<GalleryPreview>,
) : AbstractGalleryInfo by galleryInfo, GalleryInfo {
override fun generateSLang() {
val index = LANGUAGES.indexOf(language)
if (index != -1) simpleLanguage = S_LANGS[index]
}
}
@@ -78,20 +78,20 @@ interface GalleryInfo : AbstractGalleryInfo, Parcelable {
S_LANG_VI,
S_LANG_PL,
S_LANG_HU,
S_LANG_NL
S_LANG_NL,
)
val S_LANG_PATTERNS = arrayOf(
Pattern.compile(
"[(\\[]eng(?:lish)?[)\\]]|英訳",
Pattern.CASE_INSENSITIVE
), // [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
Pattern.CASE_INSENSITIVE,
), // [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
Pattern.compile(
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
Pattern.CASE_INSENSITIVE
Pattern.CASE_INSENSITIVE,
),
Pattern.compile(
"[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]|スペイン翻訳",
Pattern.CASE_INSENSITIVE
Pattern.CASE_INSENSITIVE,
),
Pattern.compile("[(\\[]korean?[)\\]]|韓国翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", Pattern.CASE_INSENSITIVE),
@@ -99,17 +99,17 @@ interface GalleryInfo : AbstractGalleryInfo, Parcelable {
Pattern.compile("[(\\[]portuguese|ポルトガル翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile(
"[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย|タイ翻訳",
Pattern.CASE_INSENSITIVE
Pattern.CASE_INSENSITIVE,
),
Pattern.compile("[(\\[]german[)\\]]|ドイツ翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]italiano?[)\\]]|イタリア翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile(
"[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]|ベトナム翻訳",
Pattern.CASE_INSENSITIVE
Pattern.CASE_INSENSITIVE,
),
Pattern.compile("[(\\[]polish[)\\]]|ポーランド翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE)
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
)
val S_LANG_TAGS = arrayOf(
"language:english",
@@ -125,7 +125,7 @@ interface GalleryInfo : AbstractGalleryInfo, Parcelable {
"language:vietnamese",
"language:polish",
"language:hungarian",
"language:dutch"
"language:dutch",
)
}
}
}
@@ -16,30 +16,24 @@
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import com.hippo.widget.LoadImageView
import kotlinx.parcelize.Parcelize
interface GalleryPreview : Parcelable {
val imageKey: String
val position: Int
}
@Parcelize
class GalleryPreview(
@JvmField
var imageKey: String? = null,
@JvmField
var imageUrl: String? = null,
@JvmField
var pageUrl: String? = null,
@JvmField
var position: Int = 0,
@JvmField
var offsetX: Int = Int.MIN_VALUE,
@JvmField
var offsetY: Int = Int.MIN_VALUE,
@JvmField
var clipWidth: Int = Int.MIN_VALUE,
@JvmField
var clipHeight: Int = Int.MIN_VALUE,
) : Parcelable {
fun load(view: LoadImageView) {
view.setClip(offsetX, offsetY, clipWidth, clipHeight)
view.load(imageKey!!, imageUrl!!)
}
}
data class LargeGalleryPreview(
override val imageKey: String,
override val position: Int,
) : GalleryPreview
@Parcelize
data class NormalGalleryPreview(
override val imageKey: String,
override val position: Int,
val offsetX: Int,
val clipWidth: Int,
val clipHeight: Int,
) : GalleryPreview
@@ -22,17 +22,5 @@ import kotlinx.parcelize.Parcelize
class GalleryTagGroup(
private val mTagList: ArrayList<String> = arrayListOf(),
@JvmField
var groupName: String? = null
) : Parcelable {
fun addTag(tag: String) {
mTagList.add(tag)
}
fun size(): Int {
return mTagList.size
}
fun getTagAt(position: Int): String {
return mTagList[position]
}
}
var groupName: String? = null,
) : Parcelable, MutableList<String> by mTagList
@@ -1,64 +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 com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.widget.LoadImageView
import com.hippo.yorozuya.collect.IntList
import kotlinx.parcelize.Parcelize
@Parcelize
class LargePreviewSet(
private val mPositionList: IntList = IntList(),
private val mImageUrlList: ArrayList<String> = ArrayList(),
private val mPageUrlList: ArrayList<String> = ArrayList()
) : PreviewSet() {
fun addItem(index: Int, imageUrl: String, pageUrl: String) {
mPositionList.add(index)
mImageUrlList.add(imageUrl)
mPageUrlList.add(pageUrl)
}
override fun size(): Int {
return mImageUrlList.size
}
override fun getPosition(index: Int): Int {
return mPositionList[index]
}
override fun getPageUrlAt(index: Int): String {
return mPageUrlList[index]
}
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
val galleryPreview = GalleryPreview()
galleryPreview.position = mPositionList[index]
galleryPreview.imageKey = EhCacheKeyFactory.getLargePreviewKey(gid, galleryPreview.position)
galleryPreview.imageUrl = mImageUrlList[index]
galleryPreview.pageUrl = mPageUrlList[index]
return galleryPreview
}
override fun load(view: LoadImageView, gid: Long, index: Int) {
view.resetClip()
view.load(
EhCacheKeyFactory.getLargePreviewKey(gid, mPositionList[index]),
mImageUrlList[index]
)
}
}
@@ -18,22 +18,22 @@ package com.hippo.ehviewer.client.data
import android.os.Parcelable
import android.text.TextUtils
import androidx.annotation.IntDef
import com.hippo.ehviewer.client.EhConfig
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.widget.AdvanceSearchTable
import com.hippo.network.UrlBuilder
import com.hippo.util.encodeUTF8
import com.hippo.yorozuya.NumberUtils
import com.hippo.yorozuya.StringUtils
import com.hippo.ehviewer.network.UrlBuilder
import com.hippo.ehviewer.ui.legacy.AdvanceSearchTable
import com.hippo.ehviewer.util.encodeUTF8
import com.hippo.ehviewer.yorozuya.NumberUtils
import com.hippo.ehviewer.yorozuya.StringUtils
import kotlinx.parcelize.Parcelize
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
@Parcelize
data class ListUrlBuilder(
@get:Mode @Mode var mode: Int = MODE_NORMAL,
@get:Mode @Mode
var mode: Int = MODE_NORMAL,
private var mPrev: String? = null,
private var mNext: String? = null,
private var mJumpTo: String? = null,
@@ -47,7 +47,6 @@ data class ListUrlBuilder(
var imagePath: String? = null,
var isUseSimilarityScan: Boolean = false,
var isOnlySearchCovers: Boolean = false,
var isShowExpunged: Boolean = false,
) : Parcelable {
fun reset() {
@@ -64,7 +63,6 @@ data class ListUrlBuilder(
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
isShowExpunged = false
mSHash = null
}
@@ -94,7 +92,6 @@ data class ListUrlBuilder(
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
isShowExpunged = false
}
fun toQuickSearch(): QuickSearch {
@@ -130,7 +127,9 @@ data class ListUrlBuilder(
}
return if (q.pageFrom != pageFrom) {
false
} else q.pageTo == pageTo
} else {
q.pageTo == pageTo
}
}
/**
@@ -161,48 +160,48 @@ data class ListUrlBuilder(
val value = str.substring(index + 1)
when (key) {
"f_cats" -> {
val cats = NumberUtils.parseIntSafely(value, EhConfig.ALL_CATEGORY)
category = category or (cats.inv() and EhConfig.ALL_CATEGORY)
val cats = NumberUtils.parseIntSafely(value, EhUtils.ALL_CATEGORY)
category = category or (cats.inv() and EhUtils.ALL_CATEGORY)
}
"f_doujinshi" -> if ("1" == value) {
category = category or EhConfig.DOUJINSHI
category = category or EhUtils.DOUJINSHI
}
"f_manga" -> if ("1" == value) {
category = category or EhConfig.MANGA
category = category or EhUtils.MANGA
}
"f_artistcg" -> if ("1" == value) {
category = category or EhConfig.ARTIST_CG
category = category or EhUtils.ARTIST_CG
}
"f_gamecg" -> if ("1" == value) {
category = category or EhConfig.GAME_CG
category = category or EhUtils.GAME_CG
}
"f_western" -> if ("1" == value) {
category = category or EhConfig.WESTERN
category = category or EhUtils.WESTERN
}
"f_non-h" -> if ("1" == value) {
category = category or EhConfig.NON_H
category = category or EhUtils.NON_H
}
"f_imageset" -> if ("1" == value) {
category = category or EhConfig.IMAGE_SET
category = category or EhUtils.IMAGE_SET
}
"f_cosplay" -> if ("1" == value) {
category = category or EhConfig.COSPLAY
category = category or EhUtils.COSPLAY
}
"f_asianporn" -> if ("1" == value) {
category = category or EhConfig.ASIAN_PORN
category = category or EhUtils.ASIAN_PORN
}
"f_misc" -> if ("1" == value) {
category = category or EhConfig.MISC
category = category or EhUtils.MISC
}
"f_search" -> try {
@@ -281,14 +280,7 @@ data class ListUrlBuilder(
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
}
// Search key
mKeyword?.run {
val keyword = trim { it <= ' ' }
if (keyword.isNotEmpty()) {
ub.addQuery("f_search", encodeUTF8(this))
}
ub.addQuery("f_cats", category.inv() and EhUtils.ALL_CATEGORY)
}
mSHash?.let {
ub.addQuery("f_shash", it)
@@ -302,6 +294,19 @@ data class ListUrlBuilder(
mNext?.let {
ub.addQuery("next", it)
}
// Search key
// the settings of ub:UrlBuilder may be overwritten by following Advance search
StringUtils.split(mKeyword, '|')?.forEachIndexed { idx, keyword ->
val keyword = keyword.trim { it <= ' ' }
when (idx) {
0 -> keyword.takeIf { it.isNotEmpty() }?.let { ub.addQuery("f_search", encodeUTF8(it)) }
else -> keyword.indexOf(':').takeIf { it >= 0 }?.run {
val key = keyword.substring(0, this).trim { it <= ' ' }
val value = keyword.substring(this + 1).trim { it <= ' ' }
ub.addQuery(key, encodeUTF8(value))
}
}
}
// Advance search
if (advanceSearch != -1) {
ub.addQuery("advsearch", "1")
@@ -386,7 +391,7 @@ data class ListUrlBuilder(
MODE_WHATS_HOT,
MODE_IMAGE_SEARCH,
MODE_SUBSCRIPTION,
MODE_TOPLIST
MODE_TOPLIST,
)
@Retention(AnnotationRetention.SOURCE)
private annotation class Mode
@@ -399,4 +404,4 @@ data class ListUrlBuilder(
const val MODE_SUBSCRIPTION = 0x5
const val MODE_TOPLIST = 0x6
}
}
}
@@ -1,89 +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 com.hippo.widget.LoadImageView
import com.hippo.yorozuya.collect.IntList
import kotlinx.parcelize.Parcelize
@Parcelize
class NormalPreviewSet(
private var mPositionList: IntList = IntList(),
private var mImageKeyList: ArrayList<String> = ArrayList(),
private var mImageUrlList: ArrayList<String> = ArrayList(),
private var mOffsetXList: IntList = IntList(),
private var mOffsetYList: IntList = IntList(),
private var mClipWidthList: IntList = IntList(),
private var mClipHeightList: IntList = IntList(),
private var mPageUrlList: ArrayList<String> = ArrayList()
) : PreviewSet() {
private fun getImageKey(imageUrl: String): String {
val index = imageUrl.indexOf('/')
return if (index >= 0) {
imageUrl.substring(index + 1)
} else {
imageUrl
}
}
fun addItem(
position: Int, imageUrl: String, xOffset: Int, yOffset: Int, width: Int,
height: Int, pageUrl: String
) {
mPositionList.add(position)
mImageKeyList.add(getImageKey(imageUrl))
mImageUrlList.add(imageUrl)
mOffsetXList.add(xOffset)
mOffsetYList.add(yOffset)
mClipWidthList.add(width)
mClipHeightList.add(height)
mPageUrlList.add(pageUrl)
}
override fun size(): Int {
return mPositionList.size()
}
override fun getPosition(index: Int): Int {
return mPositionList[index]
}
override fun getPageUrlAt(index: Int): String {
return mPageUrlList[index]
}
override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {
val galleryPreview = GalleryPreview()
galleryPreview.position = mPositionList[index]
galleryPreview.imageKey = mImageKeyList[index]
galleryPreview.imageUrl = mImageUrlList[index]
galleryPreview.pageUrl = mPageUrlList[index]
galleryPreview.offsetX = mOffsetXList[index]
galleryPreview.offsetY = mOffsetYList[index]
galleryPreview.clipWidth = mClipWidthList[index]
galleryPreview.clipHeight = mClipHeightList[index]
return galleryPreview
}
override fun load(view: LoadImageView, gid: Long, index: Int) {
view.setClip(
mOffsetXList[index], mOffsetYList[index],
mClipWidthList[index], mClipHeightList[index]
)
view.load(mImageKeyList[index], mImageUrlList[index])
}
}
@@ -1,27 +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 com.hippo.widget.LoadImageView
abstract class PreviewSet : Parcelable {
abstract fun size(): Int
abstract fun getPosition(index: Int): Int
abstract fun getPageUrlAt(index: Int): String
abstract fun getGalleryPreview(gid: Long, index: Int): GalleryPreview
abstract fun load(view: LoadImageView, gid: Long, index: Int)
}
@@ -18,4 +18,4 @@ package com.hippo.ehviewer.client.exception
open class EhException : Exception {
constructor(detailMessage: String?) : super(detailMessage)
constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)
}
}
@@ -15,4 +15,4 @@
*/
package com.hippo.ehviewer.client.exception
class NoHAtHClientException(detailMessage: String?) : EhException(detailMessage)
class NoHAtHClientException(detailMessage: String?) : EhException(detailMessage)
@@ -18,4 +18,4 @@ package com.hippo.ehviewer.client.exception
/**
* It is an exception for get offensive tip for g.e-hentai.org
*/
class OffensiveException : EhException("OFFENSIVE")
class OffensiveException : EhException("OFFENSIVE")
@@ -24,8 +24,8 @@ class ParseException : EhException {
constructor(detailMessage: String?, body: String, cause: Throwable?) : super(
detailMessage,
cause
cause,
) {
this.body = body
}
}
}
@@ -15,4 +15,4 @@
*/
package com.hippo.ehviewer.client.exception
class PiningException : EhException("pining for the fjords")
class PiningException : EhException("pining for the fjords")
@@ -15,113 +15,71 @@
*/
package com.hippo.ehviewer.client.parser
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.exception.NoHAtHClientException
import org.jsoup.Jsoup
import java.util.function.IntFunction
import java.util.regex.Pattern
object ArchiveParser {
private val PATTERN_ARCHIVE_URL =
Pattern.compile("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
Regex("<strong>(.*)</strong>.*<a href=\"([^\"]*)\">Click Here To Start Downloading</a>")
private val PATTERN_CURRENT_FUNDS =
Pattern.compile("<p>([\\d,]+) GP \\[[^]]*] &nbsp; ([\\d,]+) Credits \\[[^]]*]</p>")
private val PATTERN_FUNDS =
Pattern.compile("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", Pattern.DOTALL)
Regex("<p>([\\d,]+) GP \\[[^]]*] &nbsp; ([\\d,]+) Credits \\[[^]]*]</p>")
private val PATTERN_HATH_FORM =
Pattern.compile("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
Regex("<form id=\"hathdl_form\" action=\"[^\"]*?or=([^=\"]*?)\" method=\"post\">")
private val PATTERN_HATH_ARCHIVE =
Pattern.compile("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
Regex("<p><a href=\"[^\"]*\" onclick=\"return do_hathdl\\('([0-9]+|org)'\\)\">([^<]+)</a></p>\\s*<p>([\\w. ]+)</p>\\s*<p>([\\w. ]+)</p>")
private val PATTERN_NEED_HATH_CLIENT =
Pattern.compile("You must have a H@H client assigned to your account to use this feature\\.")
Regex("You must have a H@H client assigned to your account to use this feature\\.")
fun parse(body: String): Result? {
var m = PATTERN_HATH_FORM.matcher(body)
if (!m.find()) {
return null
}
val paramOr = m.group(1)
fun parse(body: String): Result {
val m = PATTERN_HATH_FORM.find(body)!!
val paramOr = m.groupValues[1]
val archiveList = ArrayList<Archive>()
val d = Jsoup.parse(body)
val es = d.select("#db>div>div")
for (e in es) {
if (e.childrenSize() > 0 && !e.attr("style").contains("color:#CCCCCC")) {
try {
val res = e.selectFirst("form>input")!!.attr("value")
val name = e.selectFirst("form>div>input")!!.attr("value")
val size = e.selectFirst("p>strong")!!.text()
val cost = e.selectFirst("div>strong")!!.text().replace(",", "")
val item = Archive(res, name, size, cost, false)
archiveList.add(item)
} catch (ex: NullPointerException) {
ex.printStackTrace()
Jsoup.parse(body).select("#db>div>div").forEach { element ->
if (element.childrenSize() > 0 && !element.attr("style").contains("color:#CCCCCC")) {
runCatching {
val res = element.selectFirst("form>input")!!.attr("value")
val name = element.selectFirst("form>div>input")!!.attr("value")
val size = element.selectFirst("p>strong")!!.text()
val cost = element.selectFirst("div>strong")!!.text().replace(",", "")
Archive(res, name, size, cost, false)
}.onSuccess {
archiveList.add(it)
}.onFailure {
it.printStackTrace()
}
}
}
m = PATTERN_HATH_ARCHIVE.matcher(body)
while (m.find()) {
val res = ParserUtils.trim(m.group(1))
val name = ParserUtils.trim(m.group(2))
val size = ParserUtils.trim(m.group(3))
val cost = ParserUtils.trim(m.group(4))
PATTERN_HATH_ARCHIVE.findAll(body).forEach { matchResult ->
val (res, name, size, cost) = matchResult.groupValues.slice(1..4)
.map { ParserUtils.trim(it) }
val item = Archive(res, name, size, cost, true)
archiveList.add(item)
}
var funds: Funds? = null
m = PATTERN_CURRENT_FUNDS.matcher(body)
if (m.find()) {
val fundsGP = ParserUtils.parseInt(m.group(1), 0)
val fundsC = ParserUtils.parseInt(m.group(2), 0)
funds = Funds(fundsGP, fundsC)
val result = Result(paramOr, archiveList, null)
PATTERN_CURRENT_FUNDS.find(body)?.groupValues?.run {
val fundsGP = ParserUtils.parseInt(get(1), 0)
val fundsC = ParserUtils.parseInt(get(2), 0)
val funds = HomeParser.Funds(fundsGP, fundsC)
result.funds = funds
}
return Result(paramOr, archiveList, funds)
return result
}
@Throws(NoHAtHClientException::class)
fun parseArchiveUrl(body: String): String? {
val m = PATTERN_NEED_HATH_CLIENT.matcher(body)
if (m.find()) {
throw NoHAtHClientException("No H@H client")
}
val d = Jsoup.parse(body)
val a = d.selectFirst("#continue>a[href]")
return if (a != null) {
a.attr("href") + "?start=1"
} else null
if (PATTERN_NEED_HATH_CLIENT.containsMatchIn(body)) throw NoHAtHClientException("No H@H client")
return Jsoup.parse(body).selectFirst("#continue>a[href]")
?.let { it.attr("href") + "?start=1" }
// TODO: Check more errors
}
fun parseFunds(body: String): Funds? {
val m = PATTERN_FUNDS.matcher(body)
return if (m.find()) {
val fundsC = ParserUtils.parseInt(m.group(1), 0)
val fundsGP = ParserUtils.parseInt(m.group(2), 0) * 1000
return Funds(fundsGP, fundsC)
} else null
}
class Archive(
val res: String,
val name: String,
val size: String,
val cost: String,
val isHAtH: Boolean
) {
fun format(getString: IntFunction<String?>): String {
return if (isHAtH) {
val costStr = if (cost == "Free") getString.apply(R.string.archive_free) else cost
"[H@H] $name [$size] [$costStr]"
} else {
val nameStr =
getString.apply(if (res == "org") R.string.archive_original else R.string.archive_resample)
val costStr = if (cost == "Free!") getString.apply(R.string.archive_free) else cost
"$nameStr [$size] [$costStr]"
}
}
}
val isHAtH: Boolean,
)
class Funds(var fundsGP: Int, var fundsC: Int)
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: Funds?)
}
class Result(val paramOr: String?, val archiveList: List<Archive>, var funds: HomeParser.Funds?)
}
@@ -15,23 +15,12 @@
* 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.client.parser
package com.hippo.ehviewer.ui.fragment;
import org.jsoup.Jsoup
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.R;
public class SettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.settings_headers);
}
@Override
public int getFragmentTitle() {
return R.string.settings;
object EventPaneParser {
fun parse(body: String): String? {
return Jsoup.parse(body).getElementById("eventpane")?.html()
}
}
@@ -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.client.parser;
import com.hippo.ehviewer.GetText;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.exception.EhException;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.JsoupUtils;
import com.hippo.yorozuya.AssertUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.List;
public class FavoritesParser {
public static Result parse(String body) throws Exception {
if (body.contains("This page requires you to log on.</p>")) {
throw new EhException(GetText.getString(R.string.need_sign_in));
}
String[] catArray = new String[10];
int[] countArray = new int[10];
try {
Document d = Jsoup.parse(body);
Element ido = JsoupUtils.getElementByClass(d, "ido");
//noinspection ConstantConditions
Elements fps = ido.getElementsByClass("fp");
// Last one is "fp fps"
AssertUtils.assertEquals(11, fps.size());
for (int i = 0; i < 10; i++) {
Element fp = fps.get(i);
countArray[i] = ParserUtils.parseInt(fp.child(0).text(), 0);
catArray[i] = ParserUtils.trim(fp.child(2).text());
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
throw new ParseException("Parse favorites error", body);
}
GalleryListParser.Result result = GalleryListParser.parse(body);
Result re = new Result();
re.catArray = catArray;
re.countArray = countArray;
re.prev = result.prev;
re.next = result.next;
re.galleryInfoList = result.galleryInfoList;
return re;
}
public static class Result {
public String[] catArray; // Size 10
public int[] countArray; // Size 10
public String prev;
public String next;
public List<GalleryInfo> galleryInfoList;
}
}
@@ -0,0 +1,62 @@
/*
* 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.GetText.getString
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.JsoupUtils
import org.jsoup.Jsoup
object FavoritesParser {
fun parse(body: String): Result {
if (body.contains("This page requires you to log on.</p>")) {
throw EhException(getString(R.string.need_sign_in))
}
val catArray = arrayOfNulls<String>(10)
val countArray = IntArray(10)
val d = Jsoup.parse(body)
runCatching {
val ido = JsoupUtils.getElementByClass(d, "ido")
val fps = ido!!.getElementsByClass("fp")
// Last one is "fp fps"
check(fps.size == 11)
for (i in 0..9) {
val fp = fps[i]
countArray[i] = ParserUtils.parseInt(fp.child(0).text(), 0)
catArray[i] = ParserUtils.trim(fp.child(2).text())
}
}.onFailure {
ExceptionUtils.throwIfFatal(it)
it.printStackTrace()
throw ParseException("Parse favorites error", body)
}
val result = GalleryListParser.parse(d, body)
return Result(catArray, countArray, result)
}
class Result(
val catArray: Array<String?>,
val countArray: IntArray,
galleryListResult: GalleryListParser.Result,
) {
val prev = galleryListResult.prev
val next = galleryListResult.next
val galleryInfoList = galleryListResult.galleryInfoList
}
}
@@ -1,40 +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.EhUrl;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.util.ExceptionUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
public class ForumsParser {
public static String parse(String body) throws ParseException {
try {
Document d = Jsoup.parse(body, EhUrl.URL_FORUMS);
Element userlinks = d.getElementById("userlinks");
Element child = userlinks.child(0).child(0).child(0);
return child.attr("href");
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
throw new ParseException("Parse forums error", body);
}
}
}
@@ -0,0 +1,35 @@
/*
* 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.EhUrl
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.util.ExceptionUtils
import org.jsoup.Jsoup
object ForumsParser {
fun parse(body: String): String {
return runCatching {
val d = Jsoup.parse(body, EhUrl.URL_FORUMS)
val userlinks = d.getElementById("userlinks")
val child = userlinks!!.child(0).child(0).child(0)
child.attr("href")
}.getOrElse {
ExceptionUtils.throwIfFatal(it)
throw EhException("Not logged in")
}
}
}
@@ -1,71 +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.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.yorozuya.NumberUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
public class GalleryApiParser {
public static void parse(String body, List<GalleryInfo> galleryInfoList) throws JSONException {
JSONObject jo = new JSONObject(body);
JSONArray ja = jo.getJSONArray("gmetadata");
for (int i = 0, length = ja.length(); i < length; i++) {
JSONObject g = ja.getJSONObject(i);
long gid = g.getLong("gid");
GalleryInfo gi = getGalleryInfoByGid(galleryInfoList, gid);
if (gi == null) {
continue;
}
gi.setTitle(ParserUtils.trim(g.getString("title")));
gi.setTitleJpn(ParserUtils.trim(g.getString("title_jpn")));
gi.setCategory(EhUtils.getCategory(g.getString("category")));
gi.setThumb(EhUtils.handleThumbUrlResolution(g.getString("thumb")));
gi.setUploader(g.getString("uploader"));
gi.setPosted(ParserUtils.formatDate(ParserUtils.parseLong(g.getString("posted"), 0) * 1000));
gi.setRating(NumberUtils.parseFloatSafely(g.getString("rating"), 0.0f));
// tags
JSONArray tagJa = g.getJSONArray("tags");
int tagLength = tagJa.length();
String[] tags = new String[tagLength];
for (int j = 0; j < tagLength; j++) {
tags[j] = tagJa.getString(j);
}
gi.setSimpleTags(tags);
gi.setPages(NumberUtils.parseIntSafely(g.getString("filecount"), 0));
gi.generateSLang();
}
}
private static GalleryInfo getGalleryInfoByGid(List<GalleryInfo> galleryInfoList, long gid) {
for (int i = 0, size = galleryInfoList.size(); i < size; i++) {
GalleryInfo gi = galleryInfoList.get(i);
if (gi.getGid() == gid) {
return gi;
}
}
return null;
}
}
@@ -0,0 +1,48 @@
/*
* 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.EhUtils.getCategory
import com.hippo.ehviewer.client.EhUtils.handleThumbUrlResolution
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.yorozuya.NumberUtils
import org.json.JSONObject
object GalleryApiParser {
fun parse(body: String, galleryInfoList: List<GalleryInfo>) {
val jo = JSONObject(body)
val ja = jo.getJSONArray("gmetadata")
for (i in 0 until ja.length()) {
val g = ja.getJSONObject(i)
val gid = g.getLong("gid")
val gi = galleryInfoList.find { it.gid == gid } ?: continue
gi.title = ParserUtils.trim(g.getString("title"))
gi.titleJpn = ParserUtils.trim(g.getString("title_jpn"))
gi.category = getCategory(g.getString("category"))
gi.thumbKey = getThumbKey(handleThumbUrlResolution(g.getString("thumb"))!!)
gi.uploader = g.getString("uploader")
gi.posted =
ParserUtils.formatDate(ParserUtils.parseLong(g.getString("posted"), 0) * 1000)
gi.rating = NumberUtils.parseFloatSafely(g.getString("rating"), 0.0f)
// tags
val tagJa = g.getJSONArray("tags")
gi.simpleTags = (0 until tagJa.length()).map { tagJa.getString(it) }.toTypedArray()
gi.pages = NumberUtils.parseIntSafely(g.getString("filecount"), 0)
gi.generateSLang()
}
}
}
@@ -1,695 +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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.EhFilter;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.BaseGalleryInfo;
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.GalleryInfo;
import com.hippo.ehviewer.client.data.GalleryTagGroup;
import com.hippo.ehviewer.client.data.LargePreviewSet;
import com.hippo.ehviewer.client.data.NormalPreviewSet;
import com.hippo.ehviewer.client.data.PreviewSet;
import com.hippo.ehviewer.client.exception.EhException;
import com.hippo.ehviewer.client.exception.OffensiveException;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.ehviewer.client.exception.PiningException;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.JsoupUtils;
import com.hippo.util.MutableBoolean;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.select.Elements;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GalleryDetailParser {
private static final Pattern PATTERN_ERROR = Pattern.compile("<div class=\"d\">\n<p>([^<]+)</p>");
private static final Pattern PATTERN_DETAIL = Pattern.compile("var gid = (\\d+);.+?var token = \"([a-f0-9]+)\";.+?var apiuid = ([\\-\\d]+);.+?var apikey = \"([a-f0-9]+)\";", Pattern.DOTALL);
private static final Pattern PATTERN_TORRENT = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a");
private static final Pattern PATTERN_ARCHIVE = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>");
private static final Pattern PATTERN_COVER = Pattern.compile("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)");
private static final Pattern PATTERN_PAGES = Pattern.compile("<tr><td[^<>]*>Length:</td><td[^<>]*>([\\d,]+) pages</td></tr>");
private static final Pattern PATTERN_PREVIEW_PAGES = Pattern.compile("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>");
private static final Pattern PATTERN_NORMAL_PREVIEW = Pattern.compile("<div class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*><img alt=\"([\\d,]+)\"");
private static final Pattern PATTERN_LARGE_PREVIEW = Pattern.compile("<div class=\"gdtl\".+?<a href=\"(.+?)\"><img alt=\"([\\d,]+)\".+?src=\"(.+?)\"");
private static final Pattern PATTERN_NEWER_DATE = Pattern.compile(", added (.+?)<br />");
private static final Pattern PATTERN_FAVORITE_SLOT = Pattern.compile("/fav.png\\); background-position:0px -(\\d+)px");
private static final GalleryTagGroup[] EMPTY_GALLERY_TAG_GROUP_ARRAY = new GalleryTagGroup[0];
private static final GalleryCommentList EMPTY_GALLERY_COMMENT_ARRAY = new GalleryCommentList(new GalleryComment[0], false);
private static final DateTimeFormatter WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC);
private static final String OFFENSIVE_STRING =
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>";
private static final String PINING_STRING =
"<p>This gallery is pining for the fjords.</p>";
private static final String[] LANGUAGES = {
"English",
"Chinese",
"Spanish",
"Korean",
"Russian",
"French",
"Portuguese",
"Thai",
"German",
"Italian",
"Vietnamese",
"Polish",
"Hungarian",
"Dutch",
};
public static GalleryDetail parse(String body) throws EhException {
if (body.contains(OFFENSIVE_STRING)) {
throw new OffensiveException();
}
if (body.contains(PINING_STRING)) {
throw new PiningException();
}
// Error info
Matcher m = PATTERN_ERROR.matcher(body);
if (m.find()) {
throw new EhException(m.group(1));
}
GalleryDetail galleryDetail = new GalleryDetail();
Document document = Jsoup.parse(body);
parseDetail(galleryDetail, document, body);
galleryDetail.tags = parseTagGroups(document);
galleryDetail.comments = parseComments(document);
galleryDetail.previewPages = parsePreviewPages(document, body);
galleryDetail.previewSet = parsePreviewSet(document, body);
// Generate simpleLanguage for local favorites
for (int i = 0; i < LANGUAGES.length; i++) {
if (galleryDetail.language.equals(LANGUAGES[i])) {
galleryDetail.setSimpleLanguage(GalleryInfo.Companion.getS_LANGS()[i]);
break;
}
}
return galleryDetail;
}
@SuppressWarnings("ConstantConditions")
private static void parseDetail(GalleryDetail gd, Document d, String body) throws ParseException {
Matcher matcher = PATTERN_DETAIL.matcher(body);
if (matcher.find()) {
gd.setGid(NumberUtils.parseLongSafely(matcher.group(1), -1L));
gd.setToken(matcher.group(2));
gd.apiUid = NumberUtils.parseLongSafely(matcher.group(3), -1L);
gd.apiKey = matcher.group(4);
} else {
throw new ParseException("Can't parse gallery detail", body);
}
if (gd.getGid() == -1L) {
throw new ParseException("Can't parse gallery detail", body);
}
matcher = PATTERN_TORRENT.matcher(body);
if (matcher.find()) {
gd.torrentUrl = StringUtils.unescapeXml(StringUtils.trim(matcher.group(1)));
gd.torrentCount = NumberUtils.parseIntSafely(matcher.group(2), 0);
} else {
gd.torrentCount = 0;
gd.torrentUrl = "";
}
matcher = PATTERN_ARCHIVE.matcher(body);
if (matcher.find()) {
gd.archiveUrl = StringUtils.unescapeXml(StringUtils.trim(matcher.group(1)));
} else {
gd.archiveUrl = "";
}
try {
Element gm = JsoupUtils.getElementByClass(d, "gm");
// Thumb url
Element gd1 = gm.getElementById("gd1");
try {
gd.setThumb(parseCoverStyle(StringUtils.trim(gd1.child(0).attr("style"))));
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
gd.setThumb("");
}
// Title
Element gn = gm.getElementById("gn");
if (null != gn) {
gd.setTitle(StringUtils.trim(gn.text()));
} else {
gd.setTitle("");
}
// Jpn title
Element gj = gm.getElementById("gj");
if (null != gj) {
gd.setTitleJpn(StringUtils.trim(gj.text()));
} else {
gd.setTitleJpn("");
}
// Category
Element gdc = gm.getElementById("gdc");
try {
Element ce = JsoupUtils.getElementByClass(gdc, "cn");
if (ce == null) {
ce = JsoupUtils.getElementByClass(gdc, "cs");
}
gd.setCategory(EhUtils.getCategory(ce.text()));
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
gd.setCategory(EhUtils.UNKNOWN);
}
// Uploader
Element gdn = gm.getElementById("gdn");
if (null != gdn) {
gd.setDisowned(gdn.attr("style").contains("opacity:0.5"));
gd.setUploader(StringUtils.trim(gdn.text()));
} else {
gd.setUploader("");
}
Element gdd = gm.getElementById("gdd");
gd.setPosted("");
gd.parent = "";
gd.visible = "";
gd.size = "";
gd.setPages(0);
gd.favoriteCount = 0;
Elements es = gdd.child(0).child(0).children();
for (int i = 0, n = es.size(); i < n; i++) {
parseDetailInfo(gd, es.get(i));
}
// Rating count
Element rating_count = gm.getElementById("rating_count");
if (null != rating_count) {
gd.ratingCount = NumberUtils.parseIntSafely(
StringUtils.trim(rating_count.text()), 0);
} else {
gd.ratingCount = 0;
}
// Rating
Element rating_label = gm.getElementById("rating_label");
if (null != rating_label) {
String ratingStr = StringUtils.trim(rating_label.text());
if ("Not Yet Rated".equals(ratingStr)) {
gd.setRating(-1.0f);
} else {
int index = ratingStr.indexOf(' ');
if (index == -1 || index >= ratingStr.length()) {
gd.setRating(0f);
} else {
gd.setRating(NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f));
}
}
} else {
gd.setRating(-1.0f);
}
// isFavorited
Element gdf = gm.getElementById("gdf");
gd.isFavorited = false;
if (gdf != null) {
final String favoriteName = StringUtils.trim(gdf.text());
if (favoriteName.equals("Add to Favorites")) {
gd.setFavoriteName(null);
} else {
gd.isFavorited = true;
gd.setFavoriteName(StringUtils.trim(gdf.text()));
matcher = PATTERN_FAVORITE_SLOT.matcher(body);
if (matcher.find()) {
gd.setFavoriteSlot((NumberUtils.parseIntSafely(matcher.group(1), 2) - 2) / 19);
}
}
}
if (gd.getFavoriteSlot() == -2 && EhDB.containLocalFavorites(gd.getGid())) {
gd.setFavoriteSlot(-1);
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
throw new ParseException("Can't parse gallery detail", body);
}
// newer version
try {
Element gnd = d.getElementById("gnd");
if (gnd != null) {
matcher = PATTERN_NEWER_DATE.matcher(body);
ArrayList<String> dates = new ArrayList<>();
while (matcher.find()) {
dates.add(matcher.group(1));
}
Elements elements = gnd.select("a");
for (int i = 0; i < elements.size(); i++) {
Element element = elements.get(i);
GalleryInfo gi = new BaseGalleryInfo();
GalleryDetailUrlParser.Result result = GalleryDetailUrlParser.parse(element.attr("href"));
if (result != null) {
gi.setGid(result.gid);
gi.setToken(result.token);
gi.setTitle(StringUtils.trim(element.text()));
gi.setPosted(dates.get(i));
gd.newerVersions.add(gi);
}
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
// width:250px; height:356px; background:transparent url(https://exhentai.org/t/fe/1f/fe1fcfa9bf8fba2f03982eda0aa347cc9d6a6372-145921-1050-1492-jpg_250.jpg) 0 0 no-repeat
private static String parseCoverStyle(String str) {
Matcher matcher = PATTERN_COVER.matcher(str);
if (matcher.find()) {
return EhUtils.handleThumbUrlResolution(matcher.group(3));
} else {
return "";
}
}
private static void parseDetailInfo(GalleryDetail gd, Element e) {
Elements es = e.children();
if (es.size() < 2) {
return;
}
String key = StringUtils.trim(es.get(0).text());
String value = StringUtils.trim(es.get(1).ownText());
if (key.startsWith("Posted")) {
gd.setPosted(value);
} else if (key.startsWith("Parent")) {
Element a = es.get(1).children().first();
if (a != null) {
gd.parent = a.attr("href");
}
} else if (key.startsWith("Visible")) {
gd.visible = value;
} else if (key.startsWith("Language")) {
gd.language = value;
} else if (key.startsWith("File Size")) {
gd.size = value;
} else if (key.startsWith("Length")) {
int index = value.indexOf(' ');
if (index >= 0) {
gd.setPages(NumberUtils.parseIntSafely(value.substring(0, index), 1));
} else {
gd.setPages(1);
}
} else if (key.startsWith("Favorited")) {
switch (value) {
case "Never" -> gd.favoriteCount = 0;
case "Once" -> gd.favoriteCount = 1;
default -> {
int index = value.indexOf(' ');
if (index == -1) {
gd.favoriteCount = 0;
} else {
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0);
}
}
}
}
}
@Nullable
private static GalleryTagGroup parseTagGroup(Element element) {
try {
GalleryTagGroup group = new GalleryTagGroup();
String nameSpace = element.child(0).text();
// Remove last ':'
nameSpace = nameSpace.substring(0, nameSpace.length() - 1);
group.groupName = nameSpace;
Elements tags = element.child(1).children();
for (int i = 0, n = tags.size(); i < n; i++) {
String tag = tags.get(i).text();
// Sometimes parody tag is followed with '|' and english translate, just remove them
int index = tag.indexOf('|');
if (index >= 0) {
tag = tag.substring(0, index).trim();
}
group.addTag(tag);
}
return group.size() > 0 ? group : null;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return null;
}
}
/**
* Parse tag groups with html parser
*/
@NonNull
public static GalleryTagGroup[] parseTagGroups(Document document) {
try {
Element taglist = document.getElementById("taglist");
assert taglist != null;
Elements tagGroups = taglist.child(0).child(0).children();
return parseTagGroups(tagGroups);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return EMPTY_GALLERY_TAG_GROUP_ARRAY;
}
}
@NonNull
public static GalleryTagGroup[] parseTagGroups(Elements trs) {
try {
List<GalleryTagGroup> list = new ArrayList<>(trs.size());
for (int i = 0, n = trs.size(); i < n; i++) {
GalleryTagGroup group = parseTagGroup(trs.get(i));
if (null != group) {
list.add(group);
}
}
return list.toArray(new GalleryTagGroup[0]);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return EMPTY_GALLERY_TAG_GROUP_ARRAY;
}
}
@Nullable
@SuppressWarnings("ConstantConditions")
public static GalleryComment parseComment(Element element) {
try {
GalleryComment comment = new GalleryComment();
// Id
Element a = element.previousElementSibling();
String name = a.attr("name");
comment.id = Integer.parseInt(StringUtils.trim(name).substring(1));
// Editable, vote up and vote down
Element c4 = JsoupUtils.getElementByClass(element, "c4");
if (null != c4) {
if ("Uploader Comment".equals(c4.text())) {
comment.uploader = true;
}
for (Element e : c4.children()) {
switch (e.text()) {
case "Vote+" -> {
comment.voteUpAble = true;
comment.voteUpEd = !StringUtils.trim(e.attr("style")).isEmpty();
}
case "Vote-" -> {
comment.voteDownAble = true;
comment.voteDownEd = !StringUtils.trim(e.attr("style")).isEmpty();
}
case "Edit" -> comment.editable = true;
}
}
}
// Vote state
Element c7 = JsoupUtils.getElementByClass(element, "c7");
if (null != c7) {
comment.voteState = StringUtils.trim(c7.text());
}
// Score
Element c5 = JsoupUtils.getElementByClass(element, "c5");
if (null != c5) {
Elements es = c5.children();
if (!es.isEmpty()) {
comment.score = NumberUtils.parseIntSafely(StringUtils.trim(es.get(0).text()), 0);
}
}
// time
Element c3 = JsoupUtils.getElementByClass(element, "c3");
String temp = c3.ownText();
temp = temp.substring("Posted on ".length(), temp.length() - " by:".length());
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
// user
comment.user = c3.child(0).text();
// comment
comment.comment = JsoupUtils.getElementByClass(element, "c6").html();
// filter comment
if (!comment.uploader) {
EhFilter sEhFilter = EhFilter.INSTANCE;
if (!sEhFilter.filterCommenter(comment.user) || !sEhFilter.filterComment(comment.comment)) {
return null;
}
}
// last edited
Element c8 = JsoupUtils.getElementByClass(element, "c8");
if (c8 != null) {
Element e = c8.children().first();
if (e != null) {
comment.lastEdited = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
}
}
return comment;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return null;
}
}
/**
* Parse comments with html parser
*/
@NonNull
public static GalleryCommentList parseComments(Document document) {
try {
Element cdiv = document.getElementById("cdiv");
assert cdiv != null;
Elements c1s = cdiv.getElementsByClass("c1");
List<GalleryComment> list = new ArrayList<>(c1s.size());
for (int i = 0, n = c1s.size(); i < n; i++) {
GalleryComment comment = parseComment(c1s.get(i));
if (null != comment) {
list.add(comment);
}
}
Element chd = cdiv.getElementById("chd");
MutableBoolean hasMore = new MutableBoolean(false);
assert chd != null;
NodeTraversor.traverse(new NodeVisitor() {
@Override
public void head(@NonNull Node node, int depth) {
if (node instanceof Element && ((Element) node).text().equals("click to show all")) {
hasMore.value = true;
}
}
@Override
public void tail(@NonNull Node node, int depth) {
}
}, chd);
return new GalleryCommentList(list.toArray(new GalleryComment[0]), hasMore.value);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
return EMPTY_GALLERY_COMMENT_ARRAY;
}
}
/**
* Parse preview pages with html parser
*/
public static int parsePreviewPages(Document document, String body) throws ParseException {
try {
Element ptt = document.getElementsByClass("ptt").first();
assert ptt != null;
Elements elements = ptt.child(0).child(0).children();
return Integer.parseInt(elements.get(elements.size() - 2).text());
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
throw new ParseException("Can't parse preview pages", body);
}
}
/**
* Parse preview pages with regular expressions
*/
public static int parsePreviewPages(String body) throws ParseException {
Matcher m = PATTERN_PREVIEW_PAGES.matcher(body);
int previewPages = -1;
if (m.find()) {
previewPages = ParserUtils.parseInt(m.group(1), -1);
}
if (previewPages <= 0) {
throw new ParseException("Parse preview page count error", body);
}
return previewPages;
}
/**
* Parse pages with regular expressions
*/
public static int parsePages(String body) throws ParseException {
int pages = -1;
Matcher m = PATTERN_PAGES.matcher(body);
if (m.find()) {
pages = ParserUtils.parseInt(m.group(1), -1);
}
if (pages < 0) {
throw new ParseException("Parse pages error", body);
}
return pages;
}
public static PreviewSet parsePreviewSet(Document d, String body) throws ParseException {
try {
return parseLargePreviewSet(d, body);
} catch (ParseException e) {
return parseNormalPreviewSet(body);
}
}
public static PreviewSet parsePreviewSet(String body) throws ParseException {
try {
return parseLargePreviewSet(body);
} catch (ParseException e) {
return parseNormalPreviewSet(body);
}
}
/**
* Parse large previews with regular expressions
*/
private static LargePreviewSet parseLargePreviewSet(Document d, String body) throws ParseException {
try {
LargePreviewSet largePreviewSet = new LargePreviewSet();
Element gdt = d.getElementById("gdt");
assert gdt != null;
Elements gdtls = gdt.getElementsByClass("gdtl");
int n = gdtls.size();
if (n == 0) {
throw new ParseException("Can't parse large preview", body);
}
for (int i = 0; i < n; i++) {
Element element = gdtls.get(i).child(0);
String pageUrl = element.attr("href");
element = element.child(0);
String imageUrl = element.attr("src");
int index = Integer.parseInt(element.attr("alt")) - 1;
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
return largePreviewSet;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
throw new ParseException("Can't parse large preview", body);
}
}
/**
* Parse large previews with regular expressions
*/
private static LargePreviewSet parseLargePreviewSet(String body) throws ParseException {
Matcher m = PATTERN_LARGE_PREVIEW.matcher(body);
LargePreviewSet largePreviewSet = new LargePreviewSet();
while (m.find()) {
int index = ParserUtils.parseInt(m.group(2), 0) - 1;
if (index < 0) {
continue;
}
String imageUrl = ParserUtils.trim(m.group(3));
String pageUrl = ParserUtils.trim(m.group(1));
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
if (largePreviewSet.size() == 0) {
throw new ParseException("Can't parse large preview", body);
}
return largePreviewSet;
}
/**
* Parse normal previews with regular expressions
*/
private static NormalPreviewSet parseNormalPreviewSet(String body) throws ParseException {
Matcher m = PATTERN_NORMAL_PREVIEW.matcher(body);
NormalPreviewSet normalPreviewSet = new NormalPreviewSet();
while (m.find()) {
int position = ParserUtils.parseInt(m.group(6), 0) - 1;
if (position < 0) {
continue;
}
String imageUrl = ParserUtils.trim(m.group(3));
int xOffset = ParserUtils.parseInt(m.group(4), 0);
int yOffset = 0;
int width = ParserUtils.parseInt(m.group(1), 0);
if (width <= 0) {
continue;
}
int height = ParserUtils.parseInt(m.group(2), 0);
if (height <= 0) {
continue;
}
String pageUrl = ParserUtils.trim(m.group(5));
normalPreviewSet.addItem(position, imageUrl, xOffset, yOffset, width, height, pageUrl);
}
if (normalPreviewSet.size() == 0) {
throw new ParseException("Can't parse normal preview", body);
}
return normalPreviewSet;
}
}
@@ -0,0 +1,508 @@
/*
* 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 arrow.core.unzip
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.EhUtils.getCategory
import com.hippo.ehviewer.client.EhUtils.handleThumbUrlResolution
import com.hippo.ehviewer.client.data.BaseGalleryInfo
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.GalleryPreview
import com.hippo.ehviewer.client.data.GalleryTagGroup
import com.hippo.ehviewer.client.data.LargeGalleryPreview
import com.hippo.ehviewer.client.data.NormalGalleryPreview
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.client.exception.OffensiveException
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.exception.PiningException
import com.hippo.ehviewer.client.getThumbKey
import com.hippo.ehviewer.util.ExceptionUtils
import com.hippo.ehviewer.util.JsoupUtils
import com.hippo.ehviewer.yorozuya.NumberUtils
import com.hippo.ehviewer.yorozuya.StringUtils
import com.hippo.ehviewer.yorozuya.trimAnd
import com.hippo.ehviewer.yorozuya.unescapeXml
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.select.Elements
import org.jsoup.select.NodeTraversor
import org.jsoup.select.NodeVisitor
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
object GalleryDetailParser {
private val PATTERN_ERROR = Regex("<div class=\"d\">\n<p>([^<]+)</p>")
private val PATTERN_DETAIL = Regex(
"var gid = (\\d+);.+?var token = \"([a-f0-9]+)\";.+?var apiuid = ([\\-\\d]+);.+?var apikey = \"([a-f0-9]+)\";",
RegexOption.DOT_MATCHES_ALL,
)
private val PATTERN_TORRENT =
Regex("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a")
private val PATTERN_ARCHIVE =
Regex("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>")
private val PATTERN_COVER = Regex("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)")
private val PATTERN_PAGES =
Regex("<tr><td[^<>]*>Length:</td><td[^<>]*>([\\d,]+) pages</td></tr>")
private val PATTERN_PREVIEW_PAGES =
Regex("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>")
private val PATTERN_NORMAL_PREVIEW =
Regex("<div class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*><img alt=\"([\\d,]+)\"")
private val PATTERN_LARGE_PREVIEW =
Regex("<div class=\"gdtl\".+?<a href=\"(.+?)\"><img alt=\"([\\d,]+)\".+?src=\"(.+?)\"")
private val PATTERN_NEWER_DATE = Regex(", added (.+?)<br />")
private val PATTERN_FAVORITE_SLOT =
Regex("/fav.png\\); background-position:0px -(\\d+)px")
private val EMPTY_GALLERY_TAG_GROUP_ARRAY = arrayOf<GalleryTagGroup>()
private val EMPTY_GALLERY_COMMENT_ARRAY = GalleryCommentList(arrayOf(), false)
private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC)
private const val OFFENSIVE_STRING =
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>"
private const val PINING_STRING = "<p>This gallery is pining for the fjords.</p>"
@Throws(EhException::class)
fun parse(body: String): GalleryDetail {
if (body.contains(OFFENSIVE_STRING)) {
throw OffensiveException()
}
if (body.contains(PINING_STRING)) {
throw PiningException()
}
// Error info
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
// Temporary workaround, see https://github.com/jhy/jsoup/issues/1850
val document = Jsoup.parse(body.replace("del>", "s>"))
val galleryDetail = GalleryDetail(
tags = parseTagGroups(document),
comments = parseComments(document),
previewPages = parsePreviewPages(body),
previewList = parsePreviewList(body).first,
)
parseDetail(galleryDetail, document, body)
// Generate simpleLanguage for local favorites
galleryDetail.generateSLang()
return galleryDetail
}
@Throws(ParseException::class)
private fun parseDetail(gd: GalleryDetail, d: Document, body: String) {
PATTERN_DETAIL.find(body)?.apply {
gd.gid = groupValues[1].toLongOrNull() ?: -1L
gd.token = groupValues[2]
gd.apiUid = groupValues[3].toLongOrNull() ?: -1L
gd.apiKey = groupValues[4]
} ?: throw ParseException("Can't parse gallery detail", body)
if (gd.gid == -1L) {
throw ParseException("Can't parse gallery detail", body)
}
PATTERN_TORRENT.find(body)?.run {
gd.torrentUrl = groupValues[1].trim().unescapeXml()
gd.torrentCount = groupValues[2].toIntOrNull() ?: 0
}
PATTERN_ARCHIVE.find(body)?.run {
gd.archiveUrl = groupValues[1].trim().unescapeXml()
}
try {
val gm = d.getElementsByClass("gm")[0]
// Thumb url
gm.getElementById("gd1")?.child(0)?.attr("style")?.trim()?.let {
gd.thumbKey = getThumbKey(
PATTERN_COVER.find(it)?.run {
handleThumbUrlResolution(groupValues[3])
}!!,
)
}
gd.title = gm.getElementById("gn")?.text()?.trim()
gd.titleJpn = gm.getElementById("gj")?.text()?.trim()
// Category
val gdc = gm.getElementById("gdc")
try {
var ce = JsoupUtils.getElementByClass(gdc, "cn")
if (ce == null) {
ce = JsoupUtils.getElementByClass(gdc, "cs")
}
gd.category = getCategory(ce!!.text())
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
gd.category = EhUtils.UNKNOWN
}
// Uploader
val gdn = gm.getElementById("gdn")
if (null != gdn) {
gd.disowned = gdn.attr("style").contains("opacity:0.5")
gd.uploader = StringUtils.trim(gdn.text())
} else {
gd.uploader = ""
}
val gdd = gm.getElementById("gdd")
gd.posted = ""
gd.parent = ""
gd.visible = ""
gd.size = ""
gd.pages = 0
gd.favoriteCount = 0
val es = gdd!!.child(0).child(0).children()
es.forEach {
parseDetailInfo(gd, it)
}
// Rating count
val ratingCount = gm.getElementById("rating_count")
if (null != ratingCount) {
gd.ratingCount = NumberUtils.parseIntSafely(
StringUtils.trim(ratingCount.text()),
0,
)
} else {
gd.ratingCount = 0
}
// Rating
val ratingLabel = gm.getElementById("rating_label")
if (null != ratingLabel) {
val ratingStr = StringUtils.trim(ratingLabel.text())
if ("Not Yet Rated" == ratingStr) {
gd.rating = -1.0f
} else {
val index = ratingStr.indexOf(' ')
if (index == -1 || index >= ratingStr.length) {
gd.rating = 0f
} else {
gd.rating = NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f)
}
}
} else {
gd.rating = -1.0f
}
// isFavorited
val gdf = gm.getElementById("gdf")
gd.isFavorited = false
if (gdf != null) {
val favoriteName = StringUtils.trim(gdf.text())
if (favoriteName == "Add to Favorites") {
gd.favoriteName = null
} else {
gd.isFavorited = true
gd.favoriteName = StringUtils.trim(gdf.text())
PATTERN_FAVORITE_SLOT.find(body)?.run {
gd.favoriteSlot = ((groupValues[1].toIntOrNull() ?: 2) - 2) / 19
}
}
}
if (gd.favoriteSlot == -2 && EhDB.containLocalFavorites(gd.gid)) {
gd.favoriteSlot = -1
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
throw ParseException("Can't parse gallery detail", body)
}
// newer version
d.getElementById("gnd")?.run {
val dates = PATTERN_NEWER_DATE.findAll(body).map { it.groupValues[1] }.toList()
select("a").forEachIndexed { index, element ->
val gi = BaseGalleryInfo()
val result = GalleryDetailUrlParser.parse(element.attr("href"))
if (result != null) {
gi.gid = result.gid
gi.token = result.token
gi.title = StringUtils.trim(element.text())
gi.posted = dates[index]
gd.newerVersions.add(gi)
}
}
}
}
private fun parseDetailInfo(gd: GalleryDetail, e: Element) {
val es = e.children()
if (es.size < 2) {
return
}
val key = StringUtils.trim(es[0].text())
val value = StringUtils.trim(es[1].ownText())
if (key.startsWith("Posted")) {
gd.posted = value
} else if (key.startsWith("Parent")) {
val a = es[1].children().first()
if (a != null) {
gd.parent = a.attr("href")
}
} else if (key.startsWith("Visible")) {
gd.visible = value
} else if (key.startsWith("Language")) {
gd.language = value
} else if (key.startsWith("File Size")) {
gd.size = value
} else if (key.startsWith("Length")) {
val index = value.indexOf(' ')
if (index >= 0) {
gd.pages = NumberUtils.parseIntSafely(value.substring(0, index), 1)
} else {
gd.pages = 1
}
} else if (key.startsWith("Favorited")) {
when (value) {
"Never" -> gd.favoriteCount = 0
"Once" -> gd.favoriteCount = 1
else -> {
val index = value.indexOf(' ')
if (index == -1) {
gd.favoriteCount = 0
} else {
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0)
}
}
}
}
}
private fun parseTagGroup(element: Element): GalleryTagGroup? {
return try {
val group = GalleryTagGroup()
var nameSpace = element.child(0).text()
// Remove last ':'
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
group.groupName = nameSpace
val tags = element.child(1).children()
tags.forEach {
var tag = it.text()
// Sometimes parody tag is followed with '|' and english translate, just remove them
val index = tag.indexOf('|')
if (index >= 0) {
tag = tag.substring(0, index).trim()
}
if (it.className() == "gtw") {
tag = "_$tag" // weak tag
}
group.add(tag)
}
if (group.size > 0) group else null
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
null
}
}
/**
* Parse tag groups with html parser
*/
private fun parseTagGroups(document: Document): Array<GalleryTagGroup> {
return try {
val taglist = document.getElementById("taglist")!!
val tagGroups = taglist.child(0).child(0).children()
parseTagGroups(tagGroups)
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
EMPTY_GALLERY_TAG_GROUP_ARRAY
}
}
private fun parseTagGroups(trs: Elements): Array<GalleryTagGroup> {
return try {
trs.mapNotNull { parseTagGroup(it) }.toTypedArray()
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
EMPTY_GALLERY_TAG_GROUP_ARRAY
}
}
private fun parseComment(element: Element): GalleryComment? {
return try {
val comment = GalleryComment()
// Id
val a = element.previousElementSibling()
val name = a!!.attr("name")
comment.id = name trimAnd { substring(1).toInt().toLong() }
// Editable, vote up and vote down
val c4 = JsoupUtils.getElementByClass(element, "c4")
if (null != c4) {
if ("Uploader Comment" == c4.text()) {
comment.uploader = true
}
for (e in c4.children()) {
when (e.text()) {
"Vote+" -> {
comment.voteUpAble = true
comment.voteUpEd = StringUtils.trim(e.attr("style")).isNotEmpty()
}
"Vote-" -> {
comment.voteDownAble = true
comment.voteDownEd = StringUtils.trim(e.attr("style")).isNotEmpty()
}
"Edit" -> comment.editable = true
}
}
}
// Vote state
val c7 = JsoupUtils.getElementByClass(element, "c7")
if (null != c7) {
comment.voteState = StringUtils.trim(c7.text())
}
// Score
val c5 = JsoupUtils.getElementByClass(element, "c5")
if (null != c5) {
val es = c5.children()
if (!es.isEmpty()) {
comment.score = NumberUtils.parseIntSafely(
StringUtils.trim(es[0].text()),
0,
)
}
}
// time
val c3 = JsoupUtils.getElementByClass(element, "c3")
val temp = c3!!.ownText()
val time = if (temp.endsWith(':')) {
// user
comment.user = c3.child(0).text()
temp.substring("Posted on ".length, temp.length - " by:".length)
} else {
temp.substring("Posted on ".length)
}
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).toEpochMilli()
// comment
val c6 = JsoupUtils.getElementByClass(element, "c6")
// fix underline support
for (e in c6!!.children()) {
if ("span" == e.tagName() && "text-decoration:underline;" == e.attr("style")) {
e.tagName("u")
}
}
comment.comment = c6.html()
// filter comment
if (!comment.uploader) {
val sEhFilter = EhFilter
if (sEhFilter.filterCommenter(comment.user!!) || sEhFilter.filterComment(comment.comment!!)) {
return null
}
}
// last edited
val c8 = JsoupUtils.getElementByClass(element, "c8")
if (c8 != null) {
val e = c8.children().first()
if (e != null) {
comment.lastEdited =
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(e.text())).toEpochMilli()
}
}
comment
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
null
}
}
/**
* Parse comments with html parser
*/
fun parseComments(document: Document): GalleryCommentList {
return try {
val cdiv = document.getElementById("cdiv")!!
val c1s = cdiv.getElementsByClass("c1")
val list = c1s.mapNotNull { parseComment(it) }
val chd = cdiv.getElementById("chd")
var hasMore = false
NodeTraversor.traverse(
object : NodeVisitor {
override fun head(node: Node, depth: Int) {
if (node is Element && node.text() == "click to show all") {
hasMore = true
}
}
override fun tail(node: Node, depth: Int) {}
},
chd!!,
)
GalleryCommentList(list.toTypedArray(), hasMore)
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
EMPTY_GALLERY_COMMENT_ARRAY
}
}
/**
* Parse preview pages with regular expressions
*/
@Throws(ParseException::class)
fun parsePreviewPages(body: String): Int {
return PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse preview page count error", body)
}
/**
* Parse pages with regular expressions
*/
@Throws(ParseException::class)
fun parsePages(body: String): Int {
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
?: throw ParseException("Parse pages error", body)
}
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> {
return runCatching { parseNormalPreview(body) }.getOrElse { parseLargePreview(body) }
}
private fun parseLargePreview(body: String): Pair<List<GalleryPreview>, List<String>> {
check(PATTERN_LARGE_PREVIEW.containsMatchIn(body))
return PATTERN_LARGE_PREVIEW.findAll(body).unzip {
val position = it.groupValues[2].toInt() - 1
val imageKey = getThumbKey(it.groupValues[3].trim())
val pageUrl = it.groupValues[1].trim()
LargeGalleryPreview(imageKey, position) to pageUrl
}.run {
first.toList() to second.toList()
}
}
private fun parseNormalPreview(body: String): Pair<List<GalleryPreview>, List<String>> {
check(PATTERN_NORMAL_PREVIEW.containsMatchIn(body))
return PATTERN_NORMAL_PREVIEW.findAll(body).unzip {
val position = it.groupValues[6].toInt() - 1
val imageKey = getThumbKey(it.groupValues[3].trim())
val xOffset = it.groupValues[4].toIntOrNull() ?: 0
val width = it.groupValues[1].toInt()
val height = it.groupValues[2].toInt()
val pageUrl = it.groupValues[5].trim()
NormalGalleryPreview(imageKey, position, xOffset, width, height) to pageUrl
}.run {
first.toList() to second.toList()
}
}
}
Loaded 100 of 515 files, more files were not shown because too many files have changed in this diff. Show more