Compare commits

...
Author SHA1 Message Date
tarsin f695c4598a Bump Version
Releases / Build (push) Has been cancelled
2023-01-15 09:57:00 +08:00
tarsin eada1bc023 Avoid unnecessary overhead 2023-01-14 19:56:55 +08:00
FooIbar 0ebb55551c Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783) (#672)
* Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783)

Resolve #641

* fixup! Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783)
2023-01-14 17:19:48 +08:00
tarsin 20bce419c9 Fix GalleryDetailScene bindViewFirst 2023-01-14 12:19:58 +08:00
tarsin ca3fc370c3 Minor refactor 2023-01-14 12:11:27 +08:00
tarsin 94a8b78d1a Use typesafe request 2023-01-14 12:03:55 +08:00
tarsin 06e9ff6fcf Add comments for some dependencies 2023-01-14 11:49:29 +08:00
FooIbar 699e74656d Disable drag and swipe on "All" or "Default" label 2023-01-14 11:41:51 +08:00
tarsin b3ad6ef8b8 Move out DirectionalViewPager
We rewrite it with ViewPager2 later
2023-01-14 11:33:26 +08:00
tarsin 673c8e25f5 Cleanup dependencies 2023-01-14 11:27:05 +08:00
tarsin 333c8e812d Remove lifecycle annotation
We turned to viewbinding
2023-01-14 10:34:44 +08:00
tarsin f0f7a88cfb Kotlinize EhFilter 2023-01-14 10:28:47 +08:00
tarsin 0661d78bce Rename .java to .kt 2023-01-14 10:28:47 +08:00
tarsin dc4aece272 Inline EhCookieStore supertype 2023-01-14 10:21:59 +08:00
tarsin 57a312b0b4 Singletonize EhCookieStore 2023-01-14 10:11:49 +08:00
tarsin b9897ed6ac Rename .java to .kt 2023-01-14 10:11:49 +08:00
tarsin 733230f037 Convert EhCallback to kotlin 2023-01-14 10:04:55 +08:00
tarsin fc6ba3e6ad Rename .java to .kt 2023-01-14 10:04:55 +08:00
tarsin 2d7c539c5f Convert FavouriteScene to kotlin 2023-01-14 09:55:11 +08:00
tarsin 7f34965026 Rename .java to .kt 2023-01-14 09:55:11 +08:00
tarsin de8b915cff Convert GalleryAdapter to kotlin 2023-01-14 09:32:15 +08:00
tarsin d56040581d Rename .java to .kt 2023-01-14 09:32:15 +08:00
tarsin 9869b7bbb5 Convert GalleryCommentsScene to kotlin 2023-01-14 09:26:56 +08:00
tarsin 7df97c28c0 Rename .java to .kt 2023-01-14 09:26:56 +08:00
tarsin 018a559f78 Fix crash in grid mode 2023-01-13 16:49:52 +08:00
tarsin 3b32ad5fed Don't refresh fragment when reselected 2023-01-13 10:26:41 +08:00
tarsin f796d2a842 Tweak NavigationUI to disable multiple backstack and use custom anim 2023-01-13 10:13:10 +08:00
FooIbar 2f7e84e126 Refresh the item as well if the confirmation dialog is canceled 2023-01-12 17:31:28 +08:00
FooIbar 996ddbf3db fixup! Support drag reordering for downloads 2023-01-12 16:48:30 +08:00
FooIbar 7779ba2ac5 Add "All" in download labels
Resolve #412
2023-01-12 15:23:12 +08:00
tarsin 4dadeab762 Avoid querying database on Main Thread, GalleryDetailScene, #629 2023-01-12 12:06:10 +08:00
tarsin f4d3131ef3 Put history in IO coroutines, #629 2023-01-12 11:53:41 +08:00
renovate[bot] efa2ac803e Update dependency androidx.compose:compose-bom to v2023 2023-01-12 11:50:46 +08:00
renovate[bot] e0be9d46bd Update room_version to v2.5.0 2023-01-12 11:50:32 +08:00
tarsin 106b27dbec Finalize ViewBindingize GalleryDetailScene 2023-01-12 11:46:56 +08:00
tarsin f764567523 ViewBindingize GalleryDetailScene, step 6 2023-01-12 11:40:56 +08:00
tarsin 245f8669bc ViewBindingize GalleryDetailScene, step 5 2023-01-12 11:34:51 +08:00
tarsin b265d9bba6 ViewBindingize GalleryDetailScene, step 4 2023-01-12 11:28:12 +08:00
tarsin 72228574f9 ViewBindingize GalleryDetailScene, step 3 2023-01-12 11:16:32 +08:00
tarsin fba1ac3d90 ViewBindingize GalleryDetailScene, step 2 2023-01-12 11:11:32 +08:00
tarsin 4a73fb565e ViewBindingize GalleryDetailScene, step 1 2023-01-12 11:05:41 +08:00
tarsin 13011ed4af Kotlinize GalleryDetailScene 2023-01-12 10:32:49 +08:00
tarsin b2eccdefd3 Rename .java to .kt 2023-01-12 10:32:49 +08:00
tarsin dd2d6a977b Kotlinize GalleryHolder 2023-01-12 10:01:07 +08:00
tarsin 38cd1b6040 Rename .java to .kt 2023-01-12 10:01:07 +08:00
tarsin 1a28bbcde8 ViewBindinglize GalleryPreviewsScene 2023-01-12 09:57:54 +08:00
tarsin 91013daadc Kotlinize GalleryPreviewsScene 2023-01-12 09:50:09 +08:00
tarsin 1ad435e79d Rename .java to .kt 2023-01-12 09:50:09 +08:00
tarsin fd5c300c53 Remove unused navigateToTop 2023-01-12 09:39:57 +08:00
tarsin aa7d9cf29c Remove unused TransitionNameFactory 2023-01-12 09:38:14 +08:00
tarsin 8db1c3400e Kotlinize ProgressScene 2023-01-12 09:34:48 +08:00
tarsin e723336706 Rename .java to .kt 2023-01-12 09:34:48 +08:00
tarsin b5cd4c36ab Singletonize ItemTouchHelper 2023-01-12 09:22:35 +08:00
tarsin d1928f2647 Fixup! Support drag reordering for downloads, fix drag ripple 2023-01-12 09:12:37 +08:00
tarsin 61621ae00b Use compatible compose compiler 2023-01-12 08:56:46 +08:00
tarsin ac20755c45 Bump Version
Releases / Build (push) Has been cancelled
2023-01-12 08:48:14 +08:00
renovate[bot] 610202bbbd Update dependency androidx.startup:startup-runtime to v1.2.0-alpha02 2023-01-12 07:23:13 +08:00
renovate[bot] 58ce7134e1 Update lifecycle_version to v2.6.0-alpha04 2023-01-12 07:22:59 +08:00
FooIbar 71f91cd79e Get updated positions in click listeners
Resolve #655
2023-01-12 07:22:32 +08:00
tarsin 8c821ecad3 Make EhEngine obtain its okhttpClient 2023-01-11 21:46:17 +08:00
FooIbar c704940462 Support drag reordering for downloads
Resolve #54
2023-01-11 21:31:18 +08:00
tarsin 6a7cd1b89a Add some comments and TODO 2023-01-11 14:35:43 +08:00
tarsin e29da0ba13 Cleanup res 2023-01-11 14:28:53 +08:00
tarsin 7791fa10f5 Getprofile in SelectSiteScreen 2023-01-11 14:11:36 +08:00
tarsin 48ed3c8f31 Avoid webview reentry 2023-01-11 14:07:44 +08:00
tarsin 7ad7a9b94e Add WebviewSignInScreen 2023-01-11 13:59:28 +08:00
tarsin 54b929ba24 Use LocalClipboardManager to better support multiplatform 2023-01-11 13:28:00 +08:00
FooIbar 7b655c6260 Show dialog before removing quick search 2023-01-11 13:24:33 +08:00
FooIbar 4e8d41b145 Show dialog before removing download label 2023-01-11 13:24:33 +08:00
tarsin f036774e07 Remove origin signin scene 2023-01-11 13:22:56 +08:00
tarsin 97d1bd7685 Start moving to new compose login activity 2023-01-11 13:12:16 +08:00
tarsin 8e39449856 Restore finish from SelectSiteScreen 2023-01-11 13:06:27 +08:00
tarsin 30f1a5a6eb Add SelectSiteScreen Impl 2023-01-11 13:01:46 +08:00
tarsin 627abb047c Compose dependency tweaks 2023-01-11 12:07:33 +08:00
tarsin 9284008afe Refine CookieSignInScreen 2023-01-11 11:44:13 +08:00
tarsin 9159f4616f Refine SignInScreen KeyBoard options 2023-01-11 10:41:42 +08:00
tarsin 73296b1430 Refine SignInScreen TextInput 2023-01-11 10:25:43 +08:00
tarsin c74e622d69 Add snackbar to CookieSignInScreen layout 2023-01-11 09:55:41 +08:00
tarsin f48eb55516 Basic CookieSignInScreen layout 2023-01-11 09:23:10 +08:00
tarsin b01f6ef8d3 Fixup! use local functions 2023-01-11 08:27:46 +08:00
tarsin 167185ef45 Basic signin refine 2023-01-11 07:58:40 +08:00
tarsin 777a8ffeaa Refine SignInScreen 2023-01-10 20:58:28 +08:00
FooIbar b2b3e6c485 Add tips for torrent download 2023-01-10 20:30:26 +08:00
tarsin 613d502fc1 Use Mdc3Theme 2023-01-10 20:26:39 +08:00
tarsin 79da457db4 Basic signin Compose layout 2023-01-10 20:21:35 +08:00
tarsin b1a431ed82 Use mca component to judge is dynamic color available 2023-01-10 18:17:10 +08:00
tarsin b0f2776a23 Cleanup compose theme and navigation skeleton 2023-01-10 18:10:24 +08:00
tarsin 900c346ee8 Fixup! Convert login to compose, step 1, skeleton 2023-01-10 17:57:04 +08:00
tarsin 7541a87b21 Convert login to compose, step 1, skeleton 2023-01-10 17:54:55 +08:00
tarsin 2547c67ebd Fixup migrate to androidx navigation 2023-01-09 19:05:37 +08:00
tarsin 5ae6a0b287 Fixup migrate to androidx navigation 2023-01-09 19:00:04 +08:00
tarsin f718fc0c89 Reduce one more layout depth 2023-01-09 18:07:05 +08:00
tarsin 57e69ba7be Layout tweaks 2023-01-09 17:47:44 +08:00
tarsin 6168b3e047 Fixup! Use viewLifecycleOwner 2023-01-09 14:17:49 +08:00
tarsin 3e500500e0 Rewrite SelectSiteScene 2023-01-09 13:41:45 +08:00
tarsin 42f06ff84a Rename .java to .kt 2023-01-09 13:41:45 +08:00
tarsin a7824fc473 Convert WebViewSignInScene to kotlin 2023-01-09 13:35:53 +08:00
tarsin 67e7fd6928 Rename .java to .kt 2023-01-09 13:35:53 +08:00
tarsin 450e8badba GetProfile when signin with cookies 2023-01-09 13:21:48 +08:00
tarsin 704c211e11 Rewrite CookieSignInScene with modern viewBinding 2023-01-09 13:14:30 +08:00
tarsin 0ad871cb5b Rename .java to .kt 2023-01-09 13:14:30 +08:00
tarsin 9920605570 Checkout xz to 5.4.0 2023-01-09 12:56:10 +08:00
tarsin b1e8678dbf Fixup! Rewrite SignInScene with modern coroutines 2023-01-08 22:30:35 +08:00
tarsin e2cd2ddc0d Rewrite SignInScene with modern coroutines 2023-01-08 22:22:34 +08:00
tarsin a3be0dfddf Rewrite SignInScene 2023-01-08 21:33:50 +08:00
tarsin 4b615ffc0b Rename .java to .kt 2023-01-08 21:33:50 +08:00
tarsin 64cbd189ad Convert to kotlin 2023-01-08 21:15:26 +08:00
tarsin 0fae5a128f Rename .java to .kt 2023-01-08 21:15:26 +08:00
tarsin 518b44216f Navchecked item now handled internally 2023-01-08 20:57:59 +08:00
tarsin 9ef4512ef9 Reformat code 2023-01-08 20:42:23 +08:00
tarsin 8bb5f1e955 Initial migrate to androidx navigation (#647)
* Neutralize scene firstly

* Remove StageLayout

* Add nav graph

* Rename .java to .kt

* Convert BaseScene to kotlin

* Add GalleryDetailScene to navgraph

No need to check topfragment is callback target fragment, since these callback must be cancelled in fragment's lifecycle when DestroyView

* Restore funcs

* Restore preview

* Revert "Convert BaseScene to kotlin"

Should not do it here
This reverts commit 5d2e3af6ad85fef7604d8df19ee58a957390cdb5.

* Revert "Rename .java to .kt"

This reverts commit 128c655bc98fc54528d58534651d61ff47b2284d.

* Restore drawer

* Remove all getScene usage

* Remove all getStageActivity usage

* Remove all getStageActivity usage

* setupWithNavController

* Refactor MainActivity with viewBinding

* Add navdrawer corresponding navdestination

* Migrate All normal fragment

* Add anims

* Fix statusbar flicker

* Remove SceneApplication

* Remove SceneFragment

* Remove StageActivity

* Drop scene totally

* Dynamically set navgraph

* Restore signin

* Restore login via cookies

* Restore login via webview

* Add some TODOs

* Fixup

* Restore onProvideAssistContent

* Restore checkClipboard

* Restore intent handle

* Fix handle intent

* Remove unused EhUrlOpener

* Let system handle that intent

* Restore handle download intent

* Restore AboveSnackerLayout

* Fix the last navigation
2023-01-08 20:37:11 +08:00
FooIbar 8f8932f617 Fix potential integer overflow 2023-01-08 18:06:49 +08:00
tarsin 23fd1d75c4 Fix supertype corresponding parameter warning 2023-01-08 09:15:22 +08:00
stevenyomi 593dcef550 Fix floating-point error in navigate pan (#8856) 2023-01-08 09:04:53 +08:00
o0kam1 d769fe9074 fix #642 2023-01-07 22:13:16 +08:00
tarsin 77a8fec790 Bump Version
Releases / Build (push) Has been cancelled
2023-01-07 09:10:50 +08:00
renovate[bot] 34440edfaa Update dependency com.google.android.material:material to v1.8.0-rc01 2023-01-06 21:54:07 +00:00
tarsin 3c3d8784ad Fixup cleanup Mainthread db query 2023-01-06 18:48:06 +08:00
tarsin 1591abc92c It's impossible no activity receive that Intent, remove workaround 2023-01-06 18:38:38 +08:00
tarsin 0ffa0c34f0 Remove some NotifyDataSetChanged usage 2023-01-06 18:27:54 +08:00
tarsin c7dc94b61b Singletonize GalleryListScene OnBackPressedCallback 2023-01-06 18:11:41 +08:00
tarsin a8b83871a4 Convert GalleryListScene drawer to ViewBinding 2023-01-06 18:07:57 +08:00
tarsin 6f547984ff Convert GalleryListScene to ViewBinding 2023-01-06 17:59:15 +08:00
tarsin 3979c11317 Refine GalleryListScene nullability 2023-01-06 17:36:55 +08:00
tarsin 2a1ec985f4 GalleryListScene urlBuilder cleanup 2023-01-06 17:27:30 +08:00
tarsin 2a1d49e945 Fix DownloadsScene delete label, #629 2023-01-06 13:02:12 +08:00
FooIbar 7b7e6140d2 Improve domain fronting description 2023-01-06 12:56:25 +08:00
tarsin 907cfd9694 Fix DownloadsScene add label, #629 2023-01-06 12:42:13 +08:00
tarsin b51b1d1ec6 Convert DownloadsScene to kotlin, #629 2023-01-06 12:37:24 +08:00
tarsin 6a026300e4 Rename .java to .kt 2023-01-06 12:37:24 +08:00
tarsin 8511907390 Fix HistoryScene history clearall, #629 2023-01-06 12:22:57 +08:00
tarsin 05a5be336e Fix HistoryScene history delete, #629 2023-01-06 12:21:18 +08:00
tarsin 5434b1ce19 Fix GalleryListScene quicksearch delete, #629 2023-01-06 12:19:20 +08:00
tarsin c2d4a4367c Fix GalleryListScene quicksearch insert, #629 2023-01-06 12:16:37 +08:00
tarsin de2f277513 Convert GalleryListScene to kotlin, #629 2023-01-06 12:11:20 +08:00
tarsin 283e522037 Rename .java to .kt 2023-01-06 12:11:20 +08:00
tarsin caebe4c23b Initialize database earlier, #629 2023-01-06 11:54:26 +08:00
tarsin b195277c9a Minor Refactor 2023-01-06 11:32:53 +08:00
tarsin 6b99cb6253 Make GalleryDetail well typed and implement Parcelable 2023-01-06 11:07:45 +08:00
txyyh a822bb670a Change bug-report issue template 2023-01-06 11:05:24 +08:00
tarsin af7cc11647 Extract properties to a standalone AbstractGalleryInfo 2023-01-06 11:04:18 +08:00
tarsin 0afe331d3d Rename GalleryInfoImpl to BaseGalleryInfo 2023-01-06 11:02:44 +08:00
tarsin 7af40f0cd7 Revert "Not good solution :("
This reverts commit 3283851dc4.
2023-01-06 11:00:28 +08:00
tarsin 52dc699ee2 Revert "Cleanup primary constructors"
This reverts commit ec28e21897.
2023-01-06 11:00:17 +08:00
tarsin ec28e21897 Cleanup primary constructors 2023-01-06 10:41:33 +08:00
tarsin 3283851dc4 Not good solution :( 2023-01-06 10:30:31 +08:00
tarsin ec5e0c7b25 Fuck you Jetbrains! 2023-01-06 09:55:09 +08:00
tarsin 13660cb70a Cleanup JvmFields in GalleryInfo 2023-01-06 09:33:24 +08:00
tarsin ce008f5576 Bump Version
Releases / Build (push) Has been cancelled
2023-01-06 08:49:47 +08:00
tarsin 25f7e8b35c Always show toast in BottomSheet mode, Resolve #633 2023-01-06 08:47:37 +08:00
tarsin 109398d955 Fix enter Reader from previews 2023-01-06 08:38:57 +08:00
tarsin 4a8551087b Fix crash when open history 2023-01-06 08:15:42 +08:00
tarsin 34febe3545 Fix crash when open download 2023-01-06 08:09:11 +08:00
123 changed files with 10369 additions and 17243 deletions

No files matched your search

+4 -11
View File
@@ -17,12 +17,14 @@ body:
attributes:
label: 复现步骤 / Step to reproduce
description: |
在此处写下复现的方式
Describe how to reproduce here
请从全新安装应用开始,在此处写下复现的方式。请尽量附带录屏
Describe how to reproduce here with fresh installation, with screen recording if possible.
placeholder: |
0. 重新安装应用 / Fresh install
1.
2.
3.
[录屏] / [Screen recording]
validations:
required: true
@@ -111,15 +113,6 @@ 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: true
- type: checkboxes
id: check
attributes:
+24 -63
View File
@@ -33,7 +33,7 @@ android {
minSdkVersion 28
targetSdkVersion 33
versionCode 180030
versionName "1.8.5.3-alpha01"
versionName "1.8.5.3-alpha05"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
}
@@ -52,9 +52,7 @@ android {
jvmTarget = '19'
freeCompilerArgs += [
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
"-progressive"
]
}
@@ -95,109 +93,72 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0-alpha02"
kotlinCompilerExtensionVersion = "1.4.0-dev-k1.8.0-33c0ad36f83"
}
namespace 'com.hippo.ehviewer'
}
dependencies {
def activity_version = "1.6.1"
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.annotation:annotation:1.5.0"
implementation "androidx.annotation:annotation-experimental:1.3.0"
implementation "androidx.appcompat:appcompat:1.7.0-alpha01"
def arch_core_version = "2.1.0"
implementation "androidx.arch.core:core-common:$arch_core_version"
implementation "androidx.arch.core:core-runtime:$arch_core_version"
// 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"
def collection_version = "1.3.0-alpha02"
implementation "androidx.collection:collection:$collection_version"
implementation "androidx.collection:collection-ktx:$collection_version"
implementation platform("androidx.compose:compose-bom:2022.12.00")
implementation "androidx.compose.animation:animation"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.foundation:foundation-layout"
// 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.compose.runtime:runtime"
implementation "androidx.compose.runtime:runtime-saveable"
implementation "androidx.compose.ui:ui"
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"
def core_version = "1.9.0"
implementation "androidx.core:core-ktx:$core_version"
implementation "androidx.customview:customview:1.2.0-alpha02"
implementation "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03"
def fragment_version = "1.6.0-alpha04"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
def lifecycle_version = "2.6.0-alpha03"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
implementation "androidx.loader:loader:1.1.0"
// 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'
def room_version = "2.5.0-rc01"
implementation "androidx.room:room-runtime:$room_version"
// 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.startup:startup-runtime:1.2.0-alpha01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation "androidx.transition:transition-ktx:1.4.1"
implementation "androidx.vectordrawable:vectordrawable:1.2.0-beta01"
implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"
implementation 'androidx.viewpager:viewpager:1.1.0-alpha01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'com.drakeet.drawer:drawer:1.0.3'
implementation 'com.google.android.material:material:1.8.0-beta01'
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'
implementation 'com.squareup.okhttp3:okhttp-coroutines'
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 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jsoup:jsoup:1.15.3'
// Tachiyomi Reader
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'dev.chrisbanes.insetter:insetter:0.6.1'
}
configurations.all {
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
exclude group: 'dev.rikka.rikkax.material', module: 'material'
exclude group: 'androidx.compose.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'
+21 -38
View File
@@ -1,19 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
@@ -28,13 +13,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application
android:name="com.hippo.ehviewer.EhApplication"
android:name=".EhApplication"
android:allowBackup="true"
android:appCategory="image"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -42,29 +26,33 @@
android:fullBackupContent="@xml/backup_scheme"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true"
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"
android:theme="@style/AppTheme"
tools:targetApi="tiramisu">
<activity
android:name="com.hippo.ehviewer.ui.MainActivity"
android:name=".ui.LoginActivity"
android:exported="false"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".ui.MainActivity"
android:banner="@mipmap/ic_leanback_banner"
android:configChanges="screenSize"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
@@ -85,26 +73,26 @@
<data android:host="g.e-hentai.org" />
<data android:host="lofi.e-hentai.org" />
</intent-filter>
<intent-filter android:label="@string/keyword_search">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter android:label="@string/image_search">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name="eu.kanade.tachiyomi.ui.reader.ReaderActivity"
android:configChanges="screenSize"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -138,21 +126,18 @@
<data android:mimeType="application/x-xz-compressed-tar" />
</intent-filter>
</activity>
<activity
android:name="com.hippo.ehviewer.ui.SettingsActivity"
android:name=".ui.SettingsActivity"
android:configChanges="screenSize"
android:label="@string/settings"
android:theme="@style/AppTheme" />
<activity
android:name="com.hippo.ehviewer.ui.SecurityActivity"
android:name=".ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings"
android:theme="@style/AppTheme" />
<activity
android:name="com.hippo.ehviewer.shortcuts.ShortcutsActivity"
android:name=".shortcuts.ShortcutsActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
@@ -166,9 +151,8 @@
android:name="autoStoreLocales"
android:value="true" />
</service>
<service
android:name="com.hippo.ehviewer.download.DownloadService"
android:name=".download.DownloadService"
android:foregroundServiceType="dataSync"
android:label="@string/download_service_label" />
@@ -177,11 +161,10 @@
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>
</manifest>
+1 -1
View File
@@ -45,6 +45,6 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
add_subdirectory(libarchive)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c)
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)
+11
View File
@@ -0,0 +1,11 @@
#include <string.h>
#include <jni.h>
#define GIF_HEADER_87A "GIF87a"
#define GIF_HEADER_89A "GIF89a"
JNIEXPORT jboolean JNICALL
Java_com_hippo_Native_isGif(JNIEnv *env, jobject thiz, jobject buffer) {
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
}
@@ -0,0 +1,49 @@
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
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
is ActivityNavigator.Destination
) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(com.hippo.ehviewer.R.anim.scene_open_enter)
.setExitAnim(com.hippo.ehviewer.R.anim.scene_open_exit)
.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.
navController.navigate(item.itemId, null, options)
// Return true only if the destination we've navigated to matches the MenuItem
navController.currentDestination?.matchDestination(item.itemId) == true
} catch (e: IllegalArgumentException) {
false
}
}
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,88 @@
@file:Suppress("SameParameterValue")
package coil.decode
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.decodeHex
import okio.ForwardingSource
import okio.Source
/**
* A [ForwardingSource] that rewrites the GIF frame delay in every graphics control block if it's
* below a threshold.
*/
internal class FrameDelayRewritingSource(delegate: Source) : ForwardingSource(delegate) {
// An intermediary buffer so we can read and alter the data before it's written to the destination.
private val buffer = Buffer()
override fun read(sink: Buffer, byteCount: Long): Long {
// Ensure our buffer has enough bytes to satisfy this read.
request(byteCount)
// Short circuit if there are no bytes in the buffer.
if (buffer.size == 0L) {
return if (byteCount == 0L) 0L else -1L
}
// Search through the buffer and rewrite any frame delays below the threshold.
var bytesWritten = 0L
while (true) {
val index = indexOf(FRAME_DELAY_START_MARKER)
if (index == -1L) break
// Write up until the end of the frame delay start marker.
bytesWritten += write(sink, index + FRAME_DELAY_START_MARKER_SIZE_BYTES)
// Check for the end of the graphics control extension block.
if (!request(5) || buffer[4] != 0.toByte()) continue
// Rewrite the frame delay if it is below the threshold.
// The frame delay is stored as two unsigned bytes in reverse order
// (i.e. the most significant bits are in the byte that's read second).
val frameDelay = (buffer[2].toUByte().toInt() shl 8) or buffer[1].toUByte().toInt()
if (frameDelay < MINIMUM_FRAME_DELAY) {
sink.writeByte(buffer[0].toInt())
sink.writeByte(DEFAULT_FRAME_DELAY)
sink.writeByte(0)
buffer.skip(3)
}
}
// Write anything left in the source.
if (bytesWritten < byteCount) {
bytesWritten += write(sink, byteCount - bytesWritten)
}
return if (bytesWritten == 0L) -1 else bytesWritten
}
private fun indexOf(bytes: ByteString): Long {
var index = -1L
while (true) {
index = buffer.indexOf(bytes[0], index + 1)
if (index == -1L) break
if (request(bytes.size.toLong()) && buffer.rangeEquals(index, bytes)) break
}
return index
}
private fun write(sink: Buffer, byteCount: Long): Long {
return buffer.read(sink, byteCount).coerceAtLeast(0)
}
private fun request(byteCount: Long): Boolean {
if (buffer.size >= byteCount) return true
val toRead = byteCount - buffer.size
return super.read(buffer, toRead) == toRead
}
private companion object {
// https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
// See: "Graphics Control Extension"
private val FRAME_DELAY_START_MARKER = "0021F904".decodeHex()
private const val FRAME_DELAY_START_MARKER_SIZE_BYTES = 4
private const val MINIMUM_FRAME_DELAY = 2
private const val DEFAULT_FRAME_DELAY = 10
}
}
+3
View File
@@ -18,6 +18,7 @@
package com.hippo
import java.io.FileDescriptor
import java.nio.ByteBuffer
object Native {
fun initialize() {
@@ -26,4 +27,6 @@ object Native {
@JvmStatic
external fun getFd(fd: FileDescriptor?): Int
external fun isGif(buffer: ByteBuffer): Boolean
}
@@ -352,21 +352,7 @@ public final class DiskLruCache implements Closeable {
i.remove();
}
}
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
}
/**
* Creates a new journal that omits redundant information. This replaces the
@@ -406,7 +392,21 @@ public final class DiskLruCache implements Closeable {
journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
/**
* Return {@code true} if it contains the key
@@ -16,7 +16,6 @@
package com.hippo.ehviewer;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -25,7 +24,6 @@ import android.os.Debug;
import androidx.annotation.NonNull;
import com.hippo.scene.StageActivity;
import com.hippo.util.PackageUtils;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
@@ -78,27 +76,10 @@ public final class Crash {
// Runtime
String topActivityClazzName = "null";
String topSceneClazzName = "null";
try {
Activity topActivity = ((EhApplication) context.getApplicationContext()).getTopActivity();
if (null != topActivity) {
topActivityClazzName = topActivity.getClass().getName();
if (topActivity instanceof StageActivity) {
Class<?> clazz = ((StageActivity) topActivity).getTopSceneClass();
if (clazz != null) {
topSceneClazzName = clazz.getName();
}
}
}
} catch (Throwable e) {
// Ignore
}
fw.write("======== Runtime ========\r\n");
fw.write("TopActivity=");
fw.write(avoidNull(topActivityClazzName));
fw.write("\r\n");
fw.write("TopScene=");
fw.write(avoidNull(topSceneClazzName));
fw.write("\r\n");
fw.write("\r\n");
@@ -19,6 +19,7 @@ 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
@@ -44,11 +45,11 @@ 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.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.scene.SceneApplication
import com.hippo.util.BitmapUtils
import com.hippo.util.ExceptionUtils
import com.hippo.util.ReadableTime
@@ -70,7 +71,7 @@ import java.util.Arrays
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderFactory {
class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
private val mIdGenerator = IntIdGenerator()
private val mGlobalStuffMap = HashMap<Int, Any>()
private val mActivityList = ArrayList<Activity>()
@@ -100,18 +101,20 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
}
handler?.uncaughtException(t, e)
}
super<SceneApplication>.onCreate()
super<Application>.onCreate()
Native.initialize()
GetText.initialize(this)
Settings.initialize()
ReadableTime.initialize(this)
AppConfig.initialize(this)
SpiderDen.initialize(this)
EhDB.initialize(this)
BitmapUtils.initialize(this)
EhTagDatabase.update()
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
launchIO {
launchIO {
ehDatabase
}
launchIO {
downloadManager
}
@@ -278,7 +281,7 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
private set
@JvmStatic
val ehCookieStore by lazy { EhCookieStore(application) }
val ehCookieStore by lazy { EhCookieStore }
@JvmStatic
val ehProxySelector by lazy { EhProxySelector() }
@@ -342,5 +345,8 @@ class EhApplication : SceneApplication(), DefaultLifecycleObserver, ImageLoaderF
@JvmStatic
val readerPreferences by lazy { ReaderPreferences(AndroidPreferenceStore(application)) }
@JvmStatic
val ehDatabase by lazy { buildMainDB(application) }
}
}
+29 -9
View File
@@ -58,7 +58,7 @@ import java.util.List;
public class EhDB {
private static final int CUR_DB_VER = 4;
private static EhDatabase db;
private static final EhDatabase db = EhApplication.getEhDatabase();
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
switch (oldVersion) {
@@ -99,10 +99,6 @@ public class EhDB {
}
}
public static void initialize(Context context) {
db = Room.databaseBuilder(context, EhDatabase.class, "eh.db").allowMainThreadQueries().build();
}
public static synchronized List<DownloadInfo> getAllDownloadInfo() {
DownloadsDao dao = db.downloadsDao();
List<DownloadInfo> list = dao.list();
@@ -115,10 +111,34 @@ public class EhDB {
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.gid)) {
if (null != dao.load(downloadInfo.getGid())) {
// Update
dao.update(downloadInfo);
} else {
@@ -256,7 +276,7 @@ public class EhDB {
public static synchronized void putLocalFavorites(GalleryInfo galleryInfo) {
LocalFavoritesDao dao = db.localFavoritesDao();
if (null == dao.load(galleryInfo.gid)) {
if (null == dao.load(galleryInfo.getGid())) {
LocalFavoriteInfo info;
if (galleryInfo instanceof LocalFavoriteInfo) {
info = (LocalFavoriteInfo) galleryInfo;
@@ -335,7 +355,7 @@ public class EhDB {
info = new HistoryInfo(galleryInfo);
}
info.time = System.currentTimeMillis();
if (null != dao.load(info.gid)) {
if (null != dao.load(info.getGid())) {
dao.update(info);
} else {
dao.insert(info);
@@ -345,7 +365,7 @@ public class EhDB {
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
HistoryDao dao = db.historyDao();
for (HistoryInfo info : historyInfoList) {
if (null == dao.load(info.gid)) {
if (null == dao.load(info.getGid())) {
dao.insert(info);
}
}
@@ -51,7 +51,7 @@ public class FavouriteStatusRouter {
for (Map<Long, GalleryInfo> map : maps.values()) {
GalleryInfo info = map.get(gid);
if (info != null) {
info.favoriteSlot = slot;
info.setFavoriteSlot(slot);
}
}
@@ -26,12 +26,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.browser.customtabs.CustomTabsIntent;
import com.hippo.ehviewer.client.EhUrlOpener;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.Announcer;
import com.hippo.scene.StageActivity;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
@@ -56,7 +52,7 @@ public final class UrlOpener {
if (galleryDetail != null) {
GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(url);
if (result != null) {
if (result.gid == galleryDetail.gid) {
if (result.gid == galleryDetail.getGid()) {
intent = new Intent(context, ReaderActivity.class);
intent.setAction(ReaderActivity.ACTION_EH);
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail);
@@ -77,16 +73,6 @@ public final class UrlOpener {
}
}
}
Announcer announcer = EhUrlOpener.parseUrl(url);
if (null != announcer) {
intent = new Intent(context, MainActivity.class);
intent.setAction(StageActivity.ACTION_START_SCENE);
intent.putExtra(StageActivity.KEY_SCENE_NAME, announcer.getClazz().getName());
intent.putExtra(StageActivity.KEY_SCENE_ARGS, announcer.getArgs());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return;
}
}
CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder();
@@ -15,7 +15,6 @@
*/
package com.hippo.ehviewer.client
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.CancellationException
@@ -26,136 +25,13 @@ 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 method = request.method
val params = request.args
val callback: Callback<Any?>? = request.callback
try {
val result: Any? = when (method) {
METHOD_SIGN_IN -> EhEngine.signIn(
mOkHttpClient,
params!![0] as String,
params[1] as String
)
METHOD_GET_GALLERY_LIST -> EhEngine.getGalleryList(
mOkHttpClient,
params!![0] as String
)
METHOD_GET_GALLERY_DETAIL -> EhEngine.getGalleryDetail(
mOkHttpClient,
params!![0] as String?
)
METHOD_GET_PREVIEW_SET -> EhEngine.getPreviewSet(
mOkHttpClient,
params!![0] as String?
)
METHOD_GET_RATE_GALLERY -> EhEngine.rateGallery(
mOkHttpClient,
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(
mOkHttpClient,
params!![0] as String?,
params[1] as String,
params[2] as String?
)
METHOD_GET_GALLERY_TOKEN -> EhEngine.getGalleryToken(
mOkHttpClient,
params!![0] as Long,
params[1] as String?,
params[2] as Int
)
METHOD_GET_FAVORITES -> EhEngine.getFavorites(
mOkHttpClient,
params!![0] as String
)
METHOD_ADD_FAVORITES -> EhEngine.addFavorites(
mOkHttpClient,
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(
mOkHttpClient,
params!![0] as LongArray,
params[1] as Array<String?>,
params[2] as Int
)
METHOD_MODIFY_FAVORITES -> EhEngine.modifyFavorites(
mOkHttpClient,
params!![0] as String,
params[1] as LongArray,
params[2] as Int
)
METHOD_GET_TORRENT_LIST -> EhEngine.getTorrentList(
mOkHttpClient,
params!![0] as String?,
params[1] as Long,
params[2] as String?
)
METHOD_GET_PROFILE -> EhEngine.getProfile(mOkHttpClient)
METHOD_VOTE_COMMENT -> EhEngine.voteComment(
mOkHttpClient,
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(
mOkHttpClient,
params!![0] as File,
params[1] as Boolean,
params[2] as Boolean,
params[3] as Boolean
)
METHOD_ARCHIVE_LIST -> EhEngine.getArchiveList(
mOkHttpClient,
params!![0] as String?,
params[1] as Long,
params[2] as String?
)
METHOD_DOWNLOAD_ARCHIVE -> EhEngine.downloadArchive(
mOkHttpClient,
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(
mOkHttpClient,
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(mOkHttpClient)
else -> throw IllegalStateException("Can't detect method $method")
val result: Any? = request.run {
if (args == null)
execute(method)
else
execute(method, *args!!)
}
withUIContext { callback?.onSuccess(result) }
} catch (e: Exception) {
@@ -167,6 +43,119 @@ object EhClient {
}
}
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)
@@ -192,5 +181,4 @@ object EhClient {
const val METHOD_DOWNLOAD_ARCHIVE = 18
const val METHOD_VOTE_TAG = 19
const val METHOD_GET_UCONFIG = 20
private val mOkHttpClient = okHttpClient
}
@@ -1,116 +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.content.Context;
import androidx.annotation.NonNull;
import com.hippo.network.CookieRepository;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
public class EhCookieStore extends CookieRepository {
public static final String KEY_IPD_MEMBER_ID = "ipb_member_id";
public static final String KEY_IPD_PASS_HASH = "ipb_pass_hash";
public static final String KEY_IGNEOUS = "igneous";
public static final Cookie sTipsCookie =
new Cookie.Builder()
.name(EhConfig.KEY_CONTENT_WARNING)
.value(EhConfig.CONTENT_WARNING_NOT_SHOW)
.domain(EhUrl.DOMAIN_E)
.path("/")
.expiresAt(Long.MAX_VALUE)
.build();
public EhCookieStore(Context context) {
super(context, "okhttp3-cookie.db");
}
public static Cookie newCookie(Cookie cookie, String newDomain, boolean forcePersistent,
boolean forceLongLive, boolean forceNotHostOnly) {
Cookie.Builder builder = new Cookie.Builder();
builder.name(cookie.name());
builder.value(cookie.value());
if (forceLongLive) {
builder.expiresAt(Long.MAX_VALUE);
} else if (cookie.persistent()) {
builder.expiresAt(cookie.expiresAt());
} else if (forcePersistent) {
builder.expiresAt(Long.MAX_VALUE);
}
if (cookie.hostOnly() && !forceNotHostOnly) {
builder.hostOnlyDomain(newDomain);
} else {
builder.domain(newDomain);
}
builder.path(cookie.path());
if (cookie.secure()) {
builder.secure();
}
if (cookie.httpOnly()) {
builder.httpOnly();
}
return builder.build();
}
public void signOut() {
clear();
}
public boolean hasSignedIn() {
HttpUrl url = HttpUrl.parse(EhUrl.HOST_E);
return contains(url, KEY_IPD_MEMBER_ID) &&
contains(url, KEY_IPD_PASS_HASH);
}
@NonNull
@Override
public List<Cookie> loadForRequest(@NonNull HttpUrl url) {
List<Cookie> cookies = super.loadForRequest(url);
boolean checkTips = domainMatch(url, EhUrl.DOMAIN_E);
if (checkTips) {
List<Cookie> result = new ArrayList<>(cookies.size() + 1);
// Add all but skip some
for (Cookie cookie : cookies) {
String name = cookie.name();
if (EhConfig.KEY_CONTENT_WARNING.equals(name)) {
continue;
}
if (EhConfig.KEY_UCONFIG.equals(name)) {
continue;
}
result.add(cookie);
}
// Add some
result.add(sTipsCookie);
return Collections.unmodifiableList(result);
} else {
return cookies;
}
}
}
@@ -0,0 +1,247 @@
/*
* 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
import com.hippo.network.CookieDatabase
import com.hippo.network.CookieSet
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.Collections
import java.util.regex.Pattern
object EhCookieStore : CookieJar {
private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie.db")
private val map: MutableMap<String, CookieSet> = db.allCookies
fun signOut() {
clear()
}
fun hasSignedIn(): Boolean {
val url = EhUrl.HOST_E.toHttpUrl()
return contains(url, KEY_IPD_MEMBER_ID) &&
contains(url, KEY_IPD_PASS_HASH)
}
const val KEY_IPD_MEMBER_ID = "ipb_member_id"
const val KEY_IPD_PASS_HASH = "ipb_pass_hash"
const val KEY_IGNEOUS = "igneous"
private val sTipsCookie: Cookie = Cookie.Builder()
.name(EhConfig.KEY_CONTENT_WARNING)
.value(EhConfig.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 {
val builder = Cookie.Builder()
builder.name(cookie.name)
builder.value(cookie.value)
if (forceLongLive) {
builder.expiresAt(Long.MAX_VALUE)
} else if (cookie.persistent) {
builder.expiresAt(cookie.expiresAt)
} else if (forcePersistent) {
builder.expiresAt(Long.MAX_VALUE)
}
if (cookie.hostOnly && !forceNotHostOnly) {
builder.hostOnlyDomain(newDomain)
} else {
builder.domain(newDomain)
}
builder.path(cookie.path)
if (cookie.secure) {
builder.secure()
}
if (cookie.httpOnly) {
builder.httpOnly()
}
return builder.build()
}
@Synchronized
fun addCookie(cookie: Cookie) {
// For cookie database
var toAdd: Cookie? = null
var toUpdate: Cookie? = null
var toRemove: Cookie? = null
var set = map[cookie.domain]
if (set == null) {
set = CookieSet()
map[cookie.domain] = set
}
if (cookie.expiresAt <= System.currentTimeMillis()) {
toRemove = set.remove(cookie)
// If the cookie is not persistent, it's not in database
if (toRemove != null && !toRemove.persistent) {
toRemove = null
}
} else {
toAdd = cookie
toUpdate = set.add(cookie)
// If the cookie is not persistent, it's not in database
if (!toAdd.persistent) toAdd = null
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
// Remove the cookie if it updates to null
if (toAdd == null && toUpdate != null) {
toRemove = toUpdate
toUpdate = null
}
}
if (toRemove != null) {
db.remove(toRemove)
}
if (toAdd != null) {
if (toUpdate != null) {
db.update(toUpdate, toAdd)
} else {
db.add(toAdd)
}
}
}
fun getCookieHeader(url: HttpUrl): String {
val cookies = getCookies(url)
val cookieHeader = StringBuilder()
var i = 0
val size = cookies.size
while (i < size) {
if (i > 0) {
cookieHeader.append("; ")
}
val cookie = cookies[i]
cookieHeader.append(cookie.name).append('=').append(cookie.value)
i++
}
return cookieHeader.toString()
}
@Synchronized
fun getCookies(url: HttpUrl): List<Cookie> {
val accepted: MutableList<Cookie> = ArrayList()
val expired: MutableList<Cookie> = ArrayList()
for ((domain, cookieSet) in map) {
if (domainMatch(url, domain)) {
cookieSet[url, accepted, expired]
}
}
for (cookie in expired) {
if (cookie.persistent) {
db.remove(cookie)
}
}
// RFC 6265 Section-5.4 step 2, sort the cookie-list
// Cookies with longer paths are listed before cookies with shorter paths.
// Ignore creation-time, we don't store them.
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
return accepted
}
fun contains(url: HttpUrl, name: String?): Boolean {
for (cookie in getCookies(url)) {
if (cookie.name == name) {
return true
}
}
return false
}
/**
* Remove all cookies in this `CookieRepository`.
*/
@Synchronized
fun clear() {
map.clear()
db.clear()
}
@Synchronized
fun close() {
db.close()
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
addCookie(cookie)
}
}
/**
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
* of Android's private InetAddress#isNumeric API.
*
*
* This matches IPv6 addresses as a hex string containing at least one colon, and possibly
* including dots after the first colon. It matches IPv4 addresses as strings containing only
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
* verification).
*/
private val VERIFY_AS_IP_ADDRESS =
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
/**
* Returns true if `host` is not a host name and might be an IP address.
*/
private fun verifyAsIpAddress(host: String): Boolean {
return VERIFY_AS_IP_ADDRESS.matcher(host).matches()
}
// okhttp3.Cookie.domainMatch(HttpUrl, String)
@JvmStatic
fun domainMatch(url: HttpUrl, domain: String?): Boolean {
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
)
// As in 'example.com' matching 'www.example.com'.
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookies = getCookies(url)
val checkTips = domainMatch(url, EhUrl.DOMAIN_E)
return if (checkTips) {
val result: MutableList<Cookie> = ArrayList(cookies.size + 1)
// 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) {
continue
}
result.add(cookie)
}
// Add some
result.add(sTipsCookie)
Collections.unmodifiableList(result)
} else {
cookies
}
}
}
@@ -19,6 +19,7 @@ import android.text.TextUtils
import android.util.Log
import android.util.Pair
import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.application
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
@@ -55,7 +56,6 @@ import okhttp3.Headers
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
@@ -75,7 +75,8 @@ object EhEngine {
private const val SAD_PANDA_LENGTH = "9615"
private const val KOKOMADE_URL = "https://exhentai.org/img/kokomade.jpg"
private val MEDIA_TYPE_JPEG: MediaType = "image/jpeg".toMediaType()
private var sEhFilter = EhFilter.getInstance()
private var sEhFilter = EhFilter
private val okHttpClient = EhApplication.okHttpClient
private fun transformException(code: Int, headers: Headers?, body: String?, e: Throwable?) {
// Check sad panda
@@ -116,7 +117,7 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun signIn(okHttpClient: OkHttpClient, username: String, password: String): String {
suspend fun signIn(username: String, password: String): String {
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
val builder = FormBody.Builder()
.add("referer", referer)
@@ -151,7 +152,6 @@ object EhEngine {
@Throws(Throwable::class)
private suspend fun fillGalleryList(
okHttpClient: OkHttpClient,
list: MutableList<GalleryInfo>,
url: String,
filter: Boolean
@@ -188,7 +188,7 @@ object EhEngine {
filter && sEhFilter.needTags() && !hasTags || Settings.getShowGalleryPages() && !hasPages ||
hasRated
if (needApi) {
fillGalleryListByApi(okHttpClient, list, url)
fillGalleryListByApi(list, url)
}
// Filter tag
@@ -212,7 +212,7 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getGalleryList(okHttpClient: OkHttpClient, url: String): GalleryListParser.Result {
suspend fun getGalleryList(url: String): GalleryListParser.Result {
val referer = EhUrl.getReferer()
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer).build()
@@ -234,7 +234,7 @@ object EhEngine {
transformException(code, headers, body, e)
throw e
}
fillGalleryList(okHttpClient, result.galleryInfoList, url, true)
fillGalleryList(result.galleryInfoList, url, true)
return result
}
@@ -242,7 +242,6 @@ object EhEngine {
@JvmStatic
@Throws(Throwable::class)
suspend fun fillGalleryListByApi(
okHttpClient: OkHttpClient,
galleryInfoList: List<GalleryInfo>,
referer: String
): List<GalleryInfo> {
@@ -252,7 +251,7 @@ object EhEngine {
while (i < size) {
requestItems.add(galleryInfoList[i])
if (requestItems.size == MAX_REQUEST_SIZE || i == size - 1) {
doFillGalleryListByApi(okHttpClient, requestItems, referer)
doFillGalleryListByApi(requestItems, referer)
requestItems.clear()
}
i++
@@ -262,7 +261,6 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun doFillGalleryListByApi(
okHttpClient: OkHttpClient,
galleryInfoList: List<GalleryInfo>,
referer: String
) {
@@ -307,7 +305,7 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getGalleryDetail(okHttpClient: OkHttpClient, url: String?): GalleryDetail {
suspend fun getGalleryDetail(url: String?): GalleryDetail {
val referer = EhUrl.getReferer()
Log.d(TAG, url!!)
val request = EhRequestBuilder(url, referer).build()
@@ -335,7 +333,7 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getPreviewSet(okHttpClient: OkHttpClient, url: String?): Pair<PreviewSet, Int> {
suspend fun getPreviewSet(url: String?): Pair<PreviewSet, Int> {
val referer = EhUrl.getReferer()
Log.d(TAG, url!!)
val request = EhRequestBuilder(url, referer).build()
@@ -363,7 +361,6 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun rateGallery(
okHttpClient: OkHttpClient,
apiUid: Long, apiKey: String?, gid: Long, token: String?,
rating: Float
): RateGalleryParser.Result {
@@ -403,7 +400,6 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun commentGallery(
okHttpClient: OkHttpClient,
url: String?, comment: String, id: String?
): GalleryCommentList {
val builder = FormBody.Builder()
@@ -444,7 +440,7 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun getGalleryToken(
okHttpClient: OkHttpClient, gid: Long,
gid: Long,
gtoken: String?, page: Int
): String {
val json = JSONObject()
@@ -482,7 +478,7 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun getFavorites(
okHttpClient: OkHttpClient, url: String
url: String
): FavoritesParser.Result {
val referer = EhUrl.getReferer()
Log.d(TAG, url)
@@ -505,7 +501,7 @@ object EhEngine {
transformException(code, headers, body, e)
throw e
}
fillGalleryList(okHttpClient, result.galleryInfoList, url, false)
fillGalleryList(result.galleryInfoList, url, false)
return result
}
@@ -515,7 +511,7 @@ object EhEngine {
*/
@Throws(Throwable::class)
suspend fun addFavorites(
okHttpClient: OkHttpClient, gid: Long,
gid: Long,
token: String?, dstCat: Int, note: String?
): Void? {
val catStr: String = when (dstCat) {
@@ -564,14 +560,14 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun addFavoritesRange(
okHttpClient: OkHttpClient, gidArray: LongArray,
gidArray: LongArray,
tokenArray: Array<String?>, dstCat: Int
): Void? {
AssertUtils.assertEquals(gidArray.size, tokenArray.size)
var i = 0
val n = gidArray.size
while (i < n) {
addFavorites(okHttpClient, gidArray[i], tokenArray[i], dstCat, null)
addFavorites(gidArray[i], tokenArray[i], dstCat, null)
i++
}
return null
@@ -579,7 +575,7 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun modifyFavorites(
okHttpClient: OkHttpClient, url: String,
url: String,
gidArray: LongArray, dstCat: Int
): FavoritesParser.Result {
val catStr: String = when (dstCat) {
@@ -623,13 +619,13 @@ object EhEngine {
transformException(code, headers, body, e)
throw e
}
fillGalleryList(okHttpClient, result.galleryInfoList, url, false)
fillGalleryList(result.galleryInfoList, url, false)
return result
}
@Throws(Throwable::class)
suspend fun getTorrentList(
okHttpClient: OkHttpClient, url: String?,
url: String?,
gid: Long, token: String?
): List<TorrentParser.Result> {
val referer = EhUrl.getGalleryDetailUrl(gid, token)
@@ -657,7 +653,7 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun getArchiveList(
okHttpClient: OkHttpClient, url: String?,
url: String?,
gid: Long, token: String?
): ArchiveParser.Result {
val referer = EhUrl.getGalleryDetailUrl(gid, token)
@@ -685,7 +681,7 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun downloadArchive(
okHttpClient: OkHttpClient, gid: Long,
gid: Long,
token: String?, or: String?, res: String?, isHAtH: Boolean
): String? {
if (or.isNullOrEmpty()) {
@@ -756,7 +752,6 @@ object EhEngine {
@Throws(Throwable::class)
private suspend fun getProfileInternal(
okHttpClient: OkHttpClient,
url: String, referer: String
): ProfileParser.Result {
Log.d(TAG, url)
@@ -780,9 +775,7 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getProfile(
okHttpClient: OkHttpClient
): ProfileParser.Result {
suspend fun getProfile(): ProfileParser.Result {
val url = EhUrl.URL_FORUMS
Log.d(TAG, url)
val request = EhRequestBuilder(url, null).build()
@@ -795,7 +788,7 @@ object EhEngine {
code = response.code
headers = response.headers
body = response.body.string()
return getProfileInternal(okHttpClient, ForumsParser.parse(body), url)
return getProfileInternal(ForumsParser.parse(body), url)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
@@ -805,9 +798,7 @@ object EhEngine {
}
@Throws(Throwable::class)
suspend fun getUConfig(
okHttpClient: OkHttpClient
): Void? {
suspend fun getUConfig(): Void? {
val url = EhUrl.getUConfigUrl()
Log.d(TAG, url)
var request = EhRequestBuilder(url, null).build()
@@ -852,7 +843,7 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun voteComment(
okHttpClient: OkHttpClient, apiUid: Long,
apiUid: Long,
apiKey: String?, gid: Long, token: String?, commentId: Long, commentVote: Int
): VoteCommentParser.Result {
val json = JSONObject()
@@ -891,7 +882,7 @@ object EhEngine {
@Throws(Throwable::class)
suspend fun voteTag(
okHttpClient: OkHttpClient, apiUid: Long,
apiUid: Long,
apiKey: String?, gid: Long, token: String?, tags: String?, vote: Int
): VoteTagParser.Result {
val json = JSONObject()
@@ -933,7 +924,7 @@ object EhEngine {
*/
@Throws(Throwable::class)
suspend fun imageSearch(
okHttpClient: OkHttpClient, image: File,
image: File,
uss: Boolean, osc: Boolean, se: Boolean
): GalleryListParser.Result {
val builder = MultipartBody.Builder()
@@ -992,14 +983,13 @@ object EhEngine {
transformException(code, headers, body, e)
throw e
}
fillGalleryList(okHttpClient, result.galleryInfoList, url, true)
fillGalleryList(result.galleryInfoList, url, true)
return result
}
@JvmStatic
@Throws(Throwable::class)
fun getGalleryPage(
okHttpClient: OkHttpClient,
url: String?,
gid: Long,
token: String?
@@ -1029,7 +1019,6 @@ object EhEngine {
@JvmStatic
@Throws(Throwable::class)
fun getGalleryPageApi(
okHttpClient: OkHttpClient,
gid: Long,
index: Int,
pToken: String?,
@@ -1,315 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client;
import android.util.Log;
import androidx.annotation.NonNull;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.Filter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class EhFilter {
public static final int MODE_TITLE = 0;
public static final int MODE_UPLOADER = 1;
public static final int MODE_TAG = 2;
public static final int MODE_TAG_NAMESPACE = 3;
public static final int MODE_COMMENTER = 4;
public static final int MODE_COMMENT = 5;
private static final String TAG = EhFilter.class.getSimpleName();
private static EhFilter sInstance;
private final List<Filter> mTitleFilterList = new ArrayList<>();
private final List<Filter> mUploaderFilterList = new ArrayList<>();
private final List<Filter> mTagFilterList = new ArrayList<>();
private final List<Filter> mTagNamespaceFilterList = new ArrayList<>();
private final List<Filter> mCommenterFilterList = new ArrayList<>();
private final List<Filter> mCommentFilterList = new ArrayList<>();
private EhFilter() {
List<Filter> list = EhDB.getAllFilter();
for (int i = 0, n = list.size(); i < n; i++) {
Filter filter = list.get(i);
switch (filter.mode) {
case MODE_TITLE -> {
filter.text = filter.text.toLowerCase();
mTitleFilterList.add(filter);
}
case MODE_TAG -> {
filter.text = filter.text.toLowerCase();
mTagFilterList.add(filter);
}
case MODE_TAG_NAMESPACE -> {
filter.text = filter.text.toLowerCase();
mTagNamespaceFilterList.add(filter);
}
case MODE_UPLOADER -> mUploaderFilterList.add(filter);
case MODE_COMMENTER -> mCommenterFilterList.add(filter);
case MODE_COMMENT -> mCommentFilterList.add(filter);
default -> Log.d(TAG, "Unknown mode: " + filter.mode);
}
}
}
public static @NonNull EhFilter getInstance() {
if (sInstance == null) {
sInstance = new EhFilter();
}
return sInstance;
}
public List<Filter> getTitleFilterList() {
return mTitleFilterList;
}
public List<Filter> getUploaderFilterList() {
return mUploaderFilterList;
}
public List<Filter> getTagFilterList() {
return mTagFilterList;
}
public List<Filter> getTagNamespaceFilterList() {
return mTagNamespaceFilterList;
}
public List<Filter> getCommenterFilterList() {
return mCommenterFilterList;
}
public List<Filter> getCommentFilterList() {
return mCommentFilterList;
}
public synchronized boolean addFilter(Filter filter) {
// enable filter by default before it is added to database
filter.enable = true;
if (!EhDB.addFilter(filter)) return false;
switch (filter.mode) {
case MODE_TITLE -> {
filter.text = filter.text.toLowerCase();
mTitleFilterList.add(filter);
}
case MODE_TAG -> {
filter.text = filter.text.toLowerCase();
mTagFilterList.add(filter);
}
case MODE_TAG_NAMESPACE -> {
filter.text = filter.text.toLowerCase();
mTagNamespaceFilterList.add(filter);
}
case MODE_UPLOADER -> mUploaderFilterList.add(filter);
case MODE_COMMENTER -> mCommenterFilterList.add(filter);
case MODE_COMMENT -> mCommentFilterList.add(filter);
default -> Log.d(TAG, "Unknown mode: " + filter.mode);
}
return true;
}
public synchronized void triggerFilter(Filter filter) {
EhDB.triggerFilter(filter);
}
public synchronized void deleteFilter(Filter filter) {
EhDB.deleteFilter(filter);
switch (filter.mode) {
case MODE_TITLE -> mTitleFilterList.remove(filter);
case MODE_TAG -> mTagFilterList.remove(filter);
case MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter);
case MODE_UPLOADER -> mUploaderFilterList.remove(filter);
case MODE_COMMENTER -> mCommenterFilterList.remove(filter);
case MODE_COMMENT -> mCommentFilterList.remove(filter);
default -> Log.d(TAG, "Unknown mode: " + filter.mode);
}
}
public synchronized boolean needTags() {
return 0 != mTagFilterList.size() || 0 != mTagNamespaceFilterList.size();
}
public synchronized boolean filterTitle(GalleryInfo info) {
if (null == info) {
return false;
}
// Title
String title = info.title;
List<Filter> filters = mTitleFilterList;
if (null != title && filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && title.toLowerCase().contains(filters.get(i).text)) {
return false;
}
}
}
return true;
}
public synchronized boolean filterUploader(GalleryInfo info) {
if (null == info) {
return false;
}
// Uploader
String uploader = info.uploader;
List<Filter> filters = mUploaderFilterList;
if (null != uploader && filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && uploader.equals(filters.get(i).text)) {
return false;
}
}
}
return true;
}
private boolean matchTag(String tag, String filter) {
if (null == tag || null == filter) {
return false;
}
String tagNamespace;
String tagName;
String filterNamespace;
String filterName;
int 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);
}
if (null != tagNamespace && null != filterNamespace &&
!tagNamespace.equals(filterNamespace)) {
return false;
}
return tagName.equals(filterName);
}
public synchronized boolean filterTag(GalleryInfo info) {
if (null == info) {
return false;
}
// Tag
String[] tags = info.simpleTags;
List<Filter> filters = mTagFilterList;
if (null != tags && filters.size() > 0) {
for (String tag : tags) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && matchTag(tag, filters.get(i).text)) {
return false;
}
}
}
}
return true;
}
private boolean matchTagNamespace(String tag, String filter) {
if (null == tag || null == filter) {
return false;
}
String tagNamespace;
int index = tag.indexOf(':');
if (index >= 0) {
tagNamespace = tag.substring(0, index);
return tagNamespace.equals(filter);
} else {
return false;
}
}
public synchronized boolean filterTagNamespace(GalleryInfo info) {
if (null == info) {
return false;
}
String[] tags = info.simpleTags;
List<Filter> filters = mTagNamespaceFilterList;
if (null != tags && filters.size() > 0) {
for (String tag : tags) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && matchTagNamespace(tag, filters.get(i).text)) {
return false;
}
}
}
}
return true;
}
public synchronized boolean filterCommenter(String commenter) {
if (null == commenter) {
return false;
}
List<Filter> filters = mCommenterFilterList;
if (filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && commenter.equals(filters.get(i).text)) {
return false;
}
}
}
return true;
}
public synchronized boolean filterComment(String comment) {
if (null == comment) {
return false;
}
List<Filter> filters = mCommentFilterList;
if (filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable) {
Pattern p = Pattern.compile(filters.get(i).text);
Matcher m = p.matcher(comment);
if (m.find()) return false;
}
}
}
return true;
}
}
@@ -0,0 +1,315 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client
import android.util.Log
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.Filter
import java.util.Locale
import java.util.regex.Pattern
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()
const val MODE_TITLE = 0
const val MODE_UPLOADER = 1
const val MODE_TAG = 2
const val MODE_TAG_NAMESPACE = 3
const val MODE_COMMENTER = 4
const val MODE_COMMENT = 5
private val TAG = EhFilter::class.java.simpleName
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)
}
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
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTitleFilterList.add(filter)
}
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)
}
return true
}
@Synchronized
fun triggerFilter(filter: Filter?) {
EhDB.triggerFilter(filter)
}
@Synchronized
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)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
}
@Synchronized
fun needTags(): Boolean {
return 0 != mTagFilterList.size || 0 != mTagNamespaceFilterList.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
}
@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
}
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
) {
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
}
}
@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
}
@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
}
@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
}
}
@@ -31,7 +31,7 @@ class EhRequest {
get() = job?.isActive ?: false
var method = 0
private set
var args: Array<out Any>? = null
var args: Array<out Any?>? = null
private set
var callback: EhClient.Callback<Any?>? = null
private set
@@ -41,7 +41,7 @@ class EhRequest {
return this
}
fun setArgs(vararg args: Any): EhRequest {
fun setArgs(vararg args: Any?): EhRequest {
this.args = args
return this
}
@@ -62,7 +62,7 @@ class EhRequest {
}
fun enqueue(fragment: Fragment) {
enqueue(fragment.lifecycleScope)
enqueue(fragment.viewLifecycleOwner.lifecycleScope)
}
fun enqueue(activity: Activity) {
@@ -1,75 +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.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.data.ListUrlBuilder;
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser;
import com.hippo.ehviewer.client.parser.GalleryListUrlParser;
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
import com.hippo.ehviewer.ui.scene.GalleryDetailScene;
import com.hippo.ehviewer.ui.scene.GalleryListScene;
import com.hippo.ehviewer.ui.scene.ProgressScene;
import com.hippo.scene.Announcer;
public class EhUrlOpener {
private static final String TAG = EhUrlOpener.class.getSimpleName();
@Nullable
public static Announcer parseUrl(String url) {
if (TextUtils.isEmpty(url)) {
return null;
}
ListUrlBuilder listUrlBuilder = GalleryListUrlParser.parse(url);
if (listUrlBuilder != null) {
Bundle args = new Bundle();
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_LIST_URL_BUILDER);
args.putParcelable(GalleryListScene.KEY_LIST_URL_BUILDER, listUrlBuilder);
return new Announcer(GalleryListScene.class).setArgs(args);
}
GalleryDetailUrlParser.Result result1 = GalleryDetailUrlParser.parse(url);
if (result1 != null) {
Bundle args = new Bundle();
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN);
args.putLong(GalleryDetailScene.KEY_GID, result1.gid);
args.putString(GalleryDetailScene.KEY_TOKEN, result1.token);
return new Announcer(GalleryDetailScene.class).setArgs(args);
}
GalleryPageUrlParser.Result result2 = GalleryPageUrlParser.parse(url);
if (result2 != null) {
Bundle args = new Bundle();
args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN);
args.putLong(ProgressScene.KEY_GID, result2.gid);
args.putString(ProgressScene.KEY_PTOKEN, result2.pToken);
args.putInt(ProgressScene.KEY_PAGE, result2.page);
return new Announcer(ProgressScene.class).setArgs(args);
}
Log.i(TAG, "Can't parse url: " + url);
return null;
}
}
@@ -133,9 +133,9 @@ public class EhUtils {
public static String getSuitableTitle(GalleryInfo gi) {
if (Settings.getShowJpnTitle()) {
return TextUtils.isEmpty(gi.titleJpn) ? gi.title : gi.titleJpn;
return TextUtils.isEmpty(gi.getTitleJpn()) ? gi.getTitle() : gi.getTitleJpn();
} else {
return TextUtils.isEmpty(gi.title) ? gi.titleJpn : gi.title;
return TextUtils.isEmpty(gi.getTitle()) ? gi.getTitleJpn() : gi.getTitle();
}
}
@@ -0,0 +1,25 @@
package com.hippo.ehviewer.client.data
interface AbstractGalleryInfo {
var gid: Long
var token: String?
var title: String?
var titleJpn: String?
var thumb: String?
var category: Int
var posted: String?
var uploader: String?
var disowned: Boolean
var rating: Float
var rated: Boolean
var simpleTags: Array<String>?
var pages: Int
var thumbWidth: Int
var thumbHeight: Int
var spanSize: Int
var spanIndex: Int
var spanGroupIndex: Int
var simpleLanguage: String?
var favoriteSlot: Int
var favoriteName: String?
}
@@ -0,0 +1,89 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
import androidx.room.ColumnInfo
import androidx.room.Ignore
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
open class BaseGalleryInfo(
@PrimaryKey
@ColumnInfo(name = "GID")
override var gid: Long = 0,
@ColumnInfo(name = "TOKEN")
override var token: String? = null,
@ColumnInfo(name = "TITLE")
override var title: String? = null,
@ColumnInfo(name = "TITLE_JPN")
override var titleJpn: String? = null,
@ColumnInfo(name = "THUMB")
override var thumb: String? = null,
@ColumnInfo(name = "CATEGORY")
override var category: Int = 0,
@ColumnInfo(name = "POSTED")
override var posted: String? = null,
@ColumnInfo(name = "UPLOADER")
override var uploader: String? = null,
@Ignore
override var disowned: Boolean = false,
@ColumnInfo(name = "RATING")
override var rating: Float = 0f,
@Ignore
override var rated: Boolean = false,
@Ignore
override var simpleTags: Array<String>? = null,
@Ignore
override var pages: Int = 0,
@Ignore
override var thumbWidth: Int = 0,
@Ignore
override var thumbHeight: Int = 0,
@Ignore
override var spanSize: Int = 0,
@Ignore
override var spanIndex: Int = 0,
@Ignore
override var spanGroupIndex: Int = 0,
@ColumnInfo(name = "SIMPLE_LANGUAGE")
override var simpleLanguage: String? = null,
@Ignore
override var favoriteSlot: Int = -2,
@Ignore
override var favoriteName: String? = null
) : GalleryInfo
@@ -17,8 +17,11 @@ package com.hippo.ehviewer.client.data
import kotlinx.parcelize.Parcelize
@Parcelize // This is wrong Parcelize usage, TODO: use delegate and move galleryinfo to primary constructor
@Parcelize
class GalleryDetail(
@JvmField
val galleryInfo: GalleryInfo = BaseGalleryInfo(),
@JvmField
var apiUid: Long = -1L,
@@ -69,4 +72,4 @@ class GalleryDetail(
@JvmField
var previewSet: PreviewSet? = null,
) : GalleryInfo()
) : AbstractGalleryInfo by galleryInfo, GalleryInfo
@@ -16,102 +16,12 @@
package com.hippo.ehviewer.client.data
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Ignore
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
import java.util.regex.Pattern
@Parcelize
open class GalleryInfo(
@JvmField
@PrimaryKey
@ColumnInfo(name = "GID")
var gid: Long = 0,
@JvmField
@ColumnInfo(name = "TOKEN")
var token: String? = null,
@JvmField
@ColumnInfo(name = "TITLE")
var title: String? = null,
@JvmField
@ColumnInfo(name = "TITLE_JPN")
var titleJpn: String? = null,
@JvmField
@ColumnInfo(name = "THUMB")
var thumb: String? = null,
@JvmField
@ColumnInfo(name = "CATEGORY")
var category: Int = 0,
@JvmField
@ColumnInfo(name = "POSTED")
var posted: String? = null,
@JvmField
@ColumnInfo(name = "UPLOADER")
var uploader: String? = null,
@JvmField
@Ignore
var disowned: Boolean = false,
@JvmField
@ColumnInfo(name = "RATING")
var rating: Float = 0f,
@JvmField
@Ignore
var rated: Boolean = false,
@JvmField
@Ignore
var simpleTags: Array<String>? = null,
@JvmField
@Ignore
var pages: Int = 0,
@JvmField
@Ignore
var thumbWidth: Int = 0,
@JvmField
@Ignore
var thumbHeight: Int = 0,
@Ignore
var spanSize: Int = 0,
@Ignore
var spanIndex: Int = 0,
@Ignore
var spanGroupIndex: Int = 0,
/**
* language from title
*/
@JvmField
@ColumnInfo(name = "SIMPLE_LANGUAGE")
var simpleLanguage: String? = null,
@JvmField
@Ignore
var favoriteSlot: Int = -2,
@JvmField
@Ignore
var favoriteName: String? = null
) : Parcelable {
interface GalleryInfo : AbstractGalleryInfo, Parcelable {
fun generateSLang() {
simpleLanguage = simpleTags?.let { generateSLangFromTags(it) } ?: title?.let { generateSLangFromTitle(it) }
simpleLanguage = simpleTags?.let { generateSLangFromTags(it) }
?: title?.let { generateSLangFromTitle(it) }
}
private fun generateSLangFromTags(simpleTags: Array<String>): String? {
@@ -154,7 +64,6 @@ open class GalleryInfo(
const val S_LANG_TH = "TH"
const val S_LANG_VI = "VI"
@JvmField
val S_LANGS = arrayOf(
S_LANG_EN,
S_LANG_ZH,
@@ -32,7 +32,7 @@ import java.io.UnsupportedEncodingException
import java.net.URLDecoder
@Parcelize
open class ListUrlBuilder(
data class ListUrlBuilder(
@get:Mode @Mode var mode: Int = MODE_NORMAL,
private var mPrev: String? = null,
private var mNext: String? = null,
@@ -48,7 +48,7 @@ open class ListUrlBuilder(
var isUseSimilarityScan: Boolean = false,
var isOnlySearchCovers: Boolean = false,
var isShowExpunged: Boolean = false,
) : Cloneable, Parcelable {
) : Parcelable {
fun reset() {
mode = MODE_NORMAL
@@ -83,29 +83,6 @@ open class ListUrlBuilder(
mKeyword = keyword
}
/**
* Make them the same
*
* @param lub The template
*/
fun set(lub: ListUrlBuilder) {
mode = lub.mode
mPrev = lub.mPrev
mNext = lub.mNext
mJumpTo = lub.mJumpTo
this.category = lub.category
mKeyword = lub.mKeyword
advanceSearch = lub.advanceSearch
minRating = lub.minRating
pageFrom = lub.pageFrom
pageTo = lub.pageTo
imagePath = lub.imagePath
isUseSimilarityScan = lub.isUseSimilarityScan
isOnlySearchCovers = lub.isOnlySearchCovers
isShowExpunged = lub.isShowExpunged
mSHash = lub.mSHash
}
fun set(q: QuickSearch) {
mode = q.mode
this.category = q.category
@@ -39,13 +39,13 @@ public class GalleryApiParser {
if (gi == null) {
continue;
}
gi.title = ParserUtils.trim(g.getString("title"));
gi.titleJpn = ParserUtils.trim(g.getString("title_jpn"));
gi.category = EhUtils.getCategory(g.getString("category"));
gi.thumb = EhUtils.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);
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();
@@ -53,8 +53,8 @@ public class GalleryApiParser {
for (int j = 0; j < tagLength; j++) {
tags[j] = tagJa.getString(j);
}
gi.simpleTags = tags;
gi.pages = NumberUtils.parseIntSafely(g.getString("filecount"), 0);
gi.setSimpleTags(tags);
gi.setPages(NumberUtils.parseIntSafely(g.getString("filecount"), 0));
gi.generateSLang();
}
}
@@ -62,7 +62,7 @@ public class GalleryApiParser {
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.gid == gid) {
if (gi.getGid() == gid) {
return gi;
}
}
@@ -22,6 +22,7 @@ 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;
@@ -124,7 +125,7 @@ public class GalleryDetailParser {
// Generate simpleLanguage for local favorites
for (int i = 0; i < LANGUAGES.length; i++) {
if (galleryDetail.language.equals(LANGUAGES[i])) {
galleryDetail.simpleLanguage = GalleryInfo.S_LANGS[i];
galleryDetail.setSimpleLanguage(GalleryInfo.Companion.getS_LANGS()[i]);
break;
}
}
@@ -136,14 +137,14 @@ public class GalleryDetailParser {
private static void parseDetail(GalleryDetail gd, Document d, String body) throws ParseException {
Matcher matcher = PATTERN_DETAIL.matcher(body);
if (matcher.find()) {
gd.gid = NumberUtils.parseLongSafely(matcher.group(1), -1L);
gd.token = matcher.group(2);
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.gid == -1L) {
if (gd.getGid() == -1L) {
throw new ParseException("Can't parse gallery detail", body);
}
@@ -169,26 +170,26 @@ public class GalleryDetailParser {
// Thumb url
Element gd1 = gm.getElementById("gd1");
try {
gd.thumb = parseCoverStyle(StringUtils.trim(gd1.child(0).attr("style")));
gd.setThumb(parseCoverStyle(StringUtils.trim(gd1.child(0).attr("style"))));
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
gd.thumb = "";
gd.setThumb("");
}
// Title
Element gn = gm.getElementById("gn");
if (null != gn) {
gd.title = StringUtils.trim(gn.text());
gd.setTitle(StringUtils.trim(gn.text()));
} else {
gd.title = "";
gd.setTitle("");
}
// Jpn title
Element gj = gm.getElementById("gj");
if (null != gj) {
gd.titleJpn = StringUtils.trim(gj.text());
gd.setTitleJpn(StringUtils.trim(gj.text()));
} else {
gd.titleJpn = "";
gd.setTitleJpn("");
}
// Category
@@ -198,27 +199,27 @@ public class GalleryDetailParser {
if (ce == null) {
ce = JsoupUtils.getElementByClass(gdc, "cs");
}
gd.category = EhUtils.getCategory(ce.text());
gd.setCategory(EhUtils.getCategory(ce.text()));
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
gd.category = EhUtils.UNKNOWN;
gd.setCategory(EhUtils.UNKNOWN);
}
// Uploader
Element gdn = gm.getElementById("gdn");
if (null != gdn) {
gd.disowned = gdn.attr("style").contains("opacity:0.5");
gd.uploader = StringUtils.trim(gdn.text());
gd.setDisowned(gdn.attr("style").contains("opacity:0.5"));
gd.setUploader(StringUtils.trim(gdn.text()));
} else {
gd.uploader = "";
gd.setUploader("");
}
Element gdd = gm.getElementById("gdd");
gd.posted = "";
gd.setPosted("");
gd.parent = "";
gd.visible = "";
gd.size = "";
gd.pages = 0;
gd.setPages(0);
gd.favoriteCount = 0;
Elements es = gdd.child(0).child(0).children();
for (int i = 0, n = es.size(); i < n; i++) {
@@ -239,17 +240,17 @@ public class GalleryDetailParser {
if (null != rating_label) {
String ratingStr = StringUtils.trim(rating_label.text());
if ("Not Yet Rated".equals(ratingStr)) {
gd.rating = -1.0f;
gd.setRating(-1.0f);
} else {
int index = ratingStr.indexOf(' ');
if (index == -1 || index >= ratingStr.length()) {
gd.rating = 0f;
gd.setRating(0f);
} else {
gd.rating = NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f);
gd.setRating(NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f));
}
}
} else {
gd.rating = -1.0f;
gd.setRating(-1.0f);
}
// isFavorited
@@ -258,19 +259,19 @@ public class GalleryDetailParser {
if (gdf != null) {
final String favoriteName = StringUtils.trim(gdf.text());
if (favoriteName.equals("Add to Favorites")) {
gd.favoriteName = null;
gd.setFavoriteName(null);
} else {
gd.isFavorited = true;
gd.favoriteName = StringUtils.trim(gdf.text());
gd.setFavoriteName(StringUtils.trim(gdf.text()));
matcher = PATTERN_FAVORITE_SLOT.matcher(body);
if (matcher.find()) {
gd.favoriteSlot = (NumberUtils.parseIntSafely(matcher.group(1), 2) - 2) / 19;
gd.setFavoriteSlot((NumberUtils.parseIntSafely(matcher.group(1), 2) - 2) / 19);
}
}
}
if (gd.favoriteSlot == -2 && EhDB.containLocalFavorites(gd.gid)) {
gd.favoriteSlot = -1;
if (gd.getFavoriteSlot() == -2 && EhDB.containLocalFavorites(gd.getGid())) {
gd.setFavoriteSlot(-1);
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
@@ -289,13 +290,13 @@ public class GalleryDetailParser {
Elements elements = gnd.select("a");
for (int i = 0; i < elements.size(); i++) {
Element element = elements.get(i);
GalleryInfo gi = new GalleryInfo();
GalleryInfo gi = new BaseGalleryInfo();
GalleryDetailUrlParser.Result 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.get(i);
gi.setGid(result.gid);
gi.setToken(result.token);
gi.setTitle(StringUtils.trim(element.text()));
gi.setPosted(dates.get(i));
gd.newerVersions.add(gi);
}
}
@@ -324,7 +325,7 @@ public class GalleryDetailParser {
String key = StringUtils.trim(es.get(0).text());
String value = StringUtils.trim(es.get(1).ownText());
if (key.startsWith("Posted")) {
gd.posted = value;
gd.setPosted(value);
} else if (key.startsWith("Parent")) {
Element a = es.get(1).children().first();
if (a != null) {
@@ -339,9 +340,9 @@ public class GalleryDetailParser {
} else if (key.startsWith("Length")) {
int index = value.indexOf(' ');
if (index >= 0) {
gd.pages = NumberUtils.parseIntSafely(value.substring(0, index), 1);
gd.setPages(NumberUtils.parseIntSafely(value.substring(0, index), 1));
} else {
gd.pages = 1;
gd.setPages(1);
}
} else if (key.startsWith("Favorited")) {
switch (value) {
@@ -476,7 +477,7 @@ public class GalleryDetailParser {
comment.comment = JsoupUtils.getElementByClass(element, "c6").html();
// filter comment
if (!comment.uploader) {
EhFilter sEhFilter = EhFilter.getInstance();
EhFilter sEhFilter = EhFilter.INSTANCE;
if (!sEhFilter.filterCommenter(comment.user) || !sEhFilter.filterComment(comment.comment)) {
return null;
}
@@ -23,6 +23,7 @@ import androidx.annotation.NonNull;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.BaseGalleryInfo;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.util.ExceptionUtils;
@@ -110,7 +111,7 @@ public class GalleryListParser {
}
private static GalleryInfo parseGalleryInfo(Element e) {
GalleryInfo gi = new GalleryInfo();
GalleryInfo gi = new BaseGalleryInfo();
// Title, gid, token (required), tags
Element glname = JsoupUtils.getElementByClass(e, "glname");
@@ -125,8 +126,8 @@ public class GalleryListParser {
if (a != null) {
GalleryDetailUrlParser.Result result = GalleryDetailUrlParser.parse(a.attr("href"));
if (result != null) {
gi.gid = result.gid;
gi.token = result.token;
gi.setGid(result.gid);
gi.setToken(result.token);
}
}
@@ -136,9 +137,9 @@ public class GalleryListParser {
child = children.get(0);
children = child.children();
}
gi.title = child.text().trim();
gi.setTitle(child.text().trim());
}
if (gi.title == null) {
if (gi.getTitle() == null) {
return null;
}
@@ -149,17 +150,17 @@ public class GalleryListParser {
for (Element gt : gts) {
tags.add(gt.attr("title"));
}
gi.simpleTags = tags.toArray(new String[0]);
gi.setSimpleTags(tags.toArray(new String[0]));
}
// Category
gi.category = EhUtils.UNKNOWN;
gi.setCategory(EhUtils.UNKNOWN);
Element ce = JsoupUtils.getElementByClass(e, "cn");
if (ce == null) {
ce = JsoupUtils.getElementByClass(e, "cs");
}
if (ce != null) {
gi.category = EhUtils.getCategory(ce.text());
gi.setCategory(EhUtils.getCategory(ce.text()));
}
// Thumb
@@ -170,12 +171,12 @@ public class GalleryListParser {
// Thumb size
Matcher m = PATTERN_THUMB_SIZE.matcher(img.attr("style"));
if (m.find()) {
gi.thumbWidth = NumberUtils.parseIntSafely(m.group(2), 0);
gi.thumbHeight = NumberUtils.parseIntSafely(m.group(1), 0);
gi.setThumbWidth(NumberUtils.parseIntSafely(m.group(2), 0));
gi.setThumbHeight(NumberUtils.parseIntSafely(m.group(1), 0));
} else {
Log.w(TAG, "Can't parse gallery info thumb size");
gi.thumbWidth = 0;
gi.thumbHeight = 0;
gi.setThumbWidth(0);
gi.setThumbHeight(0);
}
// Thumb url
String url = img.attr("data-src");
@@ -185,7 +186,7 @@ public class GalleryListParser {
if (TextUtils.isEmpty(url)) {
url = null;
}
gi.thumb = EhUtils.handleThumbUrlResolution(url);
gi.setThumb(EhUtils.handleThumbUrlResolution(url));
}
// Pages
@@ -193,12 +194,12 @@ public class GalleryListParser {
if (div != null) {
Matcher matcher = PATTERN_PAGES.matcher(div.text());
if (matcher.find()) {
gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0);
gi.setPages(NumberUtils.parseIntSafely(matcher.group(1), 0));
}
}
}
// Try extended and thumbnail version
if (gi.thumb == null) {
if (gi.getThumb() == null) {
Element gl = JsoupUtils.getElementByClass(e, "gl1e");
if (gl == null) {
gl = JsoupUtils.getElementByClass(e, "gl3t");
@@ -209,35 +210,35 @@ public class GalleryListParser {
// Thumb size
Matcher m = PATTERN_THUMB_SIZE.matcher(img.attr("style"));
if (m.find()) {
gi.thumbWidth = NumberUtils.parseIntSafely(m.group(2), 0);
gi.thumbHeight = NumberUtils.parseIntSafely(m.group(1), 0);
gi.setThumbWidth(NumberUtils.parseIntSafely(m.group(2), 0));
gi.setThumbHeight(NumberUtils.parseIntSafely(m.group(1), 0));
} else {
Log.w(TAG, "Can't parse gallery info thumb size");
gi.thumbWidth = 0;
gi.thumbHeight = 0;
gi.setThumbWidth(0);
gi.setThumbHeight(0);
}
gi.thumb = EhUtils.handleThumbUrlResolution(img.attr("src"));
gi.setThumb(EhUtils.handleThumbUrlResolution(img.attr("src")));
}
}
}
// Posted
gi.favoriteSlot = -2;
Element posted = e.getElementById("posted_" + gi.gid);
gi.setFavoriteSlot(-2);
Element posted = e.getElementById("posted_" + gi.getGid());
if (posted != null) {
gi.posted = posted.text().trim();
gi.favoriteSlot = parseFavoriteSlot(posted.attr("style"));
gi.setPosted(posted.text().trim());
gi.setFavoriteSlot(parseFavoriteSlot(posted.attr("style")));
}
if (gi.favoriteSlot == -2) {
gi.favoriteSlot = EhDB.containLocalFavorites(gi.gid) ? -1 : -2;
if (gi.getFavoriteSlot() == -2) {
gi.setFavoriteSlot(EhDB.containLocalFavorites(gi.getGid()) ? -1 : -2);
}
// Rating
Element ir = JsoupUtils.getElementByClass(e, "ir");
if (ir != null) {
gi.rating = NumberUtils.parseFloatSafely(parseRating(ir.attr("style")), -1.0f);
gi.setRating(NumberUtils.parseFloatSafely(parseRating(ir.attr("style")), -1.0f));
// TODO The gallery may be rated even if it doesn't has one of these classes
gi.rated = ir.hasClass("irr") || ir.hasClass("irg") || ir.hasClass("irb");
gi.setRated(ir.hasClass("irr") || ir.hasClass("irg") || ir.hasClass("irb"));
}
// Uploader and pages
@@ -255,15 +256,15 @@ public class GalleryListParser {
if (children.size() > uploaderIndex) {
Element div = children.get(uploaderIndex);
if (div != null) {
gi.disowned = div.attr("style").contains("opacity:0.5");
gi.setDisowned(div.attr("style").contains("opacity:0.5"));
Element a = div.children().first();
gi.uploader = a == null ? div.text().trim() : a.text().trim();
gi.setUploader(a == null ? div.text().trim() : a.text().trim());
}
}
if (children.size() > pagesIndex) {
Matcher matcher = PATTERN_PAGES.matcher(children.get(pagesIndex).text());
if (matcher.find()) {
gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0);
gi.setPages(NumberUtils.parseIntSafely(matcher.group(1), 0));
}
}
}
@@ -274,7 +275,7 @@ public class GalleryListParser {
if (div != null) {
Matcher matcher = PATTERN_PAGES.matcher(div.text());
if (matcher.find()) {
gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0);
gi.setPages(NumberUtils.parseIntSafely(matcher.group(1), 0));
}
}
}
@@ -2,10 +2,10 @@ package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.BaseGalleryInfo
@Entity(tableName = "BOOKMARKS")
class BookmarkInfo : GalleryInfo() {
class BookmarkInfo : BaseGalleryInfo() {
@JvmField
@ColumnInfo(name = "PAGE")
var page = 0
@@ -3,10 +3,11 @@ package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
@Entity(tableName = "DOWNLOADS")
class DownloadInfo() : GalleryInfo() {
class DownloadInfo() : BaseGalleryInfo() {
@JvmField
@ColumnInfo(name = "STATE")
var state = 0
@@ -11,14 +11,20 @@ interface DownloadsDao : BasicDao<DownloadInfo> {
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME DESC")
override fun list(): List<DownloadInfo>
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME DESC LIMIT :limit OFFSET :offset")
fun list(offset: Int, limit: Int): List<DownloadInfo>
@Query("SELECT * FROM DOWNLOADS WHERE GID = :gid")
fun load(gid: Long): DownloadInfo?
@Update
fun update(downloadInfos: List<DownloadInfo>)
@Update
fun update(downloadInfo: DownloadInfo)
@Insert
override fun insert(downloadInfo: DownloadInfo): Long
override fun insert(t: DownloadInfo): Long
@Delete
fun delete(downloadInfo: DownloadInfo)
@@ -1,6 +1,8 @@
package com.hippo.ehviewer.dao
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(
@@ -18,3 +20,9 @@ abstract class EhDatabase : RoomDatabase() {
abstract fun localFavoritesDao(): LocalFavoritesDao
abstract fun quickSearchDao(): QuickSearchDao
}
fun buildMainDB(context: Context): EhDatabase {
// TODO: Remove allowMainThreadQueries
return Room.databaseBuilder(context, EhDatabase::class.java, "eh.db").allowMainThreadQueries()
.build()
}
@@ -2,10 +2,11 @@ package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
@Entity(tableName = "HISTORY")
class HistoryInfo() : GalleryInfo() {
class HistoryInfo() : BaseGalleryInfo() {
@JvmField
@ColumnInfo(name = "TIME")
var time: Long = 0
@@ -2,10 +2,11 @@ package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
@Entity(tableName = "LOCAL_FAVORITES")
class LocalFavoriteInfo() : GalleryInfo() {
class LocalFavoriteInfo() : BaseGalleryInfo() {
@JvmField
@ColumnInfo(name = "TIME")
var time: Long = 0
@@ -24,6 +24,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.data.BaseGalleryInfo;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.dao.DownloadLabel;
@@ -53,7 +54,7 @@ import java.util.Map;
public class DownloadManager implements SpiderQueen.OnSpiderListener {
private static final String TAG = DownloadManager.class.getSimpleName();
private static final Comparator<DownloadInfo> DATE_DESC_COMPARATOR = (lhs, rhs) -> (int) (rhs.time - lhs.time);
private static final Comparator<DownloadInfo> DATE_DESC_COMPARATOR = (lhs, rhs) -> lhs.time == rhs.time ? 0 : lhs.time - rhs.time > 0 ? -1 : 1;
// All download info list
private final LinkedList<DownloadInfo> mAllInfoList;
// All download info map
@@ -104,7 +105,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
DownloadInfo info = allInfoList.get(i);
// Add to all info map
allInfoMap.put(info.gid, info);
allInfoMap.put(info.getGid(), info);
// Add to each label list
LinkedList<DownloadInfo> list = getInfoListForLabel(info.label);
@@ -157,6 +158,11 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
return mLabelList;
}
@NonNull
public List<DownloadInfo> getAllDownloadInfoList() {
return mAllInfoList;
}
@NonNull
public List<DownloadInfo> getDefaultDownloadInfoList() {
return mDefaultInfoList;
@@ -232,13 +238,13 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
}
void startDownload(GalleryInfo galleryInfo, @Nullable String label) {
if (mCurrentTask != null && mCurrentTask.gid == galleryInfo.gid) {
if (mCurrentTask != null && mCurrentTask.getGid() == galleryInfo.getGid()) {
// It is current task
return;
}
// Check in download list
DownloadInfo info = mAllInfoMap.get(galleryInfo.gid);
DownloadInfo info = mAllInfoMap.get(galleryInfo.getGid());
if (info != null) { // Get it in download list
if (info.state != DownloadInfo.STATE_WAIT) {
// Set state DownloadInfo.STATE_WAIT
@@ -274,7 +280,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
// Add to all download list and map
mAllInfoList.addFirst(info);
mAllInfoMap.put(galleryInfo.gid, info);
mAllInfoMap.put(galleryInfo.getGid(), info);
// Add to wait list
mWaitList.add(info);
@@ -359,7 +365,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
public void addDownload(List<DownloadInfo> downloadInfoList, boolean notify) {
for (DownloadInfo info : downloadInfoList) {
if (containDownloadInfo(info.gid)) {
if (containDownloadInfo(info.getGid())) {
// Contain
return;
}
@@ -387,7 +393,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
// Add to all download list and map
mAllInfoList.add(info);
mAllInfoMap.put(info.gid, info);
mAllInfoMap.put(info.getGid(), info);
// Save to
EhDB.putDownloadInfo(info);
@@ -415,7 +421,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
}
public void addDownload(GalleryInfo galleryInfo, @Nullable String label) {
if (containDownloadInfo(galleryInfo.gid)) {
if (containDownloadInfo(galleryInfo.getGid())) {
// Contain
return;
}
@@ -436,7 +442,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
// Add to all download list and map
mAllInfoList.addFirst(info);
mAllInfoMap.put(galleryInfo.gid, info);
mAllInfoMap.put(galleryInfo.getGid(), info);
// Save to
EhDB.putDownloadInfo(info);
@@ -516,7 +522,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
// Remove all list and map
mAllInfoList.remove(info);
mAllInfoMap.remove(info.gid);
mAllInfoMap.remove(info.getGid());
// Remove label list
LinkedList<DownloadInfo> list = getInfoListForLabel(info.label);
@@ -552,7 +558,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
// Remove from all info map
mAllInfoList.remove(info);
mAllInfoMap.remove(info.gid);
mAllInfoMap.remove(info.getGid());
// Remove from label list
LinkedList<DownloadInfo> list = getInfoListForLabel(info.label);
@@ -577,18 +583,18 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
GalleryInfo galleryInfo = new GalleryInfo();
GalleryInfo galleryInfo = new BaseGalleryInfo();
for (DownloadInfo downloadInfo : list) {
galleryInfo.gid = downloadInfo.gid;
galleryInfo.token = downloadInfo.token;
galleryInfo.title = downloadInfo.title;
galleryInfo.thumb = downloadInfo.thumb;
galleryInfo.category = downloadInfo.category;
galleryInfo.posted = downloadInfo.posted;
galleryInfo.uploader = downloadInfo.uploader;
galleryInfo.rating = downloadInfo.rating;
galleryInfo.setGid(downloadInfo.getGid());
galleryInfo.setToken(downloadInfo.getToken());
galleryInfo.setTitle(downloadInfo.getTitle());
galleryInfo.setThumb(downloadInfo.getThumb());
galleryInfo.setCategory(downloadInfo.getCategory());
galleryInfo.setPosted(downloadInfo.getPosted());
galleryInfo.setUploader(downloadInfo.getUploader());
galleryInfo.setRating(downloadInfo.getRating());
UniFile downloadDir = SpiderDen.getGalleryDownloadDir(galleryInfo.gid);
UniFile downloadDir = SpiderDen.getGalleryDownloadDir(galleryInfo.getGid());
if (downloadDir == null) {
continue;
}
@@ -618,14 +624,14 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
// No ensureDownload
private DownloadInfo stopDownloadInternal(long gid) {
// Check current task
if (mCurrentTask != null && mCurrentTask.gid == gid) {
if (mCurrentTask != null && mCurrentTask.getGid() == gid) {
// Stop current
return stopCurrentDownloadInternal();
}
for (Iterator<DownloadInfo> iterator = mWaitList.iterator(); iterator.hasNext(); ) {
DownloadInfo info = iterator.next();
if (info.gid == gid) {
if (info.getGid() == gid) {
// Remove from wait list
iterator.remove();
// Update state
@@ -677,7 +683,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
}
} else {
// Check current task
if (mCurrentTask != null && gidList.contains(mCurrentTask.gid)) {
if (mCurrentTask != null && gidList.contains(mCurrentTask.getGid())) {
// Stop current
stopCurrentDownloadInternal();
}
@@ -685,7 +691,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
// Check all in wait list
for (Iterator<DownloadInfo> iterator = mWaitList.iterator(); iterator.hasNext(); ) {
DownloadInfo info = iterator.next();
if (gidList.contains(info.gid)) {
if (gidList.contains(info.getGid())) {
// Remove from wait list
iterator.remove();
// Update state
@@ -36,7 +36,6 @@ import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.scene.DownloadsScene
import com.hippo.scene.StageActivity
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.SimpleHandler
@@ -186,9 +185,8 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
val bundle = Bundle()
bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = StageActivity.ACTION_START_SCENE
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)
activityIntent.action = ACTION_START_DOWNLOADSCENE
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
val piActivity = PendingIntent.getActivity(
this@DownloadService, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
@@ -241,9 +239,8 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
val bundle = Bundle()
bundle.putLong(DownloadsScene.KEY_GID, info.gid)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = StageActivity.ACTION_START_SCENE
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)
activityIntent.action = ACTION_START_DOWNLOADSCENE
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
val piActivity = PendingIntent.getActivity(
this@DownloadService, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
@@ -516,6 +513,9 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
}
companion object {
const val ACTION_START_DOWNLOADSCENE = "start_download_scene"
const val ACTION_START_DOWNLOADSCENE_ARGS = "start_download_scene_args"
const val ACTION_START = "start"
const val ACTION_START_RANGE = "start_range"
const val ACTION_START_ALL = "start_all"
@@ -71,7 +71,7 @@ public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderLis
@NonNull
@Override
public String getImageFilename(int index) {
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.gid, mGalleryInfo.token, index + 1);
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
}
@NonNull
@@ -80,10 +80,10 @@ public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderLis
if (null != mSpiderQueen) {
String extension = mSpiderQueen.getExtension(index);
if (extension != null) {
return String.format(Locale.US, "%d-%s-%08d.%s", mGalleryInfo.gid, mGalleryInfo.token, index + 1, extension);
return String.format(Locale.US, "%d-%s-%08d.%s", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1, extension);
}
}
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.gid, mGalleryInfo.token, index + 1);
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
}
@Override
@@ -20,13 +20,12 @@ import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine.fillGalleryListByApi
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.spider.SpiderInfo
import com.hippo.ehviewer.spider.SpiderQueen
@@ -35,7 +34,6 @@ import com.hippo.unifile.UniFile
import com.hippo.util.ExceptionUtils
import com.hippo.yorozuya.IOUtils
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import java.io.IOException
import java.io.InputStream
import java.util.Collections
@@ -49,7 +47,6 @@ class RestoreDownloadPreference constructor(
private class RestoreTask(context: Context) : Task(context) {
private val mManager: DownloadManager = downloadManager
private val mHttpClient: OkHttpClient = okHttpClient
private var restoreDirCount = 0
private fun getRestoreItem(file: UniFile?): RestoreItem? {
if (null == file || !file.isDirectory) {
@@ -99,7 +96,6 @@ class RestoreDownloadPreference constructor(
} else try {
runBlocking {
fillGalleryListByApi(
mHttpClient,
ArrayList(restoreItemList),
EhUrl.getReferer()
)
@@ -163,7 +159,7 @@ class RestoreDownloadPreference constructor(
}
}
private class RestoreItem : GalleryInfo() {
private class RestoreItem : BaseGalleryInfo() {
var dirname: String? = null
}
}
@@ -56,8 +56,8 @@ public final class SpiderDen {
public SpiderDen(GalleryInfo galleryInfo) {
mGalleryInfo = galleryInfo;
mGid = galleryInfo.gid;
mDownloadDir = getGalleryDownloadDir(galleryInfo.gid);
mGid = galleryInfo.getGid();
mDownloadDir = getGalleryDownloadDir(galleryInfo.getGid());
}
public static void initialize(Context context) {
@@ -179,10 +179,10 @@ public final class SpiderQueen implements Runnable {
public static SpiderQueen obtainSpiderQueen(@NonNull GalleryInfo galleryInfo, @Mode int mode) {
OSUtils.checkMainLoop();
SpiderQueen queen = sQueenMap.get(galleryInfo.gid);
SpiderQueen queen = sQueenMap.get(galleryInfo.getGid());
if (queen == null) {
queen = new SpiderQueen(galleryInfo);
sQueenMap.put(galleryInfo.gid, queen);
sQueenMap.put(galleryInfo.getGid(), queen);
// Set mode
queen.setMode(mode);
queen.start();
@@ -203,7 +203,7 @@ public final class SpiderQueen implements Runnable {
if (queen.mReadReference == 0 && queen.mDownloadReference == 0) {
// Stop and remove if there is no reference
queen.stop();
sQueenMap.remove(queen.mGalleryInfo.gid);
sQueenMap.remove(queen.mGalleryInfo.getGid());
}
}
@@ -697,19 +697,19 @@ public final class SpiderQueen implements Runnable {
if (downloadDir != null) {
UniFile file = downloadDir.findFile(SPIDER_INFO_FILENAME);
spiderInfo = SpiderInfo.read(file);
if (spiderInfo != null && spiderInfo.gid == mGalleryInfo.gid &&
spiderInfo.token.equals(mGalleryInfo.token)) {
if (spiderInfo != null && spiderInfo.gid == mGalleryInfo.getGid() &&
spiderInfo.token.equals(mGalleryInfo.getToken())) {
return spiderInfo;
}
}
// Read from cache
var entry = mSpiderInfoCache.get(Long.toString(mGalleryInfo.gid));
var entry = mSpiderInfoCache.get(Long.toString(mGalleryInfo.getGid()));
if (entry == null) return null;
try (entry; InputStream is = new FileInputStream(entry.getData().toFile())) {
spiderInfo = SpiderInfo.read(is);
if (spiderInfo != null && spiderInfo.gid == mGalleryInfo.gid &&
spiderInfo.token.equals(mGalleryInfo.token)) {
if (spiderInfo != null && spiderInfo.gid == mGalleryInfo.getGid() &&
spiderInfo.token.equals(mGalleryInfo.getToken())) {
return spiderInfo;
}
} catch (IOException e) {
@@ -744,11 +744,11 @@ public final class SpiderQueen implements Runnable {
private SpiderInfo readSpiderInfoFromInternet() {
SpiderInfo spiderInfo = new SpiderInfo();
spiderInfo.gid = mGalleryInfo.gid;
spiderInfo.token = mGalleryInfo.token;
spiderInfo.gid = mGalleryInfo.getGid();
spiderInfo.token = mGalleryInfo.getToken();
Request request = new EhRequestBuilder(EhUrl.getGalleryDetailUrl(
mGalleryInfo.gid, mGalleryInfo.token, 0, false), EhUrl.getReferer()).build();
mGalleryInfo.getGid(), mGalleryInfo.getToken(), 0, false), EhUrl.getReferer()).build();
try (Response response = mHttpClient.newCall(request).execute()) {
String body = response.body().string();
@@ -769,7 +769,7 @@ public final class SpiderQueen implements Runnable {
}
String url = EhUrl.getGalleryMultiPageViewerUrl(
mGalleryInfo.gid, mGalleryInfo.token);
mGalleryInfo.getGid(), mGalleryInfo.getToken());
if (DEBUG_PTOKEN) {
Log.d(TAG, "getPTokenFromMultiPageViewer index " + index + ", url " + url);
}
@@ -815,7 +815,7 @@ public final class SpiderQueen implements Runnable {
}
String url = EhUrl.getGalleryDetailUrl(
mGalleryInfo.gid, mGalleryInfo.token, previewIndex, false);
mGalleryInfo.getGid(), mGalleryInfo.getToken(), previewIndex, false);
String referer = EhUrl.getReferer();
if (DEBUG_PTOKEN) {
Log.d(TAG, "index " + index + ", previewIndex " + previewIndex +
@@ -861,7 +861,7 @@ public final class SpiderQueen implements Runnable {
}
// Write to cache
var editor = mSpiderInfoCache.edit(Long.toString(mGalleryInfo.gid));
var editor = mSpiderInfoCache.edit(Long.toString(mGalleryInfo.getGid()));
if (editor == null) return;
try (OutputStream os = new FileOutputStream(editor.getData().toFile())) {
spiderInfo.write(os);
@@ -1097,7 +1097,7 @@ public final class SpiderQueen implements Runnable {
private final long mGid;
public SpiderWorker() {
mGid = mGalleryInfo.gid;
mGid = mGalleryInfo.getGid();
}
private String getPageUrl(long gid, int index, String pToken,
@@ -1115,7 +1115,7 @@ public final class SpiderQueen implements Runnable {
}
private GalleryPageParser.Result fetchPageResultFromHtml(int index, String pageUrl) throws Throwable {
GalleryPageParser.Result result = EhEngine.getGalleryPage(mHttpClient, pageUrl, mGalleryInfo.gid, mGalleryInfo.token);
GalleryPageParser.Result result = EhEngine.getGalleryPage(pageUrl, mGalleryInfo.getGid(), mGalleryInfo.getToken());
if (StringUtils.endsWith(result.imageUrl, URL_509_SUFFIX_ARRAY)) {
// Get 509
// Notify listeners
@@ -1127,7 +1127,7 @@ public final class SpiderQueen implements Runnable {
}
private GalleryPageApiParser.Result fetchPageResultFromApi(long gid, int index, String pToken, String showKey, String previousPToken) throws Throwable {
GalleryPageApiParser.Result result = EhEngine.getGalleryPageApi(mHttpClient, gid, index, pToken, showKey, previousPToken);
GalleryPageApiParser.Result result = EhEngine.getGalleryPageApi(gid, index, pToken, showKey, previousPToken);
if (StringUtils.endsWith(result.imageUrl, URL_509_SUFFIX_ARRAY)) {
// Get 509
// Notify listeners
@@ -1395,11 +1395,13 @@ public final class SpiderQueen implements Runnable {
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.sleep(0); // to make allow to catch InterruptedException
return true;
} catch (IOException e) {
e.printStackTrace();
error = GetText.getString(R.string.error_socket);
forceHtml = true;
} catch (InterruptedException ignored) {
} finally {
IOUtils.closeQuietly(is);
@@ -56,7 +56,7 @@ public final class CommonOperations {
EhClient client = EhClient.INSTANCE;
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_ADD_FAVORITES);
request.setArgs(galleryInfo.gid, galleryInfo.token, slot, "");
request.setArgs(galleryInfo.getGid(), galleryInfo.getToken(), slot, "");
request.setCallback(listener);
request.enqueue(activity);
} else {
@@ -99,11 +99,11 @@ public final class CommonOperations {
public static void removeFromFavorites(Activity activity, GalleryInfo galleryInfo,
final EhClient.Callback<Void> listener) {
EhDB.removeLocalFavorites(galleryInfo.gid);
EhDB.removeLocalFavorites(galleryInfo.getGid());
EhClient client = EhClient.INSTANCE;
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_ADD_FAVORITES);
request.setArgs(galleryInfo.gid, galleryInfo.token, -1, "");
request.setArgs(galleryInfo.getGid(), galleryInfo.getToken(), -1, "");
request.setCallback(new DelegateFavoriteCallback(listener, galleryInfo, null, -2));
request.enqueue(activity);
}
@@ -125,8 +125,8 @@ public final class CommonOperations {
LongList toStart = new LongList();
List<GalleryInfo> toAdd = new ArrayList<>();
for (GalleryInfo gi : galleryInfos) {
if (dm.containDownloadInfo(gi.gid)) {
toStart.add(gi.gid);
if (dm.containDownloadInfo(gi.getGid())) {
toStart.add(gi.getGid());
} else {
toAdd.add(gi);
}
@@ -259,10 +259,10 @@ public final class CommonOperations {
@Override
public void onSuccess(Void result) {
info.favoriteName = newFavoriteName;
info.favoriteSlot = slot;
info.setFavoriteName(newFavoriteName);
info.setFavoriteSlot(slot);
delegate.onSuccess(result);
EhApplication.getFavouriteStatusRouter().modifyFavourites(info.gid, slot);
EhApplication.getFavouriteStatusRouter().modifyFavourites(info.getGid(), slot);
}
@Override
@@ -29,7 +29,7 @@ import com.hippo.ehviewer.UrlOpener
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.util.tellClipboard
import com.hippo.util.tellClipboardWithToast
class GalleryInfoBottomSheet(detail: GalleryDetail) : BottomSheetDialogFragment() {
private var mKeys: ArrayList<String> = arrayListOf()
@@ -117,7 +117,7 @@ class GalleryInfoBottomSheet(detail: GalleryDetail) : BottomSheetDialogFragment(
if (position == INDEX_PARENT) {
UrlOpener.openUrl(context, mValues[position], true)
} else {
requireActivity() tellClipboard mValues[position]
requireActivity() tellClipboardWithToast mValues[position]
if (position == INDEX_URL) {
// Save it to avoid detect the gallery
Settings.putClipboardTextHashCode(mValues[position].hashCode())
@@ -0,0 +1,57 @@
package com.hippo.ehviewer.ui
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.navigation.ActivityNavigator
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.hippo.ehviewer.ui.compose.CookieSignInScene
import com.hippo.ehviewer.ui.compose.SelectSiteScreen
import com.hippo.ehviewer.ui.compose.SignInScreen
import com.hippo.ehviewer.ui.compose.WebviewSignInScreen
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Mdc3Theme {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = SIGN_IN_ROUTE_NAME) {
composable(SIGN_IN_ROUTE_NAME) {
SignInScreen(navController)
}
composable(WEBVIEW_SIGN_IN_ROUTE_NAME) {
WebviewSignInScreen(navController)
}
composable(COOKIE_SIGN_IN_ROUTE_NAME) {
CookieSignInScene(navController)
}
composable(SELECT_SITE_ROUTE_NAME) {
SelectSiteScreen {
finish()
}
}
}
}
}
}
override fun finish() {
super.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}
companion object {
const val SIGN_IN_ROUTE_NAME = "SignIn"
const val WEBVIEW_SIGN_IN_ROUTE_NAME = "WebViewSignIn"
const val COOKIE_SIGN_IN_ROUTE_NAME = "CookieSignIn"
const val SELECT_SITE_ROUTE_NAME = "SelectSite"
}
}
@@ -16,6 +16,7 @@
package com.hippo.ehviewer.ui
import android.annotation.SuppressLint
import android.app.assist.AssistContent
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
@@ -26,113 +27,61 @@ import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.view.MenuItem
import android.text.TextUtils
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.view.GravityCompat
import androidx.customview.widget.Openable
import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import com.google.android.material.navigation.NavigationView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.onNavDestinationSelected2
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import com.hippo.app.BaseDialogBuilder
import com.hippo.app.EditTextDialogBuilder
import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUrlOpener
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser
import com.hippo.ehviewer.client.parser.GalleryListUrlParser
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.hippo.ehviewer.databinding.ActivityMainBinding
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.ehviewer.ui.scene.CookieSignInScene
import com.hippo.ehviewer.ui.scene.DownloadsScene
import com.hippo.ehviewer.ui.scene.FavoritesScene
import com.hippo.ehviewer.ui.scene.GalleryCommentsScene
import com.hippo.ehviewer.ui.scene.GalleryDetailScene
import com.hippo.ehviewer.ui.scene.GalleryListScene
import com.hippo.ehviewer.ui.scene.GalleryPreviewsScene
import com.hippo.ehviewer.ui.scene.HistoryScene
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.ehviewer.ui.scene.ProgressScene
import com.hippo.ehviewer.ui.scene.SelectSiteScene
import com.hippo.ehviewer.ui.scene.SignInScene
import com.hippo.ehviewer.ui.scene.SolidScene
import com.hippo.ehviewer.ui.scene.WebViewSignInScene
import com.hippo.ehviewer.widget.EhStageLayout
import com.hippo.io.UniFileInputStreamPipe
import com.hippo.scene.Announcer
import com.hippo.scene.SceneFragment
import com.hippo.scene.StageActivity
import com.hippo.unifile.UniFile
import com.hippo.util.BitmapUtils
import com.hippo.util.addTextToClipboard
import com.hippo.util.getClipboardManager
import com.hippo.util.getUrlFromClipboard
import com.hippo.widget.DrawerView
import com.hippo.yorozuya.IOUtils
import com.hippo.yorozuya.SimpleHandler
import com.hippo.yorozuya.ViewUtils
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedListener {
private var settingsLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) refreshTopScene()
}
/*---------------
Whole life cycle
---------------*/
private var mDrawerLayout: DrawerLayout? = null
private var mStageLayout: EhStageLayout? = null
private var mNavView: NavigationView? = null
private var mRightDrawer: DrawerView? = null
private var mNavCheckedItem = 0
override fun getContainerViewId(): Int {
return R.id.fragment_container
}
override fun getLaunchAnnouncer(): Announcer {
return if (EhUtils.needSignedIn()) {
Announcer(SignInScene::class.java)
} else if (Settings.getSelectSite()) {
Announcer(SelectSiteScene::class.java)
} else {
val args = Bundle()
args.putString(
GalleryListScene.KEY_ACTION,
Settings.getLaunchPageGalleryListSceneAction()
)
Announcer(GalleryListScene::class.java).setArgs(args)
}
}
// Sometimes scene can't show directly
private fun processAnnouncer(announcer: Announcer): Announcer {
if (0 == sceneCount) {
if (EhUtils.needSignedIn()) {
val newArgs = Bundle()
newArgs.putString(SignInScene.KEY_TARGET_SCENE, announcer.clazz.name)
newArgs.putBundle(SignInScene.KEY_TARGET_ARGS, announcer.args)
return Announcer(SignInScene::class.java).setArgs(newArgs)
} else if (Settings.getSelectSite()) {
val newArgs = Bundle()
newArgs.putString(SelectSiteScene.KEY_TARGET_SCENE, announcer.clazz.name)
newArgs.putBundle(SelectSiteScene.KEY_TARGET_ARGS, announcer.args)
return Announcer(SelectSiteScene::class.java).setArgs(newArgs)
}
}
return announcer
}
class MainActivity : EhActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private fun saveImageToTempFile(file: UniFile?): File? {
file ?: return null
@@ -166,16 +115,68 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
}
}
private fun handleIntent(intent: Intent?): Boolean {
if (intent == null) {
return false
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
lifecycleScope.launchUI {
if (!handleIntent(intent)) {
if (intent != null && Intent.ACTION_VIEW == intent.action) {
if (intent.data != null) {
val url = intent.data.toString()
EditTextDialogBuilder(this@MainActivity, url, "")
.setTitle(R.string.error_cannot_parse_the_url)
.setPositiveButton(android.R.string.copy) { _: DialogInterface?, _: Int ->
this@MainActivity.addTextToClipboard(
url,
false
)
}
.show()
}
}
}
}
}
private fun handleIntent(intent: Intent?): Boolean {
intent ?: return false
val action = intent.action
if (Intent.ACTION_VIEW == action) {
val uri = intent.data ?: return false
val announcer = EhUrlOpener.parseUrl(uri.toString())
if (announcer != null) {
startScene(processAnnouncer(announcer))
val url = uri.toString()
if (TextUtils.isEmpty(url)) {
return false
}
val listUrlBuilder = GalleryListUrlParser.parse(url)
if (listUrlBuilder != null) {
val args = Bundle()
args.putString(
GalleryListScene.KEY_ACTION,
GalleryListScene.ACTION_LIST_URL_BUILDER
)
args.putParcelable(GalleryListScene.KEY_LIST_URL_BUILDER, listUrlBuilder)
navController.navigate(R.id.galleryListScene, args)
return true
}
val result1 = GalleryDetailUrlParser.parse(url)
if (result1 != null) {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)
args.putLong(GalleryDetailScene.KEY_GID, result1.gid)
args.putString(GalleryDetailScene.KEY_TOKEN, result1.token)
navController.navigate(R.id.galleryDetailScene, args)
return true
}
val result2 = GalleryPageUrlParser.parse(url)
if (result2 != null) {
val args = Bundle()
args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN)
args.putLong(ProgressScene.KEY_GID, result2.gid)
args.putString(ProgressScene.KEY_PTOKEN, result2.pToken)
args.putInt(ProgressScene.KEY_PAGE, result2.page)
navController.navigate(R.id.progressScene, args)
return true
}
} else if (Intent.ACTION_SEND == action) {
@@ -183,7 +184,10 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
if ("text/plain" == type) {
val builder = ListUrlBuilder()
builder.keyword = intent.getStringExtra(Intent.EXTRA_TEXT)
startScene(processAnnouncer(GalleryListScene.getStartAnnouncer(builder)))
navController.navigate(
R.id.galleryListScene,
builder.toStartArgs()
)
return true
} else if (type != null && type.startsWith("image/")) {
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
@@ -195,69 +199,67 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
builder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
builder.imagePath = temp.path
builder.isUseSimilarityScan = true
startScene(processAnnouncer(GalleryListScene.getStartAnnouncer(builder)))
navController.navigate(
R.id.galleryListScene,
builder.toStartArgs()
)
return true
}
}
}
} else if (action == DownloadService.ACTION_START_DOWNLOADSCENE) {
val args = intent.getBundleExtra(DownloadService.ACTION_START_DOWNLOADSCENE_ARGS)
navController.navigate(R.id.nav_downloads, args)
}
return false
}
override fun onUnrecognizedIntent(intent: Intent?) {
val clazz = topSceneClass
if (clazz != null && SolidScene::class.java.isAssignableFrom(clazz)) {
// TODO the intent lost
return
}
if (!handleIntent(intent)) {
var handleUrl = false
if (intent != null && Intent.ACTION_VIEW == intent.action) {
handleUrl = true
if (intent.data != null) {
val url = intent.data.toString()
EditTextDialogBuilder(this, url, "")
.setTitle(R.string.error_cannot_parse_the_url)
.setPositiveButton(android.R.string.copy) { _: DialogInterface?, _: Int ->
this.addTextToClipboard(
url,
false
)
}
.show()
override fun onCreate(savedInstanceState: Bundle?) {
if (EhUtils.needSignedIn())
startActivity(Intent(this, LoginActivity::class.java))
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navController = navHostFragment.navController
navController.apply {
graph = navInflater.inflate(R.navigation.nav_graph).apply {
when (Settings.getLaunchPageGalleryListSceneAction()) {
GalleryListScene.ACTION_HOMEPAGE -> setStartDestination(R.id.nav_homepage)
GalleryListScene.ACTION_SUBSCRIPTION -> setStartDestination(R.id.nav_subscription)
GalleryListScene.ACTION_WHATS_HOT -> setStartDestination(R.id.nav_whats_hot)
GalleryListScene.ACTION_TOP_LIST -> setStartDestination(R.id.nav_toplist)
}
}
if (0 == sceneCount) {
if (handleUrl) {
finish()
}
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
binding.navView.setupWithNavController(navController)
// Trick: Tweak NavigationUI to disable multiple backstack
binding.navView.setNavigationItemSelectedListener {
val navigationView = binding.navView
val handled = onNavDestinationSelected2(it, navController)
if (handled) {
val parent = navigationView.parent
if (parent is Openable) {
parent.close()
} else {
val args = Bundle()
args.putString(
GalleryListScene.KEY_ACTION,
Settings.getLaunchPageGalleryListSceneAction()
)
startScene(processAnnouncer(Announcer(GalleryListScene::class.java).setArgs(args)))
val bottomSheetBehavior = NavigationUI.findBottomSheetBehavior(navigationView)
if (bottomSheetBehavior != null) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
}
}
handled
}
}
// Trick End
override fun onStartSceneFromIntent(clazz: Class<*>, args: Bundle?): Announcer {
return processAnnouncer(Announcer(clazz).setArgs(args))
}
override fun onCreate2(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)
mStageLayout = ViewUtils.`$$`(this, R.id.fragment_container) as EhStageLayout
mDrawerLayout = ViewUtils.`$$`(this, R.id.draw_view) as DrawerLayout
mNavView = ViewUtils.`$$`(this, R.id.nav_view) as NavigationView
mRightDrawer = ViewUtils.`$$`(this, R.id.right_drawer) as DrawerView
mDrawerLayout?.addDrawerListener(mDrawerOnBackPressedCallback)
onBackPressedDispatcher.addCallback(mDrawerOnBackPressedCallback)
if (mNavView != null) {
mNavView!!.setNavigationItemSelectedListener(this)
}
if (savedInstanceState == null) {
if (intent.action != Intent.ACTION_MAIN) {
onNewIntent(intent)
}
checkDownloadLocation()
if (Settings.getMeteredNetworkWarning()) {
checkMeteredNetwork()
@@ -270,8 +272,6 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
}
}
}
} else {
onRestore(savedInstanceState)
}
}
@@ -335,9 +335,9 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
private fun checkMeteredNetwork() {
val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
if (cm.isActiveNetworkMetered) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && mDrawerLayout != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Snackbar.make(
mDrawerLayout!!,
binding.drawView,
R.string.metered_network_warning,
Snackbar.LENGTH_LONG
)
@@ -353,110 +353,72 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
}
}
private fun onRestore(savedInstanceState: Bundle) {
mNavCheckedItem = savedInstanceState.getInt(KEY_NAV_CHECKED_ITEM)
}
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
super.onSaveInstanceState(outState, outPersistentState)
outState.putInt(KEY_NAV_CHECKED_ITEM, mNavCheckedItem)
}
override fun onDestroy() {
super.onDestroy()
mDrawerLayout = null
mNavView = null
mRightDrawer = null
}
override fun onResume() {
super.onResume()
setNavCheckedItem(mNavCheckedItem)
checkClipboardUrl()
}
override fun onTransactScene() {
super.onTransactScene()
checkClipboardUrl()
}
private fun checkClipboardUrl() {
SimpleHandler.getInstance().postDelayed({
if (!isSolid) {
checkClipboardUrlInternal()
}
}, 300)
}
private val isSolid: Boolean
get() {
val topClass = topSceneClass
return topClass == null || SolidScene::class.java.isAssignableFrom(topClass)
lifecycleScope.launch {
delay(100)
// delay 100ms to make sure it take precedence over androidx navigation's callback
mDrawerOnBackPressedCallback.remove()
onBackPressedDispatcher.addCallback(mDrawerOnBackPressedCallback)
delay(200)
checkClipboardUrl()
}
private fun createAnnouncerFromClipboardUrl(url: String): Announcer? {
val result1 = GalleryDetailUrlParser.parse(url, false)
if (result1 != null) {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)
args.putLong(GalleryDetailScene.KEY_GID, result1.gid)
args.putString(GalleryDetailScene.KEY_TOKEN, result1.token)
return Announcer(GalleryDetailScene::class.java).setArgs(args)
}
val result2 = GalleryPageUrlParser.parse(url, false)
if (result2 != null) {
val args = Bundle()
args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN)
args.putLong(ProgressScene.KEY_GID, result2.gid)
args.putString(ProgressScene.KEY_PTOKEN, result2.pToken)
args.putInt(ProgressScene.KEY_PAGE, result2.page)
return Announcer(ProgressScene::class.java).setArgs(args)
}
return null
}
private fun checkClipboardUrlInternal() {
private suspend fun checkClipboardUrl() {
val text = this.getClipboardManager().getUrlFromClipboard(this)
val hashCode = text?.hashCode() ?: 0
if (text != null && hashCode != 0 && Settings.getClipboardTextHashCode() != hashCode) {
val announcer = createAnnouncerFromClipboardUrl(text)
if (announcer != null && mDrawerLayout != null) {
val snackbar = Snackbar.make(
mDrawerLayout!!,
R.string.clipboard_gallery_url_snack_message,
Snackbar.LENGTH_SHORT
)
snackbar.setAction(R.string.clipboard_gallery_url_snack_action) {
startScene(
announcer
val result1 = GalleryDetailUrlParser.parse(text, false)
var launch: (() -> Unit)? = null
if (result1 != null) {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)
args.putLong(GalleryDetailScene.KEY_GID, result1.gid)
args.putString(GalleryDetailScene.KEY_TOKEN, result1.token)
launch = { navController.navigate(R.id.galleryDetailScene, args) }
}
val result2 = GalleryPageUrlParser.parse(text, false)
if (result2 != null) {
val args = Bundle()
args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN)
args.putLong(ProgressScene.KEY_GID, result2.gid)
args.putString(ProgressScene.KEY_PTOKEN, result2.pToken)
args.putInt(ProgressScene.KEY_PAGE, result2.page)
launch = { navController.navigate(R.id.progressScene, args) }
}
launch?.let {
withUIContext {
val snackbar = Snackbar.make(
binding.drawView,
R.string.clipboard_gallery_url_snack_message,
Snackbar.LENGTH_SHORT
)
snackbar.setAction(R.string.clipboard_gallery_url_snack_action) {
it()
}
snackbar.show()
}
snackbar.show()
}
}
Settings.putClipboardTextHashCode(hashCode)
}
override fun onSceneViewCreated(scene: SceneFragment, savedInstanceState: Bundle?) {
super.onSceneViewCreated(scene, savedInstanceState)
createDrawerView(scene)
}
@SuppressLint("RtlHardcoded")
fun createDrawerView(scene: SceneFragment?) {
if (scene is BaseScene && mRightDrawer != null && mDrawerLayout != null) {
mRightDrawer!!.removeAllViews()
fun createDrawerView(scene: Fragment?) {
if (scene is BaseScene) {
binding.rightDrawer.removeAllViews()
val drawerView = scene.createDrawerView(
scene.layoutInflater, mRightDrawer, null
scene.layoutInflater, binding.rightDrawer, null
)
if (drawerView != null) {
mRightDrawer!!.addView(drawerView)
mDrawerLayout!!.setDrawerLockMode(
binding.rightDrawer.addView(drawerView)
binding.drawView.setDrawerLockMode(
DrawerLayout.LOCK_MODE_UNLOCKED,
GravityCompat.END
)
} else {
mDrawerLayout!!.setDrawerLockMode(
binding.drawView.setDrawerLockMode(
DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
GravityCompat.END
)
@@ -464,39 +426,32 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
}
}
override fun onSceneViewDestroyed(scene: SceneFragment) {
super.onSceneViewDestroyed(scene)
if (scene is BaseScene) {
scene.destroyDrawerView()
}
}
fun addAboveSnackView(view: View?) {
mStageLayout?.addAboveSnackView(view)
binding.absnacker.addAboveSnackView(view)
}
fun removeAboveSnackView(view: View?) {
mStageLayout?.removeAboveSnackView(view)
binding.absnacker.removeAboveSnackView(view)
}
fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {
mDrawerLayout?.setDrawerLockMode(lockMode, edgeGravity)
binding.drawView.setDrawerLockMode(lockMode, edgeGravity)
}
fun getDrawerLockMode(edgeGravity: Int): Int? {
return mDrawerLayout?.getDrawerLockMode(edgeGravity)
fun getDrawerLockMode(edgeGravity: Int): Int {
return binding.drawView.getDrawerLockMode(edgeGravity)
}
fun openDrawer(drawerGravity: Int) {
mDrawerLayout?.openDrawer(drawerGravity)
binding.drawView.openDrawer(drawerGravity)
}
fun closeDrawer(drawerGravity: Int) {
mDrawerLayout?.closeDrawer(drawerGravity)
binding.drawView.closeDrawer(drawerGravity)
}
fun toggleDrawer(drawerGravity: Int) {
mDrawerLayout?.run {
binding.drawView.run {
if (isDrawerOpen(drawerGravity)) {
closeDrawer(drawerGravity)
} else {
@@ -505,26 +460,17 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
}
}
fun setNavCheckedItem(@IdRes resId: Int) {
mNavCheckedItem = resId
mNavView?.run {
if (resId == 0) {
setCheckedItem(R.id.nav_stub)
} else {
setCheckedItem(resId)
}
}
}
fun showTip(@StringRes id: Int, length: Int) {
showTip(getString(id), length)
@JvmOverloads
fun showTip(@StringRes id: Int, length: Int, useToast: Boolean = false) {
showTip(getString(id), length, useToast)
}
/**
* If activity is running, show snack bar, otherwise show toast
*/
fun showTip(message: CharSequence, length: Int) {
findViewById<View>(R.id.snackbar)?.apply {
@JvmOverloads
fun showTip(message: CharSequence, length: Int, useToast: Boolean = false) {
findViewById<View>(R.id.snackbar)?.takeUnless { useToast }?.apply {
Snackbar.make(
this, message,
if (length == BaseScene.LENGTH_LONG) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT
@@ -535,77 +481,17 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
).show()
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Don't select twice
if (item.isChecked) {
return false
}
val id = item.itemId
when (id) {
R.id.nav_homepage -> {
val args = Bundle()
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_HOMEPAGE)
startSceneFirstly(
Announcer(GalleryListScene::class.java)
.setArgs(args)
)
}
R.id.nav_subscription -> {
val args = Bundle()
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_SUBSCRIPTION)
startSceneFirstly(
Announcer(GalleryListScene::class.java)
.setArgs(args)
)
}
R.id.nav_whats_hot -> {
val args = Bundle()
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_WHATS_HOT)
startSceneFirstly(
Announcer(GalleryListScene::class.java)
.setArgs(args)
)
}
R.id.nav_toplist -> {
val args = Bundle()
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_TOP_LIST)
startSceneFirstly(
Announcer(GalleryListScene::class.java)
.setArgs(args)
)
}
R.id.nav_favourite -> {
startScene(Announcer(FavoritesScene::class.java))
}
R.id.nav_history -> {
startScene(Announcer(HistoryScene::class.java))
}
R.id.nav_downloads -> {
startScene(Announcer(DownloadsScene::class.java))
}
R.id.nav_settings -> {
val intent = Intent(this, SettingsActivity::class.java)
settingsLauncher.launch(intent)
}
}
if (id != R.id.nav_stub) {
mDrawerLayout?.closeDrawers()
}
return true
var mShareUrl: String? = null
override fun onProvideAssistContent(outContent: AssistContent?) {
super.onProvideAssistContent(outContent)
mShareUrl?.let { outContent?.webUri = Uri.parse(mShareUrl) }
}
private val mDrawerOnBackPressedCallback =
object : OnBackPressedCallback(false), DrawerListener {
val slideThreshold = 0.05
override fun handleOnBackPressed() {
mDrawerLayout?.closeDrawers()
binding.drawView.closeDrawers()
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
@@ -616,26 +502,4 @@ class MainActivity : StageActivity(), NavigationView.OnNavigationItemSelectedLis
override fun onDrawerClosed(drawerView: View) {}
override fun onDrawerStateChanged(newState: Int) {}
}
companion object {
private const val KEY_NAV_CHECKED_ITEM = "nav_checked_item"
init {
registerLaunchMode(SignInScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)
registerLaunchMode(
WebViewSignInScene::class.java,
SceneFragment.LAUNCH_MODE_SINGLE_TASK
)
registerLaunchMode(CookieSignInScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)
registerLaunchMode(SelectSiteScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)
registerLaunchMode(GalleryListScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TOP)
registerLaunchMode(GalleryDetailScene::class.java, SceneFragment.LAUNCH_MODE_STANDARD)
registerLaunchMode(GalleryCommentsScene::class.java, SceneFragment.LAUNCH_MODE_STANDARD)
registerLaunchMode(GalleryPreviewsScene::class.java, SceneFragment.LAUNCH_MODE_STANDARD)
registerLaunchMode(DownloadsScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)
registerLaunchMode(FavoritesScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)
registerLaunchMode(HistoryScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TOP)
registerLaunchMode(ProgressScene::class.java, SceneFragment.LAUNCH_MODE_STANDARD)
}
}
}
@@ -20,6 +20,7 @@ import android.view.MenuItem
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentTransaction
import androidx.navigation.ActivityNavigator
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
@@ -78,4 +79,9 @@ class SettingsActivity : EhActivity() {
fun resetAppbarLiftStatus() {
mAppbarLayout?.setExpanded(true)
}
override fun finish() {
super.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
}
}
@@ -1,37 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.annotation;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created in {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)},
* destroyed in {@link Fragment#onDestroyView()}.
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface DrawerLifeCircle {
}
@@ -1,37 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.annotation;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created in {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)},
* destroyed in {@link Fragment#onDestroyView()}.
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface ViewLifeCircle {
}
@@ -1,36 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.annotation;
import android.app.Activity;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created in {@link Fragment#onCreate(Bundle)} or {@link Activity#onCreate(Bundle)},
* destroyed in {@link Fragment#onDestroy()} or {@link Activity#onDestroy()}.
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface WholeLifeCircle {
}
@@ -0,0 +1,309 @@
package com.hippo.ehviewer.ui.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
import com.hippo.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.Cookie
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CookieSignInScene(navController: NavController) {
val clipboardManager = LocalClipboardManager.current
val focusManager = LocalFocusManager.current
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
var isProgressIndicatorVisible by rememberSaveable { mutableStateOf(false) }
var ipbMemberId by rememberSaveable { mutableStateOf("") }
var ipbPassHash by rememberSaveable { mutableStateOf("") }
var igneous by rememberSaveable { mutableStateOf("") }
var ipbMemberIdErrorState by rememberSaveable { mutableStateOf(false) }
var ipbPassHashErrorState by rememberSaveable { mutableStateOf(false) }
var signInJob by remember { mutableStateOf<Job?>(null) }
var showErrorDialog by rememberSaveable { mutableStateOf(false) }
var loginErrorException by remember { mutableStateOf<Throwable?>(null) }
val noCookies = stringResource(R.string.from_clipboard_error)
fun newCookie(name: String, value: String, domain: String): Cookie {
return Cookie.Builder().name(name).value(value).domain(domain).expiresAt(Long.MAX_VALUE)
.build()
}
fun storeCookie(id: String, hash: String, igneous: String) {
EhUtils.signOut()
val store = EhApplication.ehCookieStore
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_E))
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_EX))
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_E))
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_EX))
if (igneous.isBlank()) return
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_E))
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX))
}
fun login() {
if (signInJob?.isActive == true) return
if (ipbMemberId.isBlank()) {
ipbMemberIdErrorState = true
return
} else {
ipbMemberIdErrorState = false
}
if (ipbPassHash.isBlank()) {
ipbPassHashErrorState = true
return
} else {
ipbPassHashErrorState = false
}
focusManager.clearFocus()
storeCookie(ipbMemberId, ipbPassHash, igneous)
isProgressIndicatorVisible = true
signInJob = coroutineScope.launchIO {
runCatching {
EhEngine.getProfile()
}.onSuccess {
withUIContext {
navController.navigate(SELECT_SITE_ROUTE_NAME)
}
}.onFailure {
EhApplication.ehCookieStore.signOut()
loginErrorException = it
showErrorDialog = true
isProgressIndicatorVisible = false
}
}
}
fun fillCookiesFromClipboard() {
focusManager.clearFocus()
val text = clipboardManager.getText()
if (text == null) {
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
return
}
runCatching {
val kvs: Array<String> = if (text.contains(";")) {
text.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else if (text.contains("\n")) {
text.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else {
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
return
}
if (kvs.size < 3) {
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
return
}
for (s in kvs) {
val kv: Array<String> = if (s.contains("=")) {
s.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else if (s.contains(":")) {
s.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} else {
continue
}
if (kv.size != 2) {
continue
}
when (kv[0].trim { it <= ' ' }.lowercase(Locale.getDefault())) {
"ipb_member_id" -> ipbMemberId = kv[1].trim { it <= ' ' }
"ipb_pass_hash" -> ipbPassHash = kv[1].trim { it <= ' ' }
"igneous" -> igneous = kv[1].trim { it <= ' ' }
}
}
login()
}.onFailure {
it.printStackTrace()
coroutineScope.launch { snackbarHostState.showSnackbar(noCookies) }
}
}
Box(
contentAlignment = Alignment.Center
) {
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { padding ->
Column(
Modifier
.padding(dimensionResource(id = R.dimen.keyline_margin))
.padding(padding)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.v_cookie_brown_x48),
contentDescription = null,
modifier = Modifier.padding(dimensionResource(id = R.dimen.keyline_margin))
)
Text(
text = stringResource(id = R.string.cookie_explain),
modifier = Modifier
.padding(
horizontal = 32.dp,
vertical = dimensionResource(id = R.dimen.keyline_margin)
),
fontSize = 16.sp
)
OutlinedTextField(
value = ipbMemberId,
onValueChange = { ipbMemberId = it.trim { char -> char <= ' ' } },
Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
label = { Text(text = "ipb_member_id") },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
supportingText = { if (ipbMemberIdErrorState) Text(stringResource(R.string.text_is_empty)) },
trailingIcon = {
if (ipbMemberIdErrorState) Icon(
imageVector = Icons.Filled.Info,
contentDescription = null
)
},
isError = ipbMemberIdErrorState,
singleLine = true
)
OutlinedTextField(
value = ipbPassHash,
onValueChange = { ipbPassHash = it.trim { char -> char <= ' ' } },
Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
label = { Text(text = "ipb_pass_hash") },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
supportingText = { if (ipbPassHashErrorState) Text(stringResource(R.string.text_is_empty)) },
trailingIcon = {
if (ipbPassHashErrorState) Icon(
imageVector = Icons.Filled.Info,
contentDescription = null
)
},
isError = ipbPassHashErrorState,
singleLine = true
)
OutlinedTextField(
value = igneous,
onValueChange = { igneous = it.trim { char -> char <= ' ' } },
Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
label = { Text(text = "igneous") },
keyboardActions = KeyboardActions(
onDone = { login() }
),
singleLine = true
)
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = { login() },
Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = android.R.string.ok))
}
TextButton(onClick = { fillCookiesFromClipboard() }) {
Text(
text = buildAnnotatedString {
withStyle(
style = SpanStyle(textDecoration = TextDecoration.Underline)
) {
append(stringResource(id = R.string.from_clipboard))
}
},
modifier = Modifier
.padding(horizontal = 8.dp)
)
}
}
}
if (isProgressIndicatorVisible)
CircularProgressIndicator()
if (showErrorDialog) {
AlertDialog(
onDismissRequest = {
showErrorDialog = false
},
confirmButton = {
TextButton(onClick = {
showErrorDialog = false
}) {
Text(text = stringResource(id = R.string.get_it))
}
},
title = {
Text(text = stringResource(id = R.string.sign_in_failed))
},
text = {
Text(
text =
"""
${ExceptionUtils.getReadableString(loginErrorException!!)}
${stringResource(R.string.wrong_cookie_warning)}
""".trimIndent()
)
}
)
}
}
}
@@ -0,0 +1,142 @@
package com.hippo.ehviewer.ui.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.GlobalScope
@Composable
fun SelectSiteScreen(finish: () -> Unit) {
var site by remember { mutableStateOf(EhApplication.ehCookieStore.hasSignedIn()) } // false for e, true for ex
Column(
Modifier
.padding(horizontal = dimensionResource(R.dimen.keyline_margin))
.padding(top = dimensionResource(R.dimen.keyline_margin))
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.select_scene),
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.weight(1f))
// TODO: Replace this dummy impl with official one when available
// See https://m3.material.io/components/segmented-buttons/overview
Row(
modifier = Modifier
.height(IntrinsicSize.Min)
.border(
1.dp,
MaterialTheme.colorScheme.outline,
RoundedCornerShape(90f, 90f, 90f, 90f)
)
.clip(RoundedCornerShape(90f, 90f, 90f, 90f))
) {
Box(
modifier = Modifier
.background(if (!site) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
.clickable { site = false }
.size(100.dp, 40.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.site_e),
style = MaterialTheme.typography.labelLarge
)
}
Divider(
color = MaterialTheme.colorScheme.outline, modifier = Modifier
.fillMaxHeight()
.width(1.dp)
)
Box(
modifier = Modifier
.background(if (site) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
.clickable { site = true }
.size(100.dp, 40.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.site_ex),
style = MaterialTheme.typography.labelLarge
)
}
}
// Dummy impl end
Text(
text = stringResource(id = R.string.select_scene_explain),
modifier = Modifier.padding(top = 16.dp)
)
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
.padding(horizontal = 12.dp)
.padding(top = 4.dp, bottom = 20.dp)
.fillMaxWidth()
) {
Button(onClick = {
Settings.putSelectSite(false)
Settings.putNeedSignIn(false)
if (!site)
Settings.putGallerySite(EhUrl.SITE_E)
else
Settings.putGallerySite(EhUrl.SITE_EX)
GlobalScope.launchIO {
getProfile()
}
finish()
}, Modifier.fillMaxWidth()) {
Text(text = stringResource(id = android.R.string.ok))
}
}
}
}
suspend fun getProfile() {
runCatching {
EhEngine.getProfile().run {
Settings.putDisplayName(displayName)
Settings.putAvatar(avatar)
}
}
}
@@ -0,0 +1,298 @@
package com.hippo.ehviewer.ui.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.UrlOpener
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.LoginActivity.Companion.COOKIE_SIGN_IN_ROUTE_NAME
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
import com.hippo.ehviewer.ui.LoginActivity.Companion.WEBVIEW_SIGN_IN_ROUTE_NAME
import com.hippo.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Job
import rikka.core.util.ContextUtils.requireActivity
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SignInScreen(navController: NavController) {
val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
var isProgressIndicatorVisible by rememberSaveable { mutableStateOf(false) }
var showUsernameError by rememberSaveable { mutableStateOf(false) }
var showPasswordError by rememberSaveable { mutableStateOf(false) }
var showErrorDialog by rememberSaveable { mutableStateOf(false) }
var loginErrorException by remember { mutableStateOf<Throwable?>(null) }
var username by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
var passwordHidden by rememberSaveable { mutableStateOf(true) }
val context = LocalContext.current
var signInJob by remember { mutableStateOf<Job?>(null) }
// Basic login request
fun signIn() {
if (signInJob?.isActive == true) return
if (username.isEmpty()) {
showUsernameError = true
return
} else {
showUsernameError = false
}
if (password.isEmpty()) {
showPasswordError = true
return
} else {
showPasswordError = false
}
focusManager.clearFocus()
isProgressIndicatorVisible = true
EhUtils.signOut()
signInJob = coroutineScope.launchIO {
runCatching {
EhEngine.signIn(username, password)
}.onFailure {
it.printStackTrace()
withUIContext {
focusManager.clearFocus()
isProgressIndicatorVisible = false
loginErrorException = it
showErrorDialog = true
}
}.onSuccess {
withUIContext {
navController.navigate(SELECT_SITE_ROUTE_NAME)
}
}
}
}
Box(
contentAlignment = Alignment.Center
) {
Column(
Modifier
.padding(dimensionResource(id = R.dimen.keyline_margin))
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
Modifier.padding(dimensionResource(id = R.dimen.keyline_margin))
)
OutlinedTextField(
value = username,
onValueChange = { username = it },
Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
label = { Text(stringResource(R.string.username)) },
supportingText = { if (showUsernameError) Text(stringResource(R.string.error_username_cannot_empty)) },
trailingIcon = {
if (showUsernameError) Icon(
imageVector = Icons.Filled.Info,
contentDescription = null
)
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
singleLine = true,
isError = showUsernameError
)
OutlinedTextField(
value = password,
onValueChange = { password = it },
Modifier.width(dimensionResource(id = R.dimen.single_max_width)),
label = { Text(stringResource(R.string.password)) },
visualTransformation = if (passwordHidden) PasswordVisualTransformation() else VisualTransformation.None,
supportingText = { if (showPasswordError) Text(stringResource(R.string.error_password_cannot_empty)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
keyboardActions = KeyboardActions(
onDone = { signIn() }
),
trailingIcon = {
if (showPasswordError)
Icon(imageVector = Icons.Filled.Info, contentDescription = null)
else
IconButton(onClick = { passwordHidden = !passwordHidden }) {
val visibilityIcon =
if (passwordHidden) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
Icon(imageVector = visibilityIcon, contentDescription = null)
}
},
singleLine = true,
isError = showPasswordError
)
Text(
text = stringResource(id = R.string.app_waring),
Modifier
.widthIn(max = dimensionResource(id = R.dimen.single_max_width))
.padding(top = 24.dp),
style = MaterialTheme.typography.labelLarge
)
Spacer(modifier = Modifier.weight(1f))
Row {
OutlinedButton(
onClick = {
UrlOpener.openUrl(
requireActivity(context),
EhUrl.URL_REGISTER,
false
)
},
Modifier
.weight(1f)
.padding(horizontal = 4.dp)
) {
Text(text = stringResource(id = R.string.register))
}
Button(
onClick = { signIn() },
Modifier
.weight(1f)
.padding(horizontal = 4.dp)
) {
Text(text = stringResource(id = R.string.sign_in))
}
}
Row(
Modifier.padding(horizontal = 4.dp)
) {
TextButton(
onClick = { navController.navigate(WEBVIEW_SIGN_IN_ROUTE_NAME) },
Modifier.padding(horizontal = 8.dp)
) {
Text(
text = buildAnnotatedString {
withStyle(
style = SpanStyle(textDecoration = TextDecoration.Underline)
) {
append(stringResource(id = R.string.sign_in_via_webview))
}
}
)
}
TextButton(
onClick = { navController.navigate(COOKIE_SIGN_IN_ROUTE_NAME) },
Modifier.padding(horizontal = 8.dp)
) {
Text(
text = buildAnnotatedString {
withStyle(
style = SpanStyle(textDecoration = TextDecoration.Underline)
) {
append(stringResource(id = R.string.sign_in_via_cookies))
}
}
)
}
}
TextButton(
onClick = { navController.navigate(SELECT_SITE_ROUTE_NAME) },
) {
Text(
text = buildAnnotatedString {
withStyle(
style = SpanStyle(textDecoration = TextDecoration.Underline)
) {
append(stringResource(id = R.string.tourist_mode))
}
}
)
}
}
if (isProgressIndicatorVisible) {
CircularProgressIndicator()
}
if (showErrorDialog) {
AlertDialog(
onDismissRequest = {
showErrorDialog = false
},
confirmButton = {
TextButton(onClick = {
showErrorDialog = false
}) {
Text(text = stringResource(id = R.string.get_it))
}
},
title = {
Text(text = stringResource(id = R.string.sign_in_failed))
},
text = {
Text(
text =
"""
${ExceptionUtils.getReadableString(loginErrorException!!)}
${stringResource(R.string.sign_in_failed_tip)}
""".trimIndent()
)
}
)
}
}
}
@@ -0,0 +1,108 @@
package com.hippo.ehviewer.ui.compose
import android.annotation.SuppressLint
import android.graphics.Color
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.navigation.NavController
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.LoginActivity.Companion.SELECT_SITE_ROUTE_NAME
import com.hippo.ehviewer.widget.DialogWebChromeClient
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebviewSignInScreen(navController: NavController) {
val context = LocalContext.current
AndroidView(factory = {
EhUtils.signOut()
// http://stackoverflow.com/questions/32284642/how-to-handle-an-uncatched-exception
CookieManager.getInstance().apply {
flush()
removeAllCookies(null)
removeSessionCookies(null)
}
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
setBackgroundColor(Color.TRANSPARENT)
settings.run {
builtInZoomControls = true
displayZoomControls = true
javaScriptEnabled = true
}
webViewClient = object : WebViewClient() {
private var present = false
fun parseCookies(url: HttpUrl?, cookieStrings: String?): List<Cookie> {
if (cookieStrings == null) {
return emptyList()
}
var cookies: MutableList<Cookie>? = null
val pieces =
cookieStrings.split(";".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
for (piece in pieces) {
val cookie = Cookie.parse(url!!, piece) ?: continue
if (cookies == null) {
cookies = ArrayList()
}
cookies.add(cookie)
}
return cookies ?: emptyList()
}
fun addCookie(domain: String, cookie: Cookie) {
EhApplication.ehCookieStore.addCookie(
EhCookieStore.newCookie(
cookie,
domain,
true,
true,
true
)
)
}
override fun onPageFinished(view: WebView, url: String) {
if (present) {
view.destroy()
return
}
val httpUrl = url.toHttpUrlOrNull() ?: return
val cookieString = CookieManager.getInstance().getCookie(EhUrl.HOST_E)
val cookies = parseCookies(httpUrl, cookieString)
var getId = false
var getHash = false
for (cookie in cookies) {
if (EhCookieStore.KEY_IPD_MEMBER_ID == cookie.name) {
getId = true
} else if (EhCookieStore.KEY_IPD_PASS_HASH == cookie.name) {
getHash = true
}
addCookie(EhUrl.DOMAIN_EX, cookie)
addCookie(EhUrl.DOMAIN_E, cookie)
}
if (getId && getHash) {
present = true
navController.navigate(SELECT_SITE_ROUTE_NAME)
}
}
}
webChromeClient = DialogWebChromeClient(context)
loadUrl(EhUrl.URL_SIGN_IN)
}
})
}
@@ -259,7 +259,7 @@ class FilterFragment : BaseFragment() {
}
private inner class FilterList {
private val mEhFilter: EhFilter = EhFilter.getInstance()
private val mEhFilter: EhFilter = EhFilter
private val mTitleFilterList: List<Filter> = mEhFilter.titleFilterList
private val mUploaderFilterList: List<Filter> = mEhFilter.uploaderFilterList
private val mTagFilterList: List<Filter> = mEhFilter.tagFilterList
@@ -407,11 +407,11 @@ class FilterFragment : BaseFragment() {
throw IndexOutOfBoundsException()
}
fun add(filter: Filter?): Boolean {
fun add(filter: Filter): Boolean {
return mEhFilter.addFilter(filter)
}
fun delete(filter: Filter?) {
fun delete(filter: Filter) {
mEhFilter.deleteFilter(filter)
}
@@ -1,281 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.view.GravityCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.FragmentActivity;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.SceneFragment;
public abstract class BaseScene extends SceneFragment {
public static final int LENGTH_SHORT = 0;
public static final int LENGTH_LONG = 1;
public static final String KEY_DRAWER_VIEW_STATE =
"com.hippo.ehviewer.ui.scene.BaseScene:DRAWER_VIEW_STATE";
private WindowInsetsControllerCompat insetsController;
@Nullable
private View drawerView;
@Nullable
private SparseArray<Parcelable> drawerViewState;
public void addAboveSnackView(View view) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).addAboveSnackView(view);
}
}
public void removeAboveSnackView(View view) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).removeAboveSnackView(view);
}
}
public void setDrawerLockMode(int lockMode, int edgeGravity) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).setDrawerLockMode(lockMode, edgeGravity);
}
}
public Integer getDrawerLockMode(int edgeGravity) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
return ((MainActivity) activity).getDrawerLockMode(edgeGravity);
}
return null;
}
public void openDrawer(int drawerGravity) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).openDrawer(drawerGravity);
}
}
public void closeDrawer(int drawerGravity) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).closeDrawer(drawerGravity);
}
}
public void toggleDrawer(int drawerGravity) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).toggleDrawer(drawerGravity);
}
}
public void showTip(CharSequence message, int length) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).showTip(message, length);
}
}
public void showTip(@StringRes int id, int length) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).showTip(id, length);
}
}
public boolean needShowLeftDrawer() {
return true;
}
public int getNavCheckedItem() {
return 0;
}
/**
* @param resId 0 for clear
*/
public void setNavCheckedItem(@IdRes int resId) {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).setNavCheckedItem(resId);
}
}
public void recreateDrawerView() {
MainActivity activity = getMainActivity();
if (activity != null) {
activity.createDrawerView(this);
}
}
public final View createDrawerView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
drawerView = onCreateDrawerView(inflater, container, savedInstanceState);
if (drawerView != null) {
SparseArray<Parcelable> saved = drawerViewState;
if (saved == null && savedInstanceState != null) {
saved = savedInstanceState.getSparseParcelableArray(KEY_DRAWER_VIEW_STATE);
}
if (saved != null) {
drawerView.restoreHierarchyState(saved);
}
}
return drawerView;
}
public View onCreateDrawerView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return null;
}
public final void destroyDrawerView() {
if (drawerView != null) {
drawerViewState = new SparseArray<>();
drawerView.saveHierarchyState(drawerViewState);
}
onDestroyDrawerView();
drawerView = null;
}
public void onDestroyDrawerView() {
}
@SuppressLint("RtlHardcoded")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
postponeEnterTransition();
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
startPostponedEnterTransition();
return true;
}
});
// Update left drawer locked state
if (needShowLeftDrawer()) {
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START);
} else {
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START);
}
// Update nav checked item
setNavCheckedItem(getNavCheckedItem());
// Hide soft ime
hideSoftInput();
}
@Nullable
public Resources getResourcesOrNull() {
Context context = getContext();
if (context != null) {
return context.getResources();
} else {
return null;
}
}
@Nullable
public MainActivity getMainActivity() {
FragmentActivity activity = getActivity();
if (activity instanceof MainActivity) {
return (MainActivity) activity;
} else {
return null;
}
}
public void hideSoftInput() {
var insetsController = getInsetsController();
if (insetsController != null) {
insetsController.hide(WindowInsetsCompat.Type.ime());
}
}
public void showSoftInput(@Nullable View view) {
if (view != null) {
view.requestFocus();
view.post(() -> {
var insetsController = getInsetsController();
if (insetsController != null) {
insetsController.show(WindowInsetsCompat.Type.ime());
}
});
}
}
public WindowInsetsControllerCompat getInsetsController() {
if (insetsController == null) {
var activity = getActivity();
if (activity != null) {
insetsController = ViewCompat.getWindowInsetsController(activity.getWindow().getDecorView());
}
}
return insetsController;
}
@Override
public void onDestroy() {
super.onDestroy();
insetsController = null;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (drawerView != null) {
drawerViewState = new SparseArray<>();
drawerView.saveHierarchyState(drawerViewState);
outState.putSparseParcelableArray(KEY_DRAWER_VIEW_STATE, drawerViewState);
}
}
public Resources.Theme getTheme() {
return requireActivity().getTheme();
}
}
@@ -0,0 +1,244 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.content.res.Resources
import android.content.res.Resources.Theme
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment
import com.hippo.ehviewer.R
import com.hippo.ehviewer.ui.MainActivity
import rikka.core.res.resolveDrawable
abstract class BaseScene : Fragment() {
private var insetsController: WindowInsetsControllerCompat? = null
private var drawerView: View? = null
private var drawerViewState: SparseArray<Parcelable?>? = null
fun addAboveSnackView(view: View?) {
val activity = activity
if (activity is MainActivity) {
activity.addAboveSnackView(view)
}
}
fun removeAboveSnackView(view: View?) {
val activity = activity
if (activity is MainActivity) {
activity.removeAboveSnackView(view)
}
}
fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.setDrawerLockMode(lockMode, edgeGravity)
}
}
fun getDrawerLockMode(edgeGravity: Int): Int? {
val activity = activity
return if (activity is MainActivity) {
activity.getDrawerLockMode(edgeGravity)
} else null
}
fun openDrawer(drawerGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.openDrawer(drawerGravity)
}
}
fun closeDrawer(drawerGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.closeDrawer(drawerGravity)
}
}
fun toggleDrawer(drawerGravity: Int) {
val activity = activity
if (activity is MainActivity) {
activity.toggleDrawer(drawerGravity)
}
}
fun showTip(message: CharSequence?, length: Int) {
val activity = activity
if (activity is MainActivity) {
activity.showTip(message!!, length)
}
}
fun showTip(@StringRes id: Int, length: Int) {
val activity = activity
if (activity is MainActivity) {
activity.showTip(id, length)
}
}
open fun needShowLeftDrawer(): Boolean {
return true
}
fun recreateDrawerView() {
val activity = mainActivity
activity?.createDrawerView(this)
}
fun createDrawerView(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
drawerView = onCreateDrawerView(inflater, container, savedInstanceState)
if (drawerView != null) {
var saved = drawerViewState
if (saved == null && savedInstanceState != null) {
saved = savedInstanceState.getSparseParcelableArray(KEY_DRAWER_VIEW_STATE)
}
if (saved != null) {
drawerView!!.restoreHierarchyState(saved)
}
}
return drawerView
}
open fun onCreateDrawerView(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
return null
}
private fun destroyDrawerView() {
if (drawerView != null) {
drawerViewState = SparseArray()
drawerView!!.saveHierarchyState(drawerViewState)
}
onDestroyDrawerView()
drawerView = null
}
open fun onDestroyDrawerView() {}
@SuppressLint("RtlHardcoded")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.background = requireActivity().theme.resolveDrawable(android.R.attr.windowBackground)
// Update left drawer locked state
if (needShowLeftDrawer()) {
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
} else {
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
}
// Hide soft ime
hideSoftInput()
mainActivity!!.createDrawerView(this)
}
override fun onDestroyView() {
super.onDestroyView()
destroyDrawerView()
}
val resourcesOrNull: Resources?
get() {
val context = context
return context?.resources
}
val mainActivity: MainActivity?
get() {
val activity = activity
return if (activity is MainActivity) {
activity
} else {
null
}
}
fun hideSoftInput() {
val insetsController = getInsetsController()
insetsController?.hide(WindowInsetsCompat.Type.ime())
}
fun showSoftInput(view: View?) {
if (view != null) {
view.requestFocus()
view.post(Runnable {
val insetsController = getInsetsController()
insetsController?.show(WindowInsetsCompat.Type.ime())
})
}
}
private fun getInsetsController(): WindowInsetsControllerCompat? {
if (insetsController == null) {
val activity = activity
if (activity != null) {
insetsController = ViewCompat.getWindowInsetsController(activity.window.decorView)
}
}
return insetsController
}
override fun onDestroy() {
super.onDestroy()
insetsController = null
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (drawerView != null) {
drawerViewState = SparseArray()
drawerView!!.saveHierarchyState(drawerViewState)
outState.putSparseParcelableArray(KEY_DRAWER_VIEW_STATE, drawerViewState)
}
}
val theme: Theme
get() = requireActivity().theme
@JvmOverloads
fun navigate(id: Int, args: Bundle?, singleTop: Boolean = false) {
val options: NavOptions = NavOptions.Builder().setLaunchSingleTop(singleTop)
.setEnterAnim(R.anim.scene_open_enter).setExitAnim(R.anim.scene_open_exit)
.setPopEnterAnim(R.anim.scene_close_enter).setPopExitAnim(R.anim.scene_close_exit)
.build()
NavHostFragment.findNavController(this).navigate(id, args, options)
}
companion object {
const val LENGTH_SHORT = 0
const val LENGTH_LONG = 1
const val KEY_DRAWER_VIEW_STATE = "com.hippo.ehviewer.ui.scene.BaseScene:DRAWER_VIEW_STATE"
}
}
@@ -1,305 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Paint;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.TextInputLayout;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.util.ClipboardUtilKt;
import com.hippo.util.ExceptionUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.ViewUtils;
import okhttp3.Cookie;
public class CookieSignInScene extends SolidScene implements EditText.OnEditorActionListener,
View.OnClickListener {
/*---------------
View life cycle
---------------*/
@Nullable
private TextInputLayout mIpbMemberIdLayout;
@Nullable
private TextInputLayout mIpbPassHashLayout;
@Nullable
private TextInputLayout mIgneousLayout;
@Nullable
private EditText mIpbMemberId;
@Nullable
private EditText mIpbPassHash;
@Nullable
private EditText mIgneous;
@Nullable
private View mOk;
@Nullable
private TextView mFromClipboard;
private static Cookie newCookie(String name, String value, String domain) {
return new Cookie.Builder().name(name).value(value)
.domain(domain).expiresAt(Long.MAX_VALUE).build();
}
@Override
public boolean needShowLeftDrawer() {
return false;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_cookie_sign_in, container, false);
mIpbMemberIdLayout = (TextInputLayout) ViewUtils.$$(view, R.id.ipb_member_id_layout);
mIpbMemberId = mIpbMemberIdLayout.getEditText();
AssertUtils.assertNotNull(mIpbMemberId);
mIpbPassHashLayout = (TextInputLayout) ViewUtils.$$(view, R.id.ipb_pass_hash_layout);
mIpbPassHash = mIpbPassHashLayout.getEditText();
AssertUtils.assertNotNull(mIpbPassHash);
mIgneousLayout = (TextInputLayout) ViewUtils.$$(view, R.id.igneous_layout);
mIgneous = mIgneousLayout.getEditText();
AssertUtils.assertNotNull(mIgneous);
mOk = ViewUtils.$$(view, R.id.ok);
mFromClipboard = (TextView) ViewUtils.$$(view, R.id.from_clipboard);
mFromClipboard.setPaintFlags(mFromClipboard.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
mIpbPassHash.setOnEditorActionListener(this);
mOk.setOnClickListener(this);
mFromClipboard.setOnClickListener(this);
// Try to get old version cookie info
Context context = getContext();
AssertUtils.assertNotNull(context);
SharedPreferences sharedPreferences = context.getSharedPreferences("eh_info", 0);
String ipbMemberId = sharedPreferences.getString("ipb_member_id", null);
String ipbPassHash = sharedPreferences.getString("ipb_pass_hash", null);
String igneous = sharedPreferences.getString("igneous", null);
boolean getIt = false;
if (!TextUtils.isEmpty(ipbMemberId)) {
mIpbMemberId.setText(ipbMemberId);
getIt = true;
}
if (!TextUtils.isEmpty(ipbPassHash)) {
mIpbPassHash.setText(ipbPassHash);
getIt = true;
}
if (!TextUtils.isEmpty(igneous)) {
mIgneous.setText(igneous);
getIt = true;
}
if (getIt) {
showTip(R.string.found_cookies, LENGTH_SHORT);
}
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
showSoftInput(mIpbMemberId);
}
@Override
public void onDestroyView() {
super.onDestroyView();
mIpbMemberIdLayout = null;
mIpbPassHashLayout = null;
mIgneousLayout = null;
mIpbMemberId = null;
mIpbPassHash = null;
mIgneous = null;
}
@Override
public void onClick(View v) {
if (mOk == v) {
enter();
} else if (mFromClipboard == v) {
fillCookiesFromClipboard();
}
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (mIpbPassHash == v) {
enter();
}
return true;
}
public void enter() {
Context context = getContext();
if (null == context || null == mIpbMemberIdLayout || null == mIpbPassHashLayout ||
null == mIgneousLayout || null == mIpbMemberId || null == mIpbPassHash ||
null == mIgneous) {
return;
}
final String ipbMemberId = mIpbMemberId.getText().toString().trim();
final String ipbPassHash = mIpbPassHash.getText().toString().trim();
final String igneous = mIgneous.getText().toString().trim();
if (TextUtils.isEmpty(ipbMemberId)) {
mIpbMemberIdLayout.setError(getString(R.string.text_is_empty));
return;
} else {
mIpbMemberIdLayout.setError(null);
}
if (TextUtils.isEmpty(ipbPassHash)) {
mIpbPassHashLayout.setError(getString(R.string.text_is_empty));
return;
} else {
mIpbPassHashLayout.setError(null);
}
hideSoftInput();
storeCookie(ipbMemberId, ipbPassHash, igneous);
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_GET_PROFILE)
.setCallback(new CookieSignInListener());
request.enqueue(this);
}
private void storeCookie(String id, String hash, String igneous) {
Context context = getContext();
if (null == context) {
return;
}
EhUtils.signOut();
EhCookieStore store = EhApplication.getEhCookieStore();
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_E));
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_EX));
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_E));
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_EX));
if (!igneous.isEmpty()) {
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_E));
store.addCookie(newCookie(EhCookieStore.KEY_IGNEOUS, igneous, EhUrl.DOMAIN_EX));
}
}
private void fillCookiesFromClipboard() {
Context context = requireContext();
hideSoftInput();
String text = ClipboardUtilKt.getTextFromClipboard(
ClipboardUtilKt.getClipboardManager(context), context);
if (text == null) {
showTip(R.string.from_clipboard_error, LENGTH_SHORT);
return;
}
try {
String[] kvs;
if (text.contains(";")) {
kvs = text.split(";");
} else if (text.contains("\n")) {
kvs = text.split("\n");
} else {
showTip(R.string.from_clipboard_error, LENGTH_SHORT);
return;
}
if (kvs.length < 3) {
showTip(R.string.from_clipboard_error, LENGTH_SHORT);
return;
}
for (String s : kvs) {
String[] kv;
if (s.contains("=")) {
kv = s.split("=");
} else if (s.contains(":")) {
kv = s.split(":");
} else {
continue;
}
if (kv.length != 2) {
continue;
}
switch (kv[0].trim().toLowerCase()) {
case "ipb_member_id":
if (mIpbMemberId != null) {
mIpbMemberId.setText(kv[1].trim());
}
break;
case "ipb_pass_hash":
if (mIpbPassHash != null) {
mIpbPassHash.setText(kv[1].trim());
}
break;
case "igneous":
if (mIgneous != null) {
mIgneous.setText(kv[1].trim());
}
break;
}
}
enter();
} catch (Exception e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
showTip(R.string.from_clipboard_error, LENGTH_SHORT);
}
}
private class CookieSignInListener implements EhClient.Callback<Object> {
@Override
public void onSuccess(Object result) {
setResult(RESULT_OK, null);
finish();
}
@Override
public void onFailure(Exception e) {
EhApplication.getEhCookieStore().signOut();
new BaseDialogBuilder(requireContext()).setTitle(R.string.sign_in_failed)
.setMessage(ExceptionUtils.getReadableString(e) + "\n\n" + getString(R.string.wrong_cookie_warning))
.setPositiveButton(R.string.get_it, null).show();
}
@Override
public void onCancel() {
EhApplication.getEhCookieStore().signOut();
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,93 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.StringRes;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.SceneFragment;
import com.hippo.scene.StageActivity;
public abstract class EhCallback<E extends SceneFragment, T> implements EhClient.Callback<T> {
private final EhApplication mApplication;
private final int mStageId;
private final String mSceneTag;
public EhCallback(Context context, int stageId, String sceneTag) {
mApplication = (EhApplication) context.getApplicationContext();
mStageId = stageId;
mSceneTag = sceneTag;
}
public abstract boolean isInstance(SceneFragment scene);
public Context getContent() {
Context context = getStageActivity();
if (context == null) {
context = getApplication();
}
return context;
}
public EhApplication getApplication() {
return mApplication;
}
public StageActivity getStageActivity() {
return mApplication.findStageActivityById(mStageId);
}
@SuppressWarnings("unchecked")
public E getScene() {
StageActivity stage = mApplication.findStageActivityById(mStageId);
if (stage == null) {
return null;
}
SceneFragment scene = stage.findSceneByTag(mSceneTag);
if (isInstance(scene)) {
return (E) scene;
} else {
return null;
}
}
public void showTip(@StringRes int id, int length) {
StageActivity activity = getStageActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).showTip(id, length);
} else {
Toast.makeText(getApplication(), id,
length == BaseScene.LENGTH_LONG ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
}
}
public void showTip(String tip, int length) {
StageActivity activity = getStageActivity();
if (activity instanceof MainActivity) {
((MainActivity) activity).showTip(tip, length);
} else {
Toast.makeText(getApplication(), tip,
length == BaseScene.LENGTH_LONG ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.content.Context
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.ui.MainActivity
abstract class EhCallback<E : Fragment?, T>(context: Context) : EhClient.Callback<T> {
val application: EhApplication
init {
application = context.applicationContext as EhApplication
}
val content: Context
get() {
var context: Context? = application.topActivity
if (context == null) {
context = application
}
return context
}
fun showTip(@StringRes id: Int, length: Int) {
val activity = application.topActivity
if (activity is MainActivity) {
activity.showTip(id, length)
} else {
Toast.makeText(
application, id,
if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
).show()
}
}
fun showTip(tip: String, length: Int) {
val activity = application.topActivity
if (activity is MainActivity) {
activity.showTip(tip, length)
} else {
Toast.makeText(
application, tip,
if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
).show()
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,245 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.annotation.SuppressLint;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.hippo.drawable.TriangleDrawable;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhCacheKeyFactory;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.ehviewer.widget.TileThumb;
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager;
import com.hippo.yorozuya.ViewUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
public static final int TYPE_INVALID = -1;
public static final int TYPE_LIST = 0;
public static final int TYPE_GRID = 1;
private final LayoutInflater mInflater;
private final Resources mResources;
private final RecyclerView mRecyclerView;
private final AutoStaggeredGridLayoutManager mLayoutManager;
private final int mPaddingTopSB;
private final int mListThumbWidth;
private final int mListThumbHeight;
private final boolean mShowFavourited;
private final DownloadManager mDownloadManager;
private MarginItemDecoration mListDecoration;
private MarginItemDecoration mGirdDecoration;
private int mType = TYPE_INVALID;
public GalleryAdapter(@NonNull LayoutInflater inflater, @NonNull Resources resources,
@NonNull RecyclerView recyclerView, int type, boolean showFavourited) {
mInflater = inflater;
mResources = resources;
mRecyclerView = recyclerView;
mLayoutManager = new AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL);
mPaddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar);
mShowFavourited = showFavourited;
mRecyclerView.setAdapter(this);
mRecyclerView.setLayoutManager(mLayoutManager);
@SuppressLint("InflateParams") View calculator = inflater.inflate(R.layout.item_gallery_list_thumb_height, null);
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT);
mListThumbHeight = calculator.getMeasuredHeight();
mListThumbWidth = mListThumbHeight * 2 / 3;
setType(type);
mDownloadManager = EhApplication.getDownloadManager();
}
private void adjustPaddings() {
RecyclerView recyclerView = mRecyclerView;
recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerView.getPaddingTop() + mPaddingTopSB,
recyclerView.getPaddingRight(), recyclerView.getPaddingBottom());
}
public int getType() {
return mType;
}
public void setType(int type) {
if (type == mType) {
return;
}
mType = type;
RecyclerView recyclerView = mRecyclerView;
switch (type) {
case GalleryAdapter.TYPE_LIST -> {
int columnWidth = mResources.getDimensionPixelOffset(Settings.getDetailSizeResId());
mLayoutManager.setColumnSize(columnWidth);
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE);
if (null != mGirdDecoration) {
recyclerView.removeItemDecoration(mGirdDecoration);
}
if (null == mListDecoration) {
int interval = mResources.getDimensionPixelOffset(R.dimen.gallery_list_interval);
int paddingH = mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h);
int paddingV = mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v);
mListDecoration = new MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV);
}
recyclerView.addItemDecoration(mListDecoration);
adjustPaddings();
notifyDataSetChanged();
}
case GalleryAdapter.TYPE_GRID -> {
int columnWidth = Settings.getThumbSize();
mLayoutManager.setColumnSize(columnWidth);
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_SUITABLE_SIZE);
if (null != mListDecoration) {
recyclerView.removeItemDecoration(mListDecoration);
}
if (null == mGirdDecoration) {
int interval = mResources.getDimensionPixelOffset(R.dimen.gallery_grid_interval);
int paddingH = mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_h);
int paddingV = mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_v);
mGirdDecoration = new MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV);
}
recyclerView.addItemDecoration(mGirdDecoration);
adjustPaddings();
notifyDataSetChanged();
}
}
}
@NonNull
@Override
public GalleryHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
int layoutId = switch (viewType) {
case TYPE_LIST -> R.layout.item_gallery_list;
case TYPE_GRID -> R.layout.item_gallery_grid;
default -> throw new IllegalStateException("Unexpected value: " + viewType);
};
GalleryHolder holder = new GalleryHolder(mInflater.inflate(layoutId, parent, false));
if (viewType == TYPE_LIST) {
ViewGroup.LayoutParams lp = holder.thumb.getLayoutParams();
lp.width = mListThumbWidth;
lp.height = mListThumbHeight;
holder.thumb.setLayoutParams(lp);
}
holder.card.setOnClickListener(v -> onItemClick(holder.itemView, holder.getBindingAdapterPosition()));
holder.card.setOnLongClickListener(v -> onItemLongClick(holder.itemView, holder.getBindingAdapterPosition()));
return holder;
}
abstract void onItemClick(View view, int position);
abstract boolean onItemLongClick(View view, int position);
@Override
public int getItemViewType(int position) {
return mType;
}
@Nullable
public abstract GalleryInfo getDataAt(int position);
@SuppressLint("SetTextI18n")
@Override
public void onBindViewHolder(@NonNull GalleryHolder holder, int position) {
GalleryInfo gi = getDataAt(position);
if (null == gi) {
return;
}
switch (mType) {
case TYPE_LIST -> {
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb);
holder.title.setText(EhUtils.getSuitableTitle(gi));
holder.uploader.setText(gi.uploader);
holder.uploader.setAlpha(gi.disowned ? .5f : 1f);
holder.rating.setRating(gi.rating);
TextView category = holder.category;
String newCategoryText = EhUtils.getCategory(gi.category);
if (!newCategoryText.equals(category.getText().toString())) {
category.setText(newCategoryText);
category.setBackgroundColor(EhUtils.getCategoryColor(gi.category));
}
holder.posted.setText(gi.posted);
if (gi.pages == 0 || !Settings.getShowGalleryPages()) {
holder.pages.setText(null);
holder.pages.setVisibility(View.GONE);
} else {
holder.pages.setText(gi.pages + "P");
holder.pages.setVisibility(View.VISIBLE);
}
if (TextUtils.isEmpty(gi.simpleLanguage)) {
holder.simpleLanguage.setText(null);
holder.simpleLanguage.setVisibility(View.GONE);
} else {
holder.simpleLanguage.setText(gi.simpleLanguage);
holder.simpleLanguage.setVisibility(View.VISIBLE);
}
holder.favourited.setVisibility((mShowFavourited && gi.favoriteSlot >= -1 && gi.favoriteSlot <= 10) ? View.VISIBLE : View.GONE);
holder.downloaded.setVisibility(mDownloadManager.containDownloadInfo(gi.gid) ? View.VISIBLE : View.GONE);
}
case TYPE_GRID -> {
((TileThumb) holder.thumb).setThumbSize(gi.thumbWidth, gi.thumbHeight);
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb);
View category = holder.category;
Drawable drawable = category.getBackground();
int color = EhUtils.getCategoryColor(gi.category);
if (!(drawable instanceof TriangleDrawable)) {
drawable = new TriangleDrawable(color);
category.setBackground(drawable);
} else {
((TriangleDrawable) drawable).setColor(color);
}
holder.simpleLanguage.setText(gi.simpleLanguage);
}
}
// Update transition name
ViewCompat.setTransitionName(holder.thumb, TransitionNameFactory.getThumbTransitionName(gi.gid));
}
@IntDef({TYPE_LIST, TYPE_GRID})
@Retention(RetentionPolicy.SOURCE)
public @interface Type {
}
}
@@ -0,0 +1,231 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.content.res.Resources
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IntDef
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.hippo.drawable.TriangleDrawable
import com.hippo.easyrecyclerview.MarginItemDecoration
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.widget.TileThumb
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager
import com.hippo.yorozuya.ViewUtils
internal abstract class GalleryAdapter(
private val mInflater: LayoutInflater, private val mResources: Resources,
private val mRecyclerView: RecyclerView, type: Int, showFavourited: Boolean
) : RecyclerView.Adapter<GalleryHolder>() {
private val mLayoutManager: AutoStaggeredGridLayoutManager =
AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
private val mPaddingTopSB: Int =
mResources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar)
private val mListThumbWidth: Int
private val mListThumbHeight: Int
private val mShowFavourited: Boolean
private val mDownloadManager: DownloadManager
private var mListDecoration: MarginItemDecoration? = null
private var mGirdDecoration: MarginItemDecoration? = null
private var mType = TYPE_INVALID
var type: Int
get() = mType
set(type) {
if (type == mType) {
return
}
mType = type
val recyclerView = mRecyclerView
when (type) {
TYPE_LIST -> {
val columnWidth =
mResources.getDimensionPixelOffset(Settings.getDetailSizeResId())
mLayoutManager.setColumnSize(columnWidth)
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE)
if (null != mGirdDecoration) {
recyclerView.removeItemDecoration(mGirdDecoration!!)
}
if (null == mListDecoration) {
val interval =
mResources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
val paddingH =
mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)
val paddingV =
mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)
mListDecoration =
MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV)
}
recyclerView.addItemDecoration(mListDecoration!!)
adjustPaddings()
notifyDataSetChanged()
}
TYPE_GRID -> {
val columnWidth = Settings.getThumbSize()
mLayoutManager.setColumnSize(columnWidth)
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_SUITABLE_SIZE)
if (null != mListDecoration) {
recyclerView.removeItemDecoration(mListDecoration!!)
}
if (null == mGirdDecoration) {
val interval =
mResources.getDimensionPixelOffset(R.dimen.gallery_grid_interval)
val paddingH =
mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_h)
val paddingV =
mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_v)
mGirdDecoration =
MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV)
}
recyclerView.addItemDecoration(mGirdDecoration!!)
adjustPaddings()
notifyDataSetChanged()
}
}
}
init {
mShowFavourited = showFavourited
mRecyclerView.adapter = this
mRecyclerView.layoutManager = mLayoutManager
@SuppressLint("InflateParams") val calculator =
mInflater.inflate(R.layout.item_gallery_list_thumb_height, null)
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT)
mListThumbHeight = calculator.measuredHeight
mListThumbWidth = mListThumbHeight * 2 / 3
this.type = type
mDownloadManager = downloadManager
}
private fun adjustPaddings() {
val recyclerView = mRecyclerView
recyclerView.setPadding(
recyclerView.paddingLeft, recyclerView.paddingTop + mPaddingTopSB,
recyclerView.paddingRight, recyclerView.paddingBottom
)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryHolder {
val layoutId = when (viewType) {
TYPE_LIST -> R.layout.item_gallery_list
TYPE_GRID -> R.layout.item_gallery_grid
else -> throw IllegalStateException("Unexpected value: $viewType")
}
val holder = GalleryHolder(mInflater.inflate(layoutId, parent, false))
if (viewType == TYPE_LIST) {
val lp = holder.thumb.layoutParams
lp.width = mListThumbWidth
lp.height = mListThumbHeight
holder.thumb.layoutParams = lp
}
holder.card.setOnClickListener {
onItemClick(
holder.itemView,
holder.bindingAdapterPosition
)
}
holder.card.setOnLongClickListener {
onItemLongClick(
holder.itemView,
holder.bindingAdapterPosition
)
}
return holder
}
abstract fun onItemClick(view: View, position: Int)
abstract fun onItemLongClick(view: View, position: Int): Boolean
override fun getItemViewType(position: Int): Int {
return mType
}
abstract fun getDataAt(position: Int): GalleryInfo?
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: GalleryHolder, position: Int) {
val gi = getDataAt(position) ?: return
when (mType) {
TYPE_LIST -> {
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb!!)
holder.title!!.text = EhUtils.getSuitableTitle(gi)
holder.uploader!!.text = gi.uploader
holder.uploader.alpha = if (gi.disowned) .5f else 1f
holder.rating!!.rating = gi.rating
val category = holder.category
val newCategoryText = EhUtils.getCategory(gi.category)
if (newCategoryText != category.text.toString()) {
category.text = newCategoryText
category.setBackgroundColor(EhUtils.getCategoryColor(gi.category))
}
holder.posted!!.text = gi.posted
if (gi.pages == 0 || !Settings.getShowGalleryPages()) {
holder.pages?.text = null
holder.pages!!.visibility = View.GONE
} else {
holder.pages!!.text = gi.pages.toString() + "P"
holder.pages.visibility = View.VISIBLE
}
if (TextUtils.isEmpty(gi.simpleLanguage)) {
holder.simpleLanguage.text = null
holder.simpleLanguage.visibility = View.GONE
} else {
holder.simpleLanguage.text = gi.simpleLanguage
holder.simpleLanguage.visibility = View.VISIBLE
}
holder.favourited!!.visibility =
if (mShowFavourited && gi.favoriteSlot >= -1 && gi.favoriteSlot <= 10) View.VISIBLE else View.GONE
holder.downloaded!!.visibility =
if (mDownloadManager.containDownloadInfo(gi.gid)) View.VISIBLE else View.GONE
}
TYPE_GRID -> {
(holder.thumb as TileThumb).setThumbSize(gi.thumbWidth, gi.thumbHeight)
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb!!)
val category: View = holder.category
var drawable = category.background
val color = EhUtils.getCategoryColor(gi.category)
if (drawable !is TriangleDrawable) {
drawable = TriangleDrawable(color)
category.background = drawable
} else {
drawable.setColor(color)
}
holder.simpleLanguage.text = gi.simpleLanguage
}
}
}
@IntDef(TYPE_LIST, TYPE_GRID)
@Retention(AnnotationRetention.SOURCE)
annotation class Type
companion object {
const val TYPE_INVALID = -1
const val TYPE_LIST = 0
const val TYPE_GRID = 1
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,914 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.animation.Animator
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Looper
import android.text.Html
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan
import android.text.style.URLSpan
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.hippo.app.BaseDialogBuilder
import com.hippo.easyrecyclerview.EasyRecyclerView
import com.hippo.ehviewer.R
import com.hippo.ehviewer.UrlOpener
import com.hippo.ehviewer.WindowInsetsAnimationHelper
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhFilter
import com.hippo.ehviewer.client.EhRequest
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryComment
import com.hippo.ehviewer.client.data.GalleryCommentList
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.parser.VoteCommentParser
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.scene.GalleryListScene.Companion.toStartArgs
import com.hippo.text.URLImageGetter
import com.hippo.util.ExceptionUtils
import com.hippo.util.ReadableTime
import com.hippo.util.TextUrl
import com.hippo.util.addTextToClipboard
import com.hippo.view.ViewTransition
import com.hippo.widget.FabLayout
import com.hippo.widget.LinkifyTextView
import com.hippo.widget.ObservedTextView
import com.hippo.yorozuya.AnimationUtils
import com.hippo.yorozuya.SimpleAnimatorListener
import com.hippo.yorozuya.StringUtils
import com.hippo.yorozuya.ViewUtils
import com.hippo.yorozuya.collect.IntList
import rikka.core.res.resolveColor
import kotlin.math.hypot
class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefreshListener {
private val mCallback = EditPanelOnBackPressedCallback()
private var mGalleryDetail: GalleryDetail? = null
private var mRecyclerView: EasyRecyclerView? = null
private var mFabLayout: FabLayout? = null
private var mFab: FloatingActionButton? = null
private var mEditPanel: View? = null
private var mSendImage: ImageView? = null
private var mEditText: EditText? = null
private var mAdapter: CommentAdapter? = null
private var mViewTransition: ViewTransition? = null
private var mRefreshLayout: SwipeRefreshLayout? = null
private var mSendDrawable: Drawable? = null
private var mPencilDrawable: Drawable? = null
private var mCommentId: Long = 0
private var mInAnimation = false
private var mShowAllComments = false
private var mRefreshingComments = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(mCallback)
if (savedInstanceState == null) {
onInit()
} else {
onRestore(savedInstanceState)
}
}
private fun handleArgs(args: Bundle?) {
if (args == null) {
return
}
mGalleryDetail = args.getParcelable(KEY_GALLERY_DETAIL)
mShowAllComments =
mGalleryDetail != null && mGalleryDetail!!.comments != null && !mGalleryDetail!!.comments!!.hasMore
}
private fun onInit() {
handleArgs(arguments)
}
private fun onRestore(savedInstanceState: Bundle) {
mGalleryDetail = savedInstanceState.getParcelable(KEY_GALLERY_DETAIL)
mShowAllComments =
mGalleryDetail != null && mGalleryDetail!!.comments != null && !mGalleryDetail!!.comments!!.hasMore
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(KEY_GALLERY_DETAIL, mGalleryDetail)
}
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.scene_gallery_comments, container, false) as ViewGroup
mRecyclerView = ViewUtils.`$$`(view, R.id.recycler_view) as EasyRecyclerView
setLiftOnScrollTargetView(mRecyclerView)
val tip = ViewUtils.`$$`(view, R.id.tip) as TextView
mEditPanel = ViewUtils.`$$`(view, R.id.edit_panel)
mSendImage = ViewUtils.`$$`(mEditPanel, R.id.send) as ImageView
mEditText = ViewUtils.`$$`(mEditPanel, R.id.edit_text) as EditText
mFabLayout = ViewUtils.`$$`(view, R.id.fab_layout) as FabLayout
mFab = ViewUtils.`$$`(view, R.id.fab) as FloatingActionButton
mRefreshLayout = ViewUtils.`$$`(view, R.id.refresh_layout) as SwipeRefreshLayout
// Workaround for fab and edittext render out of screen
view.removeView(mFabLayout)
view.removeView(mEditPanel!!.parent as View)
assert(container != null)
container!!.addView(mFabLayout)
container.addView(mEditPanel!!.parent as View)
ViewCompat.setWindowInsetsAnimationCallback(
view, WindowInsetsAnimationHelper(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
mEditPanel,
mFabLayout
)
)
mRefreshLayout!!.setOnRefreshListener(this)
val context = requireContext()
val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
tip.setCompoundDrawables(null, drawable, null, null)
mSendDrawable = ContextCompat.getDrawable(context, R.drawable.v_send_dark_x24)
mPencilDrawable = ContextCompat.getDrawable(context, R.drawable.v_pencil_dark_x24)
mAdapter = CommentAdapter()
mRecyclerView!!.adapter = mAdapter
mRecyclerView!!.layoutManager = LinearLayoutManager(
context,
RecyclerView.VERTICAL, false
)
mRecyclerView!!.setHasFixedSize(true)
// Cancel change animator
val itemAnimator = mRecyclerView!!.itemAnimator
if (itemAnimator is DefaultItemAnimator) {
itemAnimator.supportsChangeAnimations = false
}
mSendImage!!.setOnClickListener(this)
mFab!!.setOnClickListener(this)
addAboveSnackView(mEditPanel)
addAboveSnackView(mFabLayout)
mViewTransition = ViewTransition(mRecyclerView, tip)
updateView(false)
return view
}
override fun onDestroyView() {
super.onDestroyView()
mCallback.remove()
if (null != mRecyclerView) {
mRecyclerView!!.stopScroll()
mRecyclerView = null
}
if (null != mEditPanel) {
removeAboveSnackView(mEditPanel)
mEditPanel = null
}
if (null != mFabLayout) {
removeAboveSnackView(mFabLayout)
mFabLayout = null
}
mFab = null
mSendImage = null
mEditText = null
mAdapter = null
mViewTransition = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(R.string.gallery_comments)
setNavigationIcon(R.drawable.v_arrow_left_dark_x24)
}
private fun showFilterCommenterDialog(commenter: String?, position: Int) {
val context = context
if (context == null || commenter == null) {
return
}
BaseDialogBuilder(context)
.setMessage(getString(R.string.filter_the_commenter, commenter))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val filter = Filter()
filter.mode = EhFilter.MODE_COMMENTER
filter.text = commenter
EhFilter.addFilter(filter)
hideComment(position)
showTip(R.string.filter_added, LENGTH_SHORT)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun hideComment(position: Int) {
if (mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null) {
return
}
val oldCommentsList = mGalleryDetail!!.comments!!.comments
val newCommentsList = arrayOfNulls<GalleryComment>(
oldCommentsList!!.size - 1
)
var i = 0
var j = 0
while (i < oldCommentsList.size) {
if (i != position) {
newCommentsList[j] = oldCommentsList[i]
j++
}
i++
}
mGalleryDetail!!.comments!!.comments = newCommentsList.requireNoNulls()
mAdapter!!.notifyDataSetChanged()
updateView(true)
}
private fun voteComment(id: Long, vote: Int) {
val context = context
val activity = mainActivity
if (null == context || null == activity) {
return
}
val request = EhRequest()
.setMethod(EhClient.METHOD_VOTE_COMMENT)
.setArgs(
mGalleryDetail!!.apiUid,
mGalleryDetail!!.apiKey,
mGalleryDetail!!.gid,
mGalleryDetail!!.token,
id,
vote
)
.setCallback(VoteCommentListener(context))
request.enqueue(this)
}
@SuppressLint("InflateParams")
fun showVoteStatusDialog(context: Context?, voteStatus: String?) {
var context = context
val temp = StringUtils.split(voteStatus, ',')
val length = temp.size
val userArray = arrayOfNulls<String>(length)
val voteArray = arrayOfNulls<String>(length)
for (i in 0 until length) {
val str = StringUtils.trim(temp[i])
val index = str.lastIndexOf(' ')
if (index < 0) {
Log.d(TAG, "Something wrong happened about vote state")
userArray[i] = str
voteArray[i] = ""
} else {
userArray[i] = StringUtils.trim(str.substring(0, index))
voteArray[i] = StringUtils.trim(str.substring(index + 1))
}
}
val builder = BaseDialogBuilder(context!!)
context = builder.context
val inflater = LayoutInflater.from(context)
val rv = inflater.inflate(R.layout.dialog_recycler_view, null) as EasyRecyclerView
rv.adapter = object : RecyclerView.Adapter<InfoHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InfoHolder {
return InfoHolder(inflater.inflate(R.layout.item_drawer_favorites, parent, false))
}
override fun onBindViewHolder(holder: InfoHolder, position: Int) {
holder.key.text = userArray[position]
holder.value.text = voteArray[position]
}
override fun getItemCount(): Int {
return length
}
}
rv.layoutManager = LinearLayoutManager(context)
rv.clipToPadding = false
builder.setView(rv).show()
}
private fun showCommentDialog(position: Int) {
val context = context
if (context == null || mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null || position >= mGalleryDetail!!.comments!!.comments!!.size || position < 0) {
return
}
val comment = mGalleryDetail!!.comments!!.comments!![position]
val menu: MutableList<String> = ArrayList()
val menuId = IntList()
val resources = context.resources
menu.add(resources.getString(R.string.copy_comment_text))
menuId.add(R.id.copy)
if (!comment.uploader && !comment.editable) {
menu.add(resources.getString(R.string.block_commenter))
menuId.add(R.id.block_commenter)
}
if (comment.editable) {
menu.add(resources.getString(R.string.edit_comment))
menuId.add(R.id.edit_comment)
}
if (comment.voteUpAble) {
menu.add(resources.getString(if (comment.voteUpEd) R.string.cancel_vote_up else R.string.vote_up))
menuId.add(R.id.vote_up)
}
if (comment.voteDownAble) {
menu.add(resources.getString(if (comment.voteDownEd) R.string.cancel_vote_down else R.string.vote_down))
menuId.add(R.id.vote_down)
}
if (!TextUtils.isEmpty(comment.voteState)) {
menu.add(resources.getString(R.string.check_vote_status))
menuId.add(R.id.check_vote_status)
}
BaseDialogBuilder(context)
.setItems(menu.toTypedArray()) { _: DialogInterface?, which: Int ->
if (which < 0 || which >= menuId.size()) {
return@setItems
}
val id = menuId[which]
if (id == R.id.copy) {
requireActivity().addTextToClipboard(comment.comment, false)
} else if (id == R.id.block_commenter) {
showFilterCommenterDialog(comment.user, position)
} else if (id == R.id.vote_up) {
voteComment(comment.id, 1)
} else if (id == R.id.vote_down) {
voteComment(comment.id, -1)
} else if (id == R.id.check_vote_status) {
showVoteStatusDialog(context, comment.voteState)
} else if (id == R.id.edit_comment) {
prepareEditComment(comment.id)
if (!mInAnimation && mEditPanel != null && mEditPanel!!.visibility != View.VISIBLE) {
showEditPanel(true)
}
}
}.show()
}
fun onItemClick(parent: EasyRecyclerView?, view2: View?, position: Int): Boolean {
val activity = mainActivity ?: return false
val holder = parent!!.getChildViewHolder(view2!!)
if (holder is ActualCommentHolder) {
val span = holder.comment.currentSpan
holder.comment.clearCurrentSpan()
if (span is URLSpan) {
UrlOpener.openUrl(activity, span.url, true, mGalleryDetail)
} else {
showCommentDialog(position)
}
} else if (holder is MoreCommentHolder && !mRefreshingComments && mAdapter != null) {
mRefreshingComments = true
mShowAllComments = true
mAdapter!!.notifyItemChanged(position)
val url = galleryDetailUrl
if (url != null) {
// Request
val request = EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)
.setArgs(url)
.setCallback(RefreshCommentListener(activity))
request.enqueue(this)
}
}
return true
}
private fun updateView(animation: Boolean) {
if (null == mViewTransition) {
return
}
if (mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null || mGalleryDetail!!.comments!!.comments!!.isEmpty()) {
mViewTransition!!.showView(1, animation)
} else {
mViewTransition!!.showView(0, animation)
}
}
private fun prepareNewComment() {
mCommentId = 0
if (mSendImage != null) {
mSendImage!!.setImageDrawable(mSendDrawable)
}
}
private fun prepareEditComment(commentId: Long) {
mCommentId = commentId
if (mSendImage != null) {
mSendImage!!.setImageDrawable(mPencilDrawable)
}
}
private fun showEditPanelWithAnimation() {
if (null == mFab || null == mEditPanel) {
return
}
mInAnimation = true
mFab!!.translationX = 0.0f
mFab!!.translationY = 0.0f
mFab!!.scaleX = 1.0f
mFab!!.scaleY = 1.0f
val fabEndX = mEditPanel!!.left + mEditPanel!!.width / 2 - mFab!!.width / 2
val fabEndY = mEditPanel!!.top + mEditPanel!!.height / 2 - mFab!!.height / 2
mFab!!.animate().x(fabEndX.toFloat()).y(fabEndY.toFloat()).scaleX(0.0f).scaleY(0.0f)
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
.setDuration(300L).setListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
if (null == mFab || null == mEditPanel) {
return
}
(mFab as View).visibility = View.INVISIBLE
mEditPanel!!.visibility = View.VISIBLE
val halfW = mEditPanel!!.width / 2
val halfH = mEditPanel!!.height / 2
val animator = ViewAnimationUtils.createCircularReveal(
mEditPanel,
halfW,
halfH,
0f,
hypot(halfW.toDouble(), halfH.toDouble()).toFloat()
).setDuration(300L)
animator.addListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(a: Animator) {
mInAnimation = false
}
})
animator.start()
}
}).start()
}
private fun showEditPanel(animation: Boolean) {
mCallback.isEnabled = true
if (animation) {
showEditPanelWithAnimation()
} else {
if (null == mFab || null == mEditPanel) {
return
}
(mFab as View).visibility = View.INVISIBLE
mEditPanel!!.visibility = View.VISIBLE
}
}
private fun hideEditPanelWithAnimation() {
if (null == mFab || null == mEditPanel) {
return
}
mInAnimation = true
val halfW = mEditPanel!!.width / 2
val halfH = mEditPanel!!.height / 2
val animator = ViewAnimationUtils.createCircularReveal(
mEditPanel,
halfW,
halfH,
hypot(halfW.toDouble(), halfH.toDouble()).toFloat(),
0.0f
).setDuration(300L)
animator.addListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(a: Animator) {
if (null == mFab || null == mEditPanel) {
return
}
if (Looper.myLooper() != Looper.getMainLooper()) {
// Some devices may run this block in non-UI thread.
// It might be a bug of Android OS.
// Check it here to avoid crash.
return
}
mEditPanel!!.visibility = View.GONE
(mFab as View).visibility = View.VISIBLE
val fabStartX = mEditPanel!!.left + mEditPanel!!.width / 2 - mFab!!.width / 2
val fabStartY = mEditPanel!!.top + mEditPanel!!.height / 2 - mFab!!.height / 2
mFab!!.x = fabStartX.toFloat()
mFab!!.y = fabStartY.toFloat()
mFab!!.scaleX = 0.0f
mFab!!.scaleY = 0.0f
mFab!!.rotation = -45.0f
mFab!!.animate().translationX(0.0f).translationY(0.0f).scaleX(1.0f).scaleY(1.0f)
.rotation(0.0f)
.setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)
.setDuration(300L).setListener(object : SimpleAnimatorListener() {
override fun onAnimationEnd(animation: Animator) {
mInAnimation = false
}
}).start()
}
})
animator.start()
}
private fun hideEditPanel(animation: Boolean) {
mCallback.isEnabled = false
hideSoftInput()
if (animation) {
hideEditPanelWithAnimation()
} else {
if (null == mFab || null == mEditPanel) {
return
}
(mFab as View).visibility = View.VISIBLE
mEditPanel!!.visibility = View.INVISIBLE
}
}
private val galleryDetailUrl: String?
get() = if (mGalleryDetail != null && mGalleryDetail!!.gid != -1L && mGalleryDetail!!.token != null) {
EhUrl.getGalleryDetailUrl(
mGalleryDetail!!.gid,
mGalleryDetail!!.token,
0,
mShowAllComments
)
} else {
null
}
override fun onClick(v: View) {
val context = context
val activity = mainActivity
if (null == context || null == activity || null == mEditText) {
return
}
if (mFab === v) {
if (!mInAnimation) {
prepareNewComment()
showEditPanel(true)
}
} else if (mSendImage === v) {
if (!mInAnimation) {
val comment = mEditText!!.text.toString()
if (TextUtils.isEmpty(comment)) {
// Comment is empty
return
}
val url = galleryDetailUrl ?: return
// Request
val request = EhRequest()
.setMethod(EhClient.METHOD_GET_COMMENT_GALLERY)
.setArgs(
url,
comment,
if (mCommentId != 0L) mCommentId.toString() else null
)
.setCallback(CommentGalleryListener(context, mCommentId))
request.enqueue(this)
hideSoftInput()
hideEditPanel(true)
}
}
}
private fun onRefreshGallerySuccess(result: GalleryCommentList?) {
if (mGalleryDetail == null || mAdapter == null) {
return
}
mRefreshLayout!!.isRefreshing = false
mRefreshingComments = false
mGalleryDetail!!.comments = result
mAdapter!!.notifyDataSetChanged()
updateView(true)
}
private fun onRefreshGalleryFailure() {
if (mAdapter == null) {
return
}
mRefreshLayout!!.isRefreshing = false
mRefreshingComments = false
val position = mAdapter!!.itemCount - 1
if (position >= 0) {
mAdapter!!.notifyItemChanged(position)
}
}
private fun onCommentGallerySuccess(result: GalleryCommentList) {
if (mGalleryDetail == null || mAdapter == null) {
return
}
mGalleryDetail!!.comments = result
mAdapter!!.notifyDataSetChanged()
// Remove text
if (mEditText != null) {
mEditText!!.setText("")
}
updateView(true)
}
private fun onVoteCommentSuccess(result: VoteCommentParser.Result) {
if (mAdapter == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null) {
return
}
var position = -1
var i = 0
val n = mGalleryDetail!!.comments!!.comments!!.size
while (i < n) {
val comment = mGalleryDetail!!.comments!!.comments!![i]
if (comment.id == result.id) {
position = i
break
}
i++
}
if (-1 == position) {
Log.d(TAG, "Can't find comment with id " + result.id)
return
}
// Update comment
val comment = mGalleryDetail!!.comments!!.comments!![position]
comment.score = result.score
if (result.expectVote > 0) {
comment.voteUpEd = 0 != result.vote
comment.voteDownEd = false
} else {
comment.voteDownEd = 0 != result.vote
comment.voteUpEd = false
}
mAdapter!!.notifyItemChanged(position)
}
override fun onRefresh() {
if (!mRefreshingComments && mAdapter != null) {
val activity = requireActivity() as MainActivity
mRefreshingComments = true
val url = galleryDetailUrl
if (url != null) {
// Request
val request = EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)
.setArgs(url)
.setCallback(RefreshCommentListener(activity))
request.enqueue(this)
}
}
}
private class InfoHolder(itemView: View?) : RecyclerView.ViewHolder(
itemView!!
) {
val key: TextView
val value: TextView
init {
key = ViewUtils.`$$`(itemView, R.id.key) as TextView
value = ViewUtils.`$$`(itemView, R.id.value) as TextView
}
}
private abstract class CommentHolder(inflater: LayoutInflater, resId: Int, parent: ViewGroup?) :
RecyclerView.ViewHolder(inflater.inflate(resId, parent, false))
private class MoreCommentHolder(inflater: LayoutInflater, parent: ViewGroup?) :
CommentHolder(inflater, R.layout.item_gallery_comment_more, parent)
private class ProgressCommentHolder(inflater: LayoutInflater, parent: ViewGroup?) :
CommentHolder(inflater, R.layout.item_gallery_comment_progress, parent)
private inner class RefreshCommentListener(context: Context) :
EhCallback<GalleryCommentsScene?, GalleryDetail>(context) {
override fun onSuccess(result: GalleryDetail) {
val scene = this@GalleryCommentsScene
scene.onRefreshGallerySuccess(result.comments)
}
override fun onFailure(e: Exception) {
val scene = this@GalleryCommentsScene
scene.onRefreshGalleryFailure()
}
override fun onCancel() {}
}
private inner class CommentGalleryListener(context: Context, private val mCommentId: Long) :
EhCallback<GalleryCommentsScene?, GalleryCommentList>(context) {
override fun onSuccess(result: GalleryCommentList) {
showTip(
if (mCommentId != 0L) R.string.edit_comment_successfully else R.string.comment_successfully,
LENGTH_SHORT
)
val scene = this@GalleryCommentsScene
scene.onCommentGallerySuccess(result)
}
override fun onFailure(e: Exception) {
showTip(
"""
${content.getString(if (mCommentId != 0L) R.string.edit_comment_failed else R.string.comment_failed)}
${ExceptionUtils.getReadableString(e)}
""".trimIndent(), LENGTH_LONG
)
}
override fun onCancel() {}
}
private inner class VoteCommentListener(context: Context) :
EhCallback<GalleryCommentsScene?, VoteCommentParser.Result>(context) {
override fun onSuccess(result: VoteCommentParser.Result) {
showTip(
if (result.expectVote > 0) (if (0 != result.vote) R.string.vote_up_successfully else R.string.cancel_vote_up_successfully) else if (0 != result.vote) R.string.vote_down_successfully else R.string.cancel_vote_down_successfully,
LENGTH_SHORT
)
val scene = this@GalleryCommentsScene
scene.onVoteCommentSuccess(result)
}
override fun onFailure(e: Exception) {
showTip(R.string.vote_failed, LENGTH_LONG)
}
override fun onCancel() {}
}
private inner class ActualCommentHolder(inflater: LayoutInflater, parent: ViewGroup?) :
CommentHolder(inflater, R.layout.item_gallery_comment, parent) {
private val user: TextView = itemView.findViewById(R.id.user)
private val time: TextView = itemView.findViewById(R.id.time)
val comment: LinkifyTextView = itemView.findViewById(R.id.comment)
private fun generateComment(
context: Context,
textView: ObservedTextView,
comment: GalleryComment
): CharSequence {
val sp = Html.fromHtml(
comment.comment,
Html.FROM_HTML_MODE_LEGACY,
URLImageGetter(textView),
null
)
val ssb = SpannableStringBuilder(sp)
if (0L != comment.id && 0 != comment.score) {
val score = comment.score
val scoreString = if (score > 0) "+$score" else score.toString()
val ss = SpannableString(scoreString)
ss.setSpan(
RelativeSizeSpan(0.8f),
0,
scoreString.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ss.setSpan(
StyleSpan(Typeface.BOLD),
0,
scoreString.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ss.setSpan(
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
0,
scoreString.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ssb.append(" ").append(ss)
}
if (comment.lastEdited != 0L) {
val str = context.getString(
R.string.last_edited,
ReadableTime.getTimeAgo(comment.lastEdited)
)
val ss = SpannableString(str)
ss.setSpan(
RelativeSizeSpan(0.8f),
0,
str.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ss.setSpan(
StyleSpan(Typeface.BOLD),
0,
str.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ss.setSpan(
ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),
0, str.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
ssb.append("\n\n").append(ss)
}
return TextUrl.handleTextUrl(ssb)
}
fun bind(value: GalleryComment) {
user.text = if (value.uploader) getString(
R.string.comment_user_uploader,
value.user
) else value.user
user.setOnClickListener {
val lub = ListUrlBuilder()
lub.mode = ListUrlBuilder.MODE_UPLOADER
lub.keyword = value.user
navigate(R.id.galleryListScene, lub.toStartArgs(), true)
}
time.text = ReadableTime.getTimeAgo(value.time)
comment.text = generateComment(comment.context, comment, value)
}
}
private inner class CommentAdapter : RecyclerView.Adapter<CommentHolder>() {
private val mInflater: LayoutInflater = layoutInflater
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentHolder {
return when (viewType) {
TYPE_COMMENT -> ActualCommentHolder(mInflater, parent)
TYPE_MORE -> MoreCommentHolder(mInflater, parent)
TYPE_PROGRESS -> ProgressCommentHolder(mInflater, parent)
else -> throw IllegalStateException("Invalid view type: $viewType")
}
}
override fun onBindViewHolder(holder: CommentHolder, position: Int) {
val context = context
if (context == null || mGalleryDetail == null || mGalleryDetail!!.comments == null) {
return
}
holder.itemView.setOnClickListener {
onItemClick(
mRecyclerView,
holder.itemView,
position
)
}
holder.itemView.isClickable = true
holder.itemView.isFocusable = true
if (holder is ActualCommentHolder) {
holder.bind(mGalleryDetail!!.comments!!.comments!![position])
}
}
override fun getItemCount(): Int {
return if (mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null) {
0
} else if (mGalleryDetail!!.comments!!.hasMore) {
mGalleryDetail!!.comments!!.comments!!.size + 1
} else {
mGalleryDetail!!.comments!!.comments!!.size
}
}
override fun getItemViewType(position: Int): Int {
return if (position >= mGalleryDetail!!.comments!!.comments!!.size) {
if (mRefreshingComments) TYPE_PROGRESS else TYPE_MORE
} else {
TYPE_COMMENT
}
}
}
internal inner class EditPanelOnBackPressedCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
if (!mInAnimation && null != mEditPanel && mEditPanel!!.visibility == View.VISIBLE) {
hideEditPanel(true)
}
}
}
companion object {
val TAG: String = GalleryCommentsScene::class.java.simpleName
const val KEY_API_UID = "api_uid"
const val KEY_API_KEY = "api_key"
const val KEY_GID = "gid"
const val KEY_TOKEN = "token"
const val KEY_COMMENT_LIST = "comment_list"
const val KEY_GALLERY_DETAIL = "gallery_detail"
private const val TYPE_COMMENT = 0
private const val TYPE_MORE = 1
private const val TYPE_PROGRESS = 2
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,59 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.card.MaterialCardView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.widget.SimpleRatingView;
import com.hippo.widget.LoadImageView;
class GalleryHolder extends RecyclerView.ViewHolder {
public final LoadImageView thumb;
public final TextView title;
public final TextView uploader;
public final SimpleRatingView rating;
public final TextView category;
public final TextView posted;
public final TextView pages;
public final TextView simpleLanguage;
public final ImageView favourited;
public final ImageView downloaded;
public final MaterialCardView card;
public GalleryHolder(View itemView) {
super(itemView);
thumb = itemView.findViewById(R.id.thumb);
title = itemView.findViewById(R.id.title);
uploader = itemView.findViewById(R.id.uploader);
rating = itemView.findViewById(R.id.rating);
category = itemView.findViewById(R.id.category);
posted = itemView.findViewById(R.id.posted);
pages = itemView.findViewById(R.id.pages);
simpleLanguage = itemView.findViewById(R.id.simple_language);
favourited = itemView.findViewById(R.id.favourited);
downloaded = itemView.findViewById(R.id.downloaded);
card = itemView.findViewById(R.id.card);
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import com.hippo.ehviewer.R
import com.hippo.ehviewer.widget.SimpleRatingView
import com.hippo.widget.LoadImageView
internal class GalleryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@JvmField
val thumb: LoadImageView = itemView.findViewById(R.id.thumb)
@JvmField
val title: TextView? = itemView.findViewById(R.id.title)
@JvmField
val uploader: TextView? = itemView.findViewById(R.id.uploader)
@JvmField
val rating: SimpleRatingView? = itemView.findViewById(R.id.rating)
@JvmField
val category: TextView = itemView.findViewById(R.id.category)
@JvmField
val posted: TextView? = itemView.findViewById(R.id.posted)
@JvmField
val pages: TextView? = itemView.findViewById(R.id.pages)
@JvmField
val simpleLanguage: TextView = itemView.findViewById(R.id.simple_language)
@JvmField
val favourited: ImageView? = itemView.findViewById(R.id.favourited)
@JvmField
val downloaded: ImageView? = itemView.findViewById(R.id.downloaded)
@JvmField
val card: MaterialCardView = itemView.findViewById(R.id.card)
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,435 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.slider.Slider;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.GalleryPreview;
import com.hippo.ehviewer.client.data.PreviewSet;
import com.hippo.ehviewer.client.exception.EhException;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.SceneFragment;
import com.hippo.widget.ContentLayout;
import com.hippo.widget.LoadImageView;
import com.hippo.widget.recyclerview.AutoGridLayoutManager;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ViewUtils;
import java.util.ArrayList;
import java.util.Locale;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
public class GalleryPreviewsScene extends BaseToolbarScene {
public static final String KEY_GALLERY_INFO = "gallery_info";
private final static String KEY_HAS_FIRST_REFRESH = "has_first_refresh";
/*---------------
Whole life cycle
---------------*/
@Nullable
private EhClient mClient;
@Nullable
private GalleryInfo mGalleryInfo;
@Nullable
private EasyRecyclerView mRecyclerView;
@Nullable
private GalleryPreviewAdapter mAdapter;
@Nullable
private GalleryPreviewHelper mHelper;
private boolean mHasFirstRefresh = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = getContext();
AssertUtils.assertNotNull(context);
mClient = EhClient.INSTANCE;
if (savedInstanceState == null) {
onInit();
} else {
onRestore(savedInstanceState);
}
}
private void onInit() {
Bundle args = getArguments();
if (args == null) {
return;
}
mGalleryInfo = args.getParcelable(KEY_GALLERY_INFO);
}
private void onRestore(@NonNull Bundle savedInstanceState) {
mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO);
mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
boolean hasFirstRefresh;
if (mHelper != null && 1 == mHelper.getShownViewIndex()) {
hasFirstRefresh = false;
} else {
hasFirstRefresh = mHasFirstRefresh;
}
outState.putBoolean(KEY_HAS_FIRST_REFRESH, hasFirstRefresh);
outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo);
}
@NonNull
@Override
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
ContentLayout mContentLayout = (ContentLayout) inflater.inflate(
R.layout.scene_gallery_previews, container, false);
mContentLayout.hideFastScroll();
mRecyclerView = mContentLayout.getRecyclerView();
setLiftOnScrollTargetView(mRecyclerView);
Context context = getContext();
AssertUtils.assertNotNull(context);
mAdapter = new GalleryPreviewAdapter();
mRecyclerView.setAdapter(mAdapter);
int columnWidth = Settings.getThumbSize();
AutoGridLayoutManager layoutManager = new AutoGridLayoutManager(context, columnWidth, LayoutUtils.dp2pix(context, 16));
layoutManager.setStrategy(AutoGridLayoutManager.STRATEGY_SUITABLE_SIZE);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setClipToPadding(false);
int padding = LayoutUtils.dp2pix(context, 4);
MarginItemDecoration decoration = new MarginItemDecoration(padding, padding, padding, padding, padding);
mRecyclerView.addItemDecoration(decoration);
mHelper = new GalleryPreviewHelper();
mContentLayout.setHelper(mHelper);
// Only refresh for the first time
if (!mHasFirstRefresh) {
mHasFirstRefresh = true;
mHelper.firstRefresh();
}
return mContentLayout;
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (null != mHelper) {
if (1 == mHelper.getShownViewIndex()) {
mHasFirstRefresh = false;
}
}
if (null != mRecyclerView) {
mRecyclerView.stopScroll();
mRecyclerView = null;
}
mAdapter = null;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setTitle(R.string.gallery_previews);
setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
if (mGalleryInfo != null) {
if (((GalleryDetail) mGalleryInfo).previewPages > 2)
showMenu(R.menu.scene_gallery_previews);
}
}
@Override
public boolean onMenuItemClick(@NonNull MenuItem item) {
Context context = getContext();
if (null == context) {
return false;
}
int id = item.getItemId();
if (id == R.id.action_go_to) {
if (mHelper == null) {
return true;
}
int pages = mHelper.getPages();
if (pages > 1 && mHelper.canGoTo()) {
GoToDialogHelper helper = new GoToDialogHelper(pages, mHelper.getPageForTop());
AlertDialog dialog = new BaseDialogBuilder(context).setTitle(R.string.go_to)
.setView(R.layout.dialog_go_to)
.setPositiveButton(android.R.string.ok, null)
.create();
dialog.show();
helper.setDialog(dialog);
}
return true;
}
return false;
}
public boolean onItemClick(int position) {
Context context = getContext();
if (null != context && null != mHelper && null != mGalleryInfo) {
GalleryPreview p = mHelper.getDataAtEx(position);
if (p != null) {
Intent intent = new Intent(context, ReaderActivity.class);
intent.setAction(ReaderActivity.ACTION_EH);
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, mGalleryInfo);
intent.putExtra(ReaderActivity.KEY_PAGE, p.position);
startActivity(intent);
}
}
return true;
}
private void onGetPreviewSetSuccess(Pair<PreviewSet, Integer> result, int taskId) {
if (null != mHelper && mHelper.isCurrentTask(taskId) && null != mGalleryInfo) {
PreviewSet previewSet = result.first;
int size = previewSet.size();
ArrayList<GalleryPreview> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add(previewSet.getGalleryPreview(mGalleryInfo.gid, i));
}
mHelper.onGetPageData(taskId, result.second, 0, null, null, list);
}
}
private void onGetPreviewSetFailure(Exception e, int taskId) {
if (mHelper != null && mHelper.isCurrentTask(taskId)) {
mHelper.onGetException(taskId, e);
}
}
private static class GetPreviewSetListener extends EhCallback<GalleryPreviewsScene, Pair<PreviewSet, Integer>> {
private final int mTaskId;
public GetPreviewSetListener(Context context, int stageId, String sceneTag, int taskId) {
super(context, stageId, sceneTag);
mTaskId = taskId;
}
@Override
public void onSuccess(Pair<PreviewSet, Integer> result) {
GalleryPreviewsScene scene = getScene();
if (scene != null) {
scene.onGetPreviewSetSuccess(result, mTaskId);
}
}
@Override
public void onFailure(Exception e) {
GalleryPreviewsScene scene = getScene();
if (scene != null) {
scene.onGetPreviewSetFailure(e, mTaskId);
}
}
@Override
public void onCancel() {
}
@Override
public boolean isInstance(SceneFragment scene) {
return scene instanceof GalleryPreviewsScene;
}
}
private static class GalleryPreviewHolder extends RecyclerView.ViewHolder {
public LoadImageView image;
public TextView text;
public GalleryPreviewHolder(View itemView) {
super(itemView);
image = itemView.findViewById(R.id.image);
text = itemView.findViewById(R.id.text);
}
}
private class GalleryPreviewAdapter extends RecyclerView.Adapter<GalleryPreviewHolder> {
private final LayoutInflater mInflater;
public GalleryPreviewAdapter() {
mInflater = getLayoutInflater();
AssertUtils.assertNotNull(mInflater);
}
@NonNull
@Override
public GalleryPreviewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new GalleryPreviewHolder(mInflater.inflate(R.layout.item_gallery_preview, parent, false));
}
@Override
@SuppressLint("SetTextI18n")
public void onBindViewHolder(@NonNull GalleryPreviewHolder holder, int position) {
if (null != mHelper) {
GalleryPreview preview = mHelper.getDataAtEx(position);
if (preview != null) {
preview.load(holder.image);
holder.text.setText(Integer.toString(preview.position + 1));
}
}
holder.itemView.setOnClickListener(v -> onItemClick(position));
}
@Override
public int getItemCount() {
return mHelper != null ? mHelper.size() : 0;
}
}
private class GalleryPreviewHelper extends ContentLayout.ContentHelper<GalleryPreview> {
@Override
protected void getPageData(final int taskId, int type, int page, String index, boolean isNext) {
MainActivity activity = getMainActivity();
if (null == activity || null == mClient || null == mGalleryInfo) {
onGetException(taskId, new EhException(getString(R.string.error_cannot_find_gallery)));
return;
}
String url = EhUrl.getGalleryDetailUrl(mGalleryInfo.gid, mGalleryInfo.token, page, false);
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_GET_PREVIEW_SET);
request.setCallback(new GetPreviewSetListener(getContext(),
activity.getStageId(), getTag(), taskId));
request.setArgs(url);
request.enqueue(GalleryPreviewsScene.this);
}
@Override
protected Context getContext() {
return GalleryPreviewsScene.this.getContext();
}
@SuppressLint("NotifyDataSetChanged")
@Override
protected void notifyDataSetChanged() {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
protected void notifyItemRangeInserted(int positionStart, int itemCount) {
if (mAdapter != null) {
mAdapter.notifyItemRangeInserted(positionStart, itemCount);
}
}
@Override
protected boolean isDuplicate(GalleryPreview d1, GalleryPreview d2) {
return false;
}
}
private class GoToDialogHelper implements View.OnClickListener,
DialogInterface.OnDismissListener {
private final int mPages;
private final int mCurrentPage;
@Nullable
private Slider mSlider;
@Nullable
private Dialog mDialog;
private GoToDialogHelper(int pages, int currentPage) {
mPages = pages;
mCurrentPage = currentPage;
}
public void setDialog(@NonNull AlertDialog dialog) {
mDialog = dialog;
((TextView) ViewUtils.$$(dialog, R.id.start)).setText(String.format(Locale.US, "%d", 1));
((TextView) ViewUtils.$$(dialog, R.id.end)).setText(String.format(Locale.US, "%d", mPages));
mSlider = (Slider) ViewUtils.$$(dialog, R.id.slider);
mSlider.setValueTo(mPages);
mSlider.setValue(mCurrentPage + 1);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
dialog.setOnDismissListener(this);
}
@Override
public void onClick(View v) {
if (null == mSlider) {
return;
}
int page = (int) (mSlider.getValue() - 1);
if (page >= 0 && page < mPages && mHelper != null) {
mHelper.goTo(page);
if (mDialog != null) {
mDialog.dismiss();
mDialog = null;
}
} else {
showTip(R.string.error_out_of_range, LENGTH_LONG);
}
}
@Override
public void onDismiss(DialogInterface dialog) {
mDialog = null;
mSlider = null;
}
}
}
@@ -0,0 +1,349 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.util.Pair
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.slider.Slider
import com.hippo.app.BaseDialogBuilder
import com.hippo.easyrecyclerview.MarginItemDecoration
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhRequest
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.PreviewSet
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.databinding.SceneGalleryPreviewsBinding
import com.hippo.widget.ContentLayout.ContentHelper
import com.hippo.widget.LoadImageView
import com.hippo.widget.recyclerview.AutoGridLayoutManager
import com.hippo.yorozuya.LayoutUtils
import com.hippo.yorozuya.ViewUtils
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import java.util.Locale
class GalleryPreviewsScene : BaseToolbarScene() {
private var _binding: SceneGalleryPreviewsBinding? = null
private val binding
get() = _binding!!
private var mGalleryInfo: GalleryInfo? = null
private var mAdapter: GalleryPreviewAdapter? = null
private var mHelper: GalleryPreviewHelper? = null
private var mHasFirstRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
onInit()
} else {
onRestore(savedInstanceState)
}
}
private fun onInit() {
val args = arguments ?: return
mGalleryInfo = args.getParcelable(KEY_GALLERY_INFO)
}
private fun onRestore(savedInstanceState: Bundle) {
mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO)
mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val hasFirstRefresh: Boolean = if (mHelper != null && 1 == mHelper!!.shownViewIndex) {
false
} else {
mHasFirstRefresh
}
outState.putBoolean(KEY_HAS_FIRST_REFRESH, hasFirstRefresh)
outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo)
}
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = SceneGalleryPreviewsBinding.inflate(inflater, container, false)
binding.contentLayout.hideFastScroll()
setLiftOnScrollTargetView(binding.contentLayout)
mAdapter = GalleryPreviewAdapter()
binding.contentLayout.recyclerView.adapter = mAdapter
val columnWidth = Settings.getThumbSize()
val layoutManager =
AutoGridLayoutManager(context, columnWidth, LayoutUtils.dp2pix(context, 16f))
layoutManager.setStrategy(AutoGridLayoutManager.STRATEGY_SUITABLE_SIZE)
binding.contentLayout.recyclerView.layoutManager = layoutManager
binding.contentLayout.recyclerView.clipToPadding = false
val padding = LayoutUtils.dp2pix(context, 4f)
val decoration = MarginItemDecoration(padding, padding, padding, padding, padding)
binding.contentLayout.recyclerView.addItemDecoration(decoration)
mHelper = GalleryPreviewHelper()
binding.contentLayout.setHelper(mHelper)
// Only refresh for the first time
if (!mHasFirstRefresh) {
mHasFirstRefresh = true
mHelper?.firstRefresh()
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
if (null != mHelper) {
if (1 == mHelper!!.shownViewIndex) {
mHasFirstRefresh = false
}
}
binding.contentLayout.recyclerView.stopScroll()
_binding = null
mAdapter = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(R.string.gallery_previews)
setNavigationIcon(R.drawable.v_arrow_left_dark_x24)
if (mGalleryInfo != null) {
if ((mGalleryInfo as GalleryDetail).previewPages > 2) showMenu(R.menu.scene_gallery_previews)
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
val context = context ?: return false
val id = item.itemId
if (id == R.id.action_go_to) {
if (mHelper == null) {
return true
}
val pages = mHelper!!.pages
if (pages > 1 && mHelper!!.canGoTo()) {
val helper = GoToDialogHelper(pages, mHelper!!.pageForTop)
val dialog = BaseDialogBuilder(context).setTitle(R.string.go_to)
.setView(R.layout.dialog_go_to)
.setPositiveButton(android.R.string.ok, null)
.create()
dialog.show()
helper.setDialog(dialog)
}
return true
}
return false
}
fun onItemClick(position: Int): Boolean {
val context = context
if (null != context && null != mHelper && null != mGalleryInfo) {
val p = mHelper!!.getDataAtEx(position)
if (p != null) {
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_EH
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, mGalleryInfo)
intent.putExtra(ReaderActivity.KEY_PAGE, p.position)
startActivity(intent)
}
}
return true
}
private fun onGetPreviewSetSuccess(result: Pair<PreviewSet, Int>, taskId: Int) {
if (null != mHelper && mHelper!!.isCurrentTask(taskId) && null != mGalleryInfo) {
val previewSet = result.first
val size = previewSet.size()
val list = ArrayList<GalleryPreview>(size)
for (i in 0 until size) {
list.add(previewSet.getGalleryPreview(mGalleryInfo!!.gid, i))
}
mHelper!!.onGetPageData(
taskId, result.second, 0, null, null,
list as List<GalleryPreview?>?
)
}
}
private fun onGetPreviewSetFailure(e: Exception, taskId: Int) {
if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {
mHelper!!.onGetException(taskId, e)
}
}
private class GalleryPreviewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var image: LoadImageView
var text: TextView
init {
image = itemView.findViewById(R.id.image)
text = itemView.findViewById(R.id.text)
}
}
private inner class GetPreviewSetListener(
context: Context,
private val mTaskId: Int
) : EhCallback<GalleryPreviewsScene, Pair<PreviewSet, Int>>(context) {
override fun onSuccess(result: Pair<PreviewSet, Int>) {
val scene = this@GalleryPreviewsScene
scene.onGetPreviewSetSuccess(result, mTaskId)
}
override fun onFailure(e: Exception) {
val scene = this@GalleryPreviewsScene
scene.onGetPreviewSetFailure(e, mTaskId)
}
override fun onCancel() {}
}
private inner class GalleryPreviewAdapter : RecyclerView.Adapter<GalleryPreviewHolder>() {
private val mInflater: LayoutInflater = layoutInflater
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryPreviewHolder {
return GalleryPreviewHolder(
mInflater.inflate(
R.layout.item_gallery_preview,
parent,
false
)
)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: GalleryPreviewHolder, position: Int) {
if (null != mHelper) {
val preview = mHelper!!.getDataAtEx(position)
if (preview != null) {
preview.load(holder.image)
holder.text.text = (preview.position + 1).toString()
}
}
holder.itemView.setOnClickListener { onItemClick(position) }
}
override fun getItemCount(): Int {
return if (mHelper != null) mHelper!!.size() else 0
}
}
private inner class GalleryPreviewHelper : ContentHelper<GalleryPreview>() {
override fun getPageData(
taskId: Int,
type: Int,
page: Int,
index: String?,
isNext: Boolean
) {
val activity = mainActivity
if (null == activity || null == mGalleryInfo) {
onGetException(taskId, EhException(getString(R.string.error_cannot_find_gallery)))
return
}
val url =
EhUrl.getGalleryDetailUrl(mGalleryInfo!!.gid, mGalleryInfo!!.token, page, false)
val request = EhRequest()
request.setMethod(EhClient.METHOD_GET_PREVIEW_SET)
request.setCallback(
GetPreviewSetListener(
context,
taskId
)
)
request.setArgs(url)
request.enqueue(this@GalleryPreviewsScene)
}
override fun getContext(): Context {
return this@GalleryPreviewsScene.requireContext()
}
@SuppressLint("NotifyDataSetChanged")
override fun notifyDataSetChanged() {
if (mAdapter != null) {
mAdapter!!.notifyDataSetChanged()
}
}
override fun notifyItemRangeInserted(positionStart: Int, itemCount: Int) {
if (mAdapter != null) {
mAdapter!!.notifyItemRangeInserted(positionStart, itemCount)
}
}
override fun isDuplicate(d1: GalleryPreview, d2: GalleryPreview): Boolean {
return false
}
}
private inner class GoToDialogHelper(private val mPages: Int, private val mCurrentPage: Int) :
View.OnClickListener, DialogInterface.OnDismissListener {
private var mSlider: Slider? = null
private var mDialog: Dialog? = null
fun setDialog(dialog: AlertDialog) {
mDialog = dialog
(ViewUtils.`$$`(dialog, R.id.start) as TextView).text =
String.format(Locale.US, "%d", 1)
(ViewUtils.`$$`(dialog, R.id.end) as TextView).text =
String.format(Locale.US, "%d", mPages)
mSlider = ViewUtils.`$$`(dialog, R.id.slider) as Slider
mSlider!!.valueTo = mPages.toFloat()
mSlider!!.value = (mCurrentPage + 1).toFloat()
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this)
dialog.setOnDismissListener(this)
}
override fun onClick(v: View) {
if (null == mSlider) {
return
}
val page = (mSlider!!.value - 1).toInt()
if (page in 0 until mPages && mHelper != null) {
mHelper!!.goTo(page)
if (mDialog != null) {
mDialog!!.dismiss()
mDialog = null
}
} else {
showTip(R.string.error_out_of_range, LENGTH_LONG)
}
}
override fun onDismiss(dialog: DialogInterface) {
mDialog = null
mSlider = null
}
}
companion object {
const val KEY_GALLERY_INFO = "gallery_info"
private const val KEY_HAS_FIRST_REFRESH = "has_first_refresh"
}
}
@@ -48,13 +48,13 @@ import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter
import com.hippo.ehviewer.widget.SimpleRatingView
import com.hippo.scene.Announcer
import com.hippo.scene.SceneFragment
import com.hippo.view.ViewTransition
import com.hippo.widget.LoadImageView
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager
import com.hippo.yorozuya.ViewUtils
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import rikka.core.res.resolveColor
@@ -112,10 +112,6 @@ class HistoryScene : BaseToolbarScene() {
mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener)
}
override fun getNavCheckedItem(): Int {
return R.id.nav_history
}
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -196,9 +192,13 @@ class HistoryScene : BaseToolbarScene() {
if (DialogInterface.BUTTON_POSITIVE != which) {
return@setPositiveButton
}
EhDB.clearHistoryInfo()
mAdapter.refresh()
mAdapter.notifyDataSetChanged()
lifecycleScope.launchIO {
EhDB.clearHistoryInfo()
withUIContext {
mAdapter.refresh()
mAdapter.notifyDataSetChanged()
}
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
@@ -217,8 +217,7 @@ class HistoryScene : BaseToolbarScene() {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO)
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi)
val announcer = Announcer(GalleryDetailScene::class.java).setArgs(args)
startScene(announcer)
navigate(R.id.galleryDetailScene, args)
return true
}
@@ -287,13 +286,13 @@ class HistoryScene : BaseToolbarScene() {
CommonOperations.removeFromFavorites(
activity,
gi,
RemoveFromFavoriteListener(context, activity.stageId, tag)
RemoveFromFavoriteListener(context)
)
} else {
CommonOperations.addToFavorites(
activity,
gi,
AddToFavoriteListener(context, activity.stageId, tag)
AddToFavoriteListener(context)
)
}
@@ -319,8 +318,8 @@ class HistoryScene : BaseToolbarScene() {
return true
}
private class AddToFavoriteListener(context: Context?, stageId: Int, sceneTag: String?) :
EhCallback<GalleryListScene?, Void?>(context, stageId, sceneTag) {
private class AddToFavoriteListener(context: Context) :
EhCallback<GalleryListScene?, Void?>(context) {
override fun onSuccess(result: Void?) {
showTip(R.string.add_to_favorite_success, LENGTH_SHORT)
}
@@ -330,13 +329,10 @@ class HistoryScene : BaseToolbarScene() {
}
override fun onCancel() {}
override fun isInstance(scene: SceneFragment): Boolean {
return scene is GalleryListScene
}
}
private class RemoveFromFavoriteListener(context: Context?, stageId: Int, sceneTag: String?) :
EhCallback<GalleryListScene?, Void?>(context, stageId, sceneTag) {
private class RemoveFromFavoriteListener(context: Context) :
EhCallback<GalleryListScene?, Void?>(context) {
override fun onSuccess(result: Void?) {
showTip(R.string.remove_from_favorite_success, LENGTH_SHORT)
}
@@ -346,9 +342,6 @@ class HistoryScene : BaseToolbarScene() {
}
override fun onCancel() {}
override fun isInstance(scene: SceneFragment): Boolean {
return scene is GalleryListScene
}
}
private class HistoryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -403,7 +396,7 @@ class HistoryScene : BaseToolbarScene() {
override fun onBindViewHolder(holder: HistoryHolder, position: Int) {
val gi: GalleryInfo? = getItem(position)
gi ?: return
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb!!)
gi.thumb?.let { holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), it) }
holder.title.text = EhUtils.getSuitableTitle(gi)
holder.uploader.text = gi.uploader
holder.rating.rating = gi.rating
@@ -450,9 +443,13 @@ class HistoryScene : BaseToolbarScene() {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val mPosition = viewHolder.bindingAdapterPosition
val info: HistoryInfo? = mAdapter.peek(mPosition)
info?.let { EhDB.deleteHistoryInfo(info) }
mAdapter.refresh()
lifecycleScope.launchIO {
val info: HistoryInfo? = mAdapter.peek(mPosition)
info?.let { EhDB.deleteHistoryInfo(info) }
withUIContext {
mAdapter.refresh()
}
}
}
}
}
@@ -1,260 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.util.ExceptionUtils;
import com.hippo.view.ViewTransition;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.ViewUtils;
/**
* Only show a progress with jobs in background
*/
public final class ProgressScene extends BaseScene implements View.OnClickListener {
public static final String KEY_ACTION = "action";
public static final String ACTION_GALLERY_TOKEN = "gallery_token";
public static final String KEY_GID = "gid";
public static final String KEY_PTOKEN = "ptoken";
public static final String KEY_PAGE = "page";
private static final String KEY_VALID = "valid";
private static final String KEY_ERROR = "error";
private boolean mValid;
private String mError;
private String mAction;
private long mGid;
private String mPToken;
private int mPage;
@Nullable
private TextView mTip;
@Nullable
private ViewTransition mViewTransition;
@Override
public boolean needShowLeftDrawer() {
return false;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
onInit();
} else {
onRestore(savedInstanceState);
}
}
private boolean doJobs() {
Context context = getContext();
MainActivity activity = getMainActivity();
if (null == context || null == activity) {
return false;
}
if (ACTION_GALLERY_TOKEN.equals(mAction)) {
if (mGid == -1 || mPToken == null || mPage == -1) {
return false;
}
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_TOKEN)
.setArgs(mGid, mPToken, mPage)
.setCallback(new GetGalleryTokenListener(context,
activity.getStageId(), getTag()));
request.enqueue(this);
return true;
}
return false;
}
private boolean handleArgs(Bundle args) {
if (args == null) {
return false;
}
mAction = args.getString(KEY_ACTION);
if (ACTION_GALLERY_TOKEN.equals(mAction)) {
mGid = args.getLong(KEY_GID, -1);
mPToken = args.getString(KEY_PTOKEN, null);
mPage = args.getInt(KEY_PAGE, -1);
return mGid != -1 && mPToken != null && mPage != -1;
}
return false;
}
private void onInit() {
mValid = handleArgs(getArguments());
if (mValid) {
mValid = doJobs();
}
if (!mValid) {
mError = getString(R.string.error_something_wrong_happened);
}
}
private void onRestore(@NonNull Bundle savedInstanceState) {
mValid = savedInstanceState.getBoolean(KEY_VALID);
mError = savedInstanceState.getString(KEY_ERROR);
mAction = savedInstanceState.getString(KEY_ACTION);
mGid = savedInstanceState.getLong(KEY_GID, -1);
mPToken = savedInstanceState.getString(KEY_PTOKEN, null);
mPage = savedInstanceState.getInt(KEY_PAGE, -1);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_VALID, mValid);
outState.putString(KEY_ERROR, mError);
outState.putString(KEY_ACTION, mAction);
outState.putLong(KEY_GID, mGid);
outState.putString(KEY_PTOKEN, mPToken);
outState.putInt(KEY_PAGE, mPage);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_progress, container, false);
View progress = ViewUtils.$$(view, R.id.progress);
mTip = (TextView) ViewUtils.$$(view, R.id.tip);
Context context = getContext();
AssertUtils.assertNotNull(context);
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
mTip.setCompoundDrawables(null, drawable, null, null);
mTip.setOnClickListener(this);
mTip.setText(mError);
mViewTransition = new ViewTransition(progress, mTip);
if (mValid) {
mViewTransition.showView(0, false);
} else {
mViewTransition.showView(1, false);
}
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mTip = null;
mViewTransition = null;
}
@Override
public void onClick(View v) {
if (mTip == v) {
if (doJobs()) {
mValid = true;
// Show progress
if (null != mViewTransition) {
mViewTransition.showView(0, true);
}
}
}
}
private void onGetGalleryTokenSuccess(String result) {
Bundle arg = new Bundle();
arg.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN);
arg.putLong(GalleryDetailScene.KEY_GID, mGid);
arg.putString(GalleryDetailScene.KEY_TOKEN, result);
arg.putInt(GalleryDetailScene.KEY_PAGE, mPage);
startScene(new Announcer(GalleryDetailScene.class).setArgs(arg));
finish();
}
private void onGetGalleryTokenFailure(Exception e) {
mValid = false;
Context context = getContext();
if (null != context && null != mViewTransition && null != mTip) {
// Show tip
mError = ExceptionUtils.getReadableString(e);
mViewTransition.showView(1);
mTip.setText(mError);
}
}
private static class GetGalleryTokenListener extends EhCallback<ProgressScene, String> {
public GetGalleryTokenListener(Context context, int stageId, String sceneTag) {
super(context, stageId, sceneTag);
}
@Override
public void onSuccess(String result) {
ProgressScene scene = getScene();
if (scene != null) {
scene.onGetGalleryTokenSuccess(result);
}
}
@Override
public void onFailure(Exception e) {
ProgressScene scene = getScene();
if (scene != null) {
scene.onGetGalleryTokenFailure(e);
}
}
@Override
public void onCancel() {
}
@Override
public boolean isInstance(SceneFragment scene) {
return scene instanceof ProgressScene;
}
}
}
@@ -0,0 +1,211 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhRequest
import com.hippo.util.ExceptionUtils
import com.hippo.view.ViewTransition
import com.hippo.yorozuya.AssertUtils
import com.hippo.yorozuya.ViewUtils
/**
* Only show a progress with jobs in background
*/
class ProgressScene : BaseScene(), View.OnClickListener {
private var mValid = false
private var mError: String? = null
private var mAction: String? = null
private var mGid: Long = 0
private var mPToken: String? = null
private var mPage = 0
private var mTip: TextView? = null
private var mViewTransition: ViewTransition? = null
override fun needShowLeftDrawer(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
onInit()
} else {
onRestore(savedInstanceState)
}
}
private fun doJobs(): Boolean {
val context = context
val activity = mainActivity
if (null == context || null == activity) {
return false
}
if (ACTION_GALLERY_TOKEN == mAction) {
if (mGid == -1L || mPToken == null || mPage == -1) {
return false
}
val request = EhRequest()
.setMethod(EhClient.METHOD_GET_GALLERY_TOKEN)
.setArgs(mGid, mPToken!!, mPage)
.setCallback(
GetGalleryTokenListener(
context
)
)
request.enqueue(this)
return true
}
return false
}
private fun handleArgs(args: Bundle?): Boolean {
if (args == null) {
return false
}
mAction = args.getString(KEY_ACTION)
if (ACTION_GALLERY_TOKEN == mAction) {
mGid = args.getLong(KEY_GID, -1)
mPToken = args.getString(KEY_PTOKEN, null)
mPage = args.getInt(KEY_PAGE, -1)
return mGid != -1L && mPToken != null && mPage != -1
}
return false
}
private fun onInit() {
mValid = handleArgs(arguments)
if (mValid) {
mValid = doJobs()
}
if (!mValid) {
mError = getString(R.string.error_something_wrong_happened)
}
}
private fun onRestore(savedInstanceState: Bundle) {
mValid = savedInstanceState.getBoolean(KEY_VALID)
mError = savedInstanceState.getString(KEY_ERROR)
mAction = savedInstanceState.getString(KEY_ACTION)
mGid = savedInstanceState.getLong(KEY_GID, -1)
mPToken = savedInstanceState.getString(KEY_PTOKEN, null)
mPage = savedInstanceState.getInt(KEY_PAGE, -1)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_VALID, mValid)
outState.putString(KEY_ERROR, mError)
outState.putString(KEY_ACTION, mAction)
outState.putLong(KEY_GID, mGid)
outState.putString(KEY_PTOKEN, mPToken)
outState.putInt(KEY_PAGE, mPage)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.scene_progress, container, false)
val progress = ViewUtils.`$$`(view, R.id.progress)
mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
val context = context
AssertUtils.assertNotNull(context)
val drawable = ContextCompat.getDrawable(context!!, R.drawable.big_sad_pandroid)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
mTip!!.setCompoundDrawables(null, drawable, null, null)
mTip!!.setOnClickListener(this)
mTip!!.text = mError
mViewTransition = ViewTransition(progress, mTip)
if (mValid) {
mViewTransition!!.showView(0, false)
} else {
mViewTransition!!.showView(1, false)
}
return view
}
override fun onDestroyView() {
super.onDestroyView()
mTip = null
mViewTransition = null
}
override fun onClick(v: View) {
if (mTip === v) {
if (doJobs()) {
mValid = true
// Show progress
if (null != mViewTransition) {
mViewTransition!!.showView(0, true)
}
}
}
}
private fun onGetGalleryTokenSuccess(result: String) {
val arg = Bundle()
arg.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)
arg.putLong(GalleryDetailScene.KEY_GID, mGid)
arg.putString(GalleryDetailScene.KEY_TOKEN, result)
arg.putInt(GalleryDetailScene.KEY_PAGE, mPage)
navigate(R.id.galleryDetailScene, arg)
}
private fun onGetGalleryTokenFailure(e: Exception) {
mValid = false
val context = context
if (null != context && null != mViewTransition && null != mTip) {
// Show tip
mError = ExceptionUtils.getReadableString(e)
mViewTransition!!.showView(1)
mTip!!.text = mError
}
}
private inner class GetGalleryTokenListener(
context: Context
) : EhCallback<ProgressScene, String>(context) {
override fun onSuccess(result: String) {
val scene = this@ProgressScene
scene.onGetGalleryTokenSuccess(result)
}
override fun onFailure(e: Exception) {
val scene = this@ProgressScene
scene.onGetGalleryTokenFailure(e)
}
override fun onCancel() {}
}
companion object {
const val KEY_ACTION = "action"
const val ACTION_GALLERY_TOKEN = "gallery_token"
const val KEY_GID = "gid"
const val KEY_PTOKEN = "ptoken"
const val KEY_PAGE = "page"
private const val KEY_VALID = "valid"
private const val KEY_ERROR = "error"
}
}
@@ -1,97 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.button.MaterialButtonToggleGroup;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.yorozuya.ViewUtils;
public class SelectSiteScene extends SolidScene implements View.OnClickListener {
private MaterialButtonToggleGroup mButtonGroup;
private View mOk;
@Override
public boolean needShowLeftDrawer() {
return false;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_select_site, container, false);
mButtonGroup = (MaterialButtonToggleGroup) ViewUtils.$$(view, R.id.button_group);
if (EhApplication.getEhCookieStore().hasSignedIn()) {
((MaterialButton) view.findViewById(R.id.site_ex)).setChecked(true);
} else {
((MaterialButton) view.findViewById(R.id.site_e)).setChecked(true);
}
mOk = ViewUtils.$$(view, R.id.ok);
mOk.setOnClickListener(this);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mButtonGroup = null;
mOk = null;
}
@Override
public void onClick(View v) {
MainActivity activity = getMainActivity();
if (null == activity || null == mButtonGroup) {
return;
}
if (v == mOk) {
int id = mButtonGroup.getCheckedButtonId();
if (id == R.id.site_e) {
Settings.putSelectSite(false);
Settings.putGallerySite(EhUrl.SITE_E);
startSceneForCheckStep(CHECK_STEP_SELECT_SITE, getArguments());
finish();
} else if (id == R.id.site_ex) {
Settings.putSelectSite(false);
Settings.putGallerySite(EhUrl.SITE_EX);
startSceneForCheckStep(CHECK_STEP_SELECT_SITE, getArguments());
finish();
} else {
showTip(R.string.no_select, LENGTH_SHORT);
}
}
}
}
@@ -1,438 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.content.Context;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.TextInputLayout;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.UrlOpener;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhRequest;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.parser.ProfileParser;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.util.ExceptionUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.IntIdGenerator;
import com.hippo.yorozuya.ViewUtils;
public final class SignInScene extends SolidScene implements EditText.OnEditorActionListener,
View.OnClickListener {
private static final String KEY_REQUEST_ID = "request_id";
private static final int REQUEST_CODE_WEBVIEW = 0;
private static final int REQUEST_CODE_COOKIE = 0;
/*---------------
View life cycle
---------------*/
@Nullable
private View mProgress;
@Nullable
private TextInputLayout mUsernameLayout;
@Nullable
private TextInputLayout mPasswordLayout;
@Nullable
private EditText mUsername;
@Nullable
private EditText mPassword;
@Nullable
private View mRegister;
@Nullable
private View mSignIn;
@Nullable
private TextView mSignInViaWebView;
@Nullable
private TextView mSignInViaCookies;
@Nullable
private TextView mSkipSigningIn;
private boolean mSigningIn;
private int mRequestId = IntIdGenerator.INVALID_ID;
@Override
public boolean needShowLeftDrawer() {
return false;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
onInit();
} else {
onRestore(savedInstanceState);
}
}
private void onInit() {
}
private void onRestore(Bundle savedInstanceState) {
mRequestId = savedInstanceState.getInt(KEY_REQUEST_ID);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_REQUEST_ID, mRequestId);
}
@NonNull
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_login, container, false);
View loginForm = ViewUtils.$$(view, R.id.login_form);
mProgress = ViewUtils.$$(view, R.id.progress);
mUsernameLayout = (TextInputLayout) ViewUtils.$$(loginForm, R.id.username_layout);
mUsername = mUsernameLayout.getEditText();
AssertUtils.assertNotNull(mUsername);
mPasswordLayout = (TextInputLayout) ViewUtils.$$(loginForm, R.id.password_layout);
mPassword = mPasswordLayout.getEditText();
AssertUtils.assertNotNull(mPassword);
mRegister = ViewUtils.$$(loginForm, R.id.register);
mSignIn = ViewUtils.$$(loginForm, R.id.sign_in);
mSignInViaWebView = (TextView) ViewUtils.$$(loginForm, R.id.sign_in_via_webview);
mSignInViaCookies = (TextView) ViewUtils.$$(loginForm, R.id.sign_in_via_cookies);
mSkipSigningIn = (TextView) ViewUtils.$$(loginForm, R.id.tourist_mode);
mSignInViaWebView.setPaintFlags(mSignInViaWebView.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
mSignInViaCookies.setPaintFlags(mSignInViaCookies.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
mSkipSigningIn.setPaintFlags(mSignInViaCookies.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
mPassword.setOnEditorActionListener(this);
mRegister.setOnClickListener(this);
mSignIn.setOnClickListener(this);
mSignInViaWebView.setOnClickListener(this);
mSignInViaCookies.setOnClickListener(this);
mSkipSigningIn.setOnClickListener(this);
Context context = getContext();
AssertUtils.assertNotNull(context);
EhApplication application = (EhApplication) context.getApplicationContext();
if (application.containGlobalStuff(mRequestId)) {
mSigningIn = true;
// request exist
showProgress(false);
}
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Show IME
if (mProgress != null && View.INVISIBLE != mProgress.getVisibility()) {
showSoftInput(mUsername);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mProgress = null;
mUsernameLayout = null;
mPasswordLayout = null;
mUsername = null;
mPassword = null;
mRegister = null;
mSignIn = null;
mSignInViaWebView = null;
mSignInViaCookies = null;
mSkipSigningIn = null;
}
private void showProgress(boolean animation) {
if (null != mProgress && View.VISIBLE != mProgress.getVisibility()) {
if (animation) {
mProgress.setAlpha(0.0f);
mProgress.setVisibility(View.VISIBLE);
mProgress.animate().alpha(1.0f).setDuration(500).start();
} else {
mProgress.setAlpha(1.0f);
mProgress.setVisibility(View.VISIBLE);
}
}
}
private void hideProgress() {
if (null != mProgress) {
mProgress.setVisibility(View.GONE);
}
}
@Override
protected void onSceneResult(int requestCode, int resultCode, Bundle data) {
if (REQUEST_CODE_WEBVIEW == requestCode) {
if (RESULT_OK == resultCode) {
getProfile();
}
} else {
super.onSceneResult(requestCode, resultCode, data);
}
}
@Override
public void onClick(View v) {
MainActivity activity = getMainActivity();
if (null == activity) {
return;
}
if (mRegister == v) {
UrlOpener.openUrl(activity, EhUrl.URL_REGISTER, false);
} else if (mSignIn == v) {
signIn();
} else if (mSignInViaWebView == v) {
startScene(new Announcer(WebViewSignInScene.class).setRequestCode(this, REQUEST_CODE_WEBVIEW));
} else if (mSignInViaCookies == v) {
startScene(new Announcer(CookieSignInScene.class).setRequestCode(this, REQUEST_CODE_COOKIE));
} else if (mSkipSigningIn == v) {
// Set gallery size SITE_E if skip sign in
Settings.putGallerySite(EhUrl.SITE_E);
redirectTo();
}
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (v == mPassword) {
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
signIn();
return true;
}
}
return false;
}
private void signIn() {
if (mSigningIn) {
return;
}
Context context = getContext();
MainActivity activity = getMainActivity();
if (null == context || null == activity || null == mUsername || null == mPassword || null == mUsernameLayout ||
null == mPasswordLayout) {
return;
}
String username = mUsername.getText().toString();
String password = mPassword.getText().toString();
if (username.isEmpty()) {
mUsernameLayout.setError(getString(R.string.error_username_cannot_empty));
return;
} else {
mUsernameLayout.setError(null);
}
if (password.isEmpty()) {
mPasswordLayout.setError(getString(R.string.error_password_cannot_empty));
return;
} else {
mPasswordLayout.setError(null);
}
hideSoftInput();
showProgress(true);
// Clean up for sign in
EhUtils.signOut();
EhCallback<?, ?> callback = new SignInListener(context,
activity.getStageId(), getTag());
mRequestId = ((EhApplication) context.getApplicationContext()).putGlobalStuff(callback);
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_SIGN_IN)
.setArgs(username, password)
.setCallback(callback);
request.enqueue(this);
mSigningIn = true;
}
private void getProfile() {
Context context = getContext();
MainActivity activity = getMainActivity();
if (null == context || null == activity) {
return;
}
hideSoftInput();
showProgress(true);
EhCallback<?, ?> callback = new GetProfileListener(context,
activity.getStageId(), getTag());
mRequestId = ((EhApplication) context.getApplicationContext()).putGlobalStuff(callback);
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_GET_PROFILE)
.setCallback(callback);
request.enqueue();
new EhRequest().setMethod(EhClient.METHOD_GET_UCONFIG).enqueue();
}
private void redirectTo() {
Settings.putNeedSignIn(false);
MainActivity activity = getMainActivity();
if (null != activity) {
startSceneForCheckStep(CHECK_STEP_SIGN_IN, getArguments());
}
finish();
}
private void whetherToSkip(Exception e) {
Context context = getContext();
if (null == context) {
return;
}
new BaseDialogBuilder(context)
.setTitle(R.string.sign_in_failed)
.setMessage(ExceptionUtils.getReadableString(e) + "\n\n" + getString(R.string.sign_in_failed_tip))
.setPositiveButton(R.string.get_it, null)
.show();
}
public void onSignInEnd(Exception e) {
Context context = getContext();
if (null == context) {
return;
}
if (EhApplication.getEhCookieStore().hasSignedIn()) {
getProfile();
} else {
mSigningIn = false;
hideProgress();
whetherToSkip(e);
}
}
public void onGetProfileEnd() {
mSigningIn = false;
redirectTo();
}
private static class SignInListener extends EhCallback<SignInScene, String> {
public SignInListener(Context context, int stageId, String sceneTag) {
super(context, stageId, sceneTag);
}
@Override
public void onSuccess(String result) {
getApplication().removeGlobalStuff(this);
Settings.putDisplayName(result);
SignInScene scene = getScene();
if (scene != null) {
scene.onSignInEnd(null);
}
}
@Override
public void onFailure(Exception e) {
getApplication().removeGlobalStuff(this);
e.printStackTrace();
SignInScene scene = getScene();
if (scene != null) {
scene.onSignInEnd(e);
}
}
@Override
public void onCancel() {
getApplication().removeGlobalStuff(this);
}
@Override
public boolean isInstance(SceneFragment scene) {
return scene instanceof SignInScene;
}
}
private static class GetProfileListener extends EhCallback<SignInScene, ProfileParser.Result> {
public GetProfileListener(Context context, int stageId, String sceneTag) {
super(context, stageId, sceneTag);
}
@Override
public void onSuccess(ProfileParser.Result result) {
getApplication().removeGlobalStuff(this);
Settings.putDisplayName(result.displayName);
Settings.putAvatar(result.avatar);
SignInScene scene = getScene();
if (scene != null) {
scene.onGetProfileEnd();
}
}
@Override
public void onFailure(Exception e) {
getApplication().removeGlobalStuff(this);
e.printStackTrace();
SignInScene scene = getScene();
if (scene != null) {
scene.onGetProfileEnd();
}
}
@Override
public void onCancel() {
getApplication().removeGlobalStuff(this);
}
@Override
public boolean isInstance(SceneFragment scene) {
return scene instanceof SignInScene;
}
}
}
@@ -1,70 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.os.Bundle;
import android.util.Log;
import com.hippo.ehviewer.Settings;
import com.hippo.scene.Announcer;
/**
* Scene for safety, can't be covered
*/
public class SolidScene extends BaseScene {
public static final int CHECK_STEP_SIGN_IN = 1;
public static final int CHECK_STEP_SELECT_SITE = 2;
public static final String KEY_TARGET_SCENE = "target_scene";
public static final String KEY_TARGET_ARGS = "target_args";
private static final String TAG = SolidScene.class.getSimpleName();
public void startSceneForCheckStep(int checkStep, Bundle args) {
switch (checkStep) {
case CHECK_STEP_SIGN_IN:
if (Settings.getSelectSite()) {
startScene(new Announcer(SelectSiteScene.class).setArgs(args), true);
break;
}
case CHECK_STEP_SELECT_SITE:
String targetScene = null;
Bundle targetArgs = null;
if (null != args) {
targetScene = args.getString(KEY_TARGET_SCENE);
targetArgs = args.getBundle(KEY_TARGET_ARGS);
}
Class<?> clazz = null;
if (targetScene != null) {
try {
clazz = Class.forName(targetScene);
} catch (ClassNotFoundException e) {
Log.e(TAG, "Can't find class with name: " + targetScene);
}
}
if (clazz != null) {
startScene(new Announcer(clazz).setArgs(targetArgs));
} else {
Bundle newArgs = new Bundle();
newArgs.putString(GalleryListScene.KEY_ACTION, Settings.getLaunchPageGalleryListSceneAction());
startScene(new Announcer(GalleryListScene.class).setArgs(newArgs));
}
break;
}
}
}
@@ -1,36 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
public final class TransitionNameFactory {
public static String getThumbTransitionName(long gid) {
return "thumb:" + gid;
}
public static String getTitleTransitionName(long gid) {
return "title:" + gid;
}
public static String getUploaderTransitionName(long gid) {
return "uploader:" + gid;
}
public static String getCategoryTransitionName(long gid) {
return "category:" + gid;
}
}
@@ -1,155 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui.scene;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.widget.DialogWebChromeClient;
import com.hippo.yorozuya.AssertUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
import rikka.core.res.ResourcesKt;
public class WebViewSignInScene extends SolidScene {
/*---------------
View life cycle
---------------*/
@Nullable
private WebView mWebView;
@Override
public boolean needShowLeftDrawer() {
return false;
}
@Nullable
@Override
@SuppressLint("SetJavaScriptEnabled")
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Context context = getContext();
AssertUtils.assertNotNull(context);
EhUtils.signOut();
// http://stackoverflow.com/questions/32284642/how-to-handle-an-uncatched-exception
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.flush();
cookieManager.removeAllCookies(null);
cookieManager.removeSessionCookies(null);
mWebView = new WebView(context);
mWebView.setBackgroundColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.colorBackground));
mWebView.getSettings().setBuiltInZoomControls(true);
mWebView.getSettings().setDisplayZoomControls(false);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.setWebViewClient(new LoginWebViewClient());
mWebView.setWebChromeClient(new DialogWebChromeClient(context));
mWebView.loadUrl(EhUrl.URL_SIGN_IN);
return mWebView;
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (null != mWebView) {
mWebView.destroy();
mWebView = null;
}
}
private class LoginWebViewClient extends WebViewClient {
public List<Cookie> parseCookies(HttpUrl url, String cookieStrings) {
if (cookieStrings == null) {
return Collections.emptyList();
}
List<Cookie> cookies = null;
String[] pieces = cookieStrings.split(";");
for (String piece : pieces) {
Cookie cookie = Cookie.parse(url, piece);
if (cookie == null) {
continue;
}
if (cookies == null) {
cookies = new ArrayList<>();
}
cookies.add(cookie);
}
return cookies != null ? cookies : Collections.emptyList();
}
private void addCookie(String domain, Cookie cookie) {
EhApplication.getEhCookieStore().addCookie(EhCookieStore.newCookie(cookie, domain, true, true, true));
}
@Override
public void onPageFinished(WebView view, String url) {
Context context = getContext();
if (context == null) {
return;
}
HttpUrl httpUrl = HttpUrl.parse(url);
if (httpUrl == null) {
return;
}
String cookieString = CookieManager.getInstance().getCookie(EhUrl.HOST_E);
List<Cookie> cookies = parseCookies(httpUrl, cookieString);
boolean getId = false;
boolean getHash = false;
for (Cookie cookie : cookies) {
if (EhCookieStore.KEY_IPD_MEMBER_ID.equals(cookie.name())) {
getId = true;
} else if (EhCookieStore.KEY_IPD_PASS_HASH.equals(cookie.name())) {
getHash = true;
}
addCookie(EhUrl.DOMAIN_EX, cookie);
addCookie(EhUrl.DOMAIN_E, cookie);
}
if (getId && getHash) {
setResult(RESULT_OK, null);
finish();
}
}
}
}
@@ -0,0 +1,229 @@
package com.hippo.ehviewer.widget;
import static androidx.core.util.ObjectsCompat.requireNonNull;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.math.MathUtils;
import androidx.core.view.GravityCompat;
import androidx.core.view.ViewCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.drakeet.drawer.FullDraggableHelper;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.yorozuya.LayoutUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class AboveSnackerLayout extends FrameLayout implements CoordinatorLayout.AttachedBehavior, FullDraggableHelper.Callback {
@NonNull
private final FullDraggableHelper helper;
private DrawerLayout drawerLayout;
private List<View> mAboveSnackViewList;
public AboveSnackerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
helper = new FullDraggableHelper(context, this);
}
public AboveSnackerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
helper = new FullDraggableHelper(context, this);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
ensureDrawerLayout();
}
private void ensureDrawerLayout() {
ViewParent parent = getParent().getParent();
if (!(parent instanceof DrawerLayout)) {
throw new IllegalStateException("This " + this + " must be added to a DrawerLayout");
}
drawerLayout = (DrawerLayout) parent;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return helper.onInterceptTouchEvent(event);
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(MotionEvent event) {
return helper.onTouchEvent(event);
}
@NonNull
@Override
public View getDrawerMainContainer() {
return this;
}
@Override
public boolean isDrawerOpen(int gravity) {
return drawerLayout.isDrawerOpen(gravity);
}
@Override
public boolean hasEnabledDrawer(int gravity) {
return drawerLayout.getDrawerLockMode(gravity) == DrawerLayout.LOCK_MODE_UNLOCKED
&& findDrawerWithGravity(gravity) != null;
}
@Override
public void offsetDrawer(int gravity, float offset) {
setDrawerToOffset(gravity, offset);
drawerLayout.invalidate();
}
@Override
public void smoothOpenDrawer(int gravity) {
drawerLayout.openDrawer(gravity, true);
}
@Override
public void smoothCloseDrawer(int gravity) {
drawerLayout.closeDrawer(gravity, true);
}
@Override
public void onDrawerDragging() {
List<DrawerLayout.DrawerListener> drawerListeners = getDrawerListeners();
if (drawerListeners != null) {
int listenerCount = drawerListeners.size();
for (int i = listenerCount - 1; i >= 0; --i) {
drawerListeners.get(i).onDrawerStateChanged(DrawerLayout.STATE_DRAGGING);
}
}
}
@Nullable
protected List<DrawerLayout.DrawerListener> getDrawerListeners() {
try {
Field field = DrawerLayout.class.getDeclaredField("mListeners");
field.setAccessible(true);
// noinspection unchecked
return (List<DrawerLayout.DrawerListener>) field.get(drawerLayout);
} catch (Exception e) {
// throw to let developer know the api is changed
throw new RuntimeException(e);
}
}
protected void setDrawerToOffset(int gravity, float offset) {
View drawerView = findDrawerWithGravity(gravity);
float slideOffsetPercent = MathUtils.clamp(offset / requireNonNull(drawerView).getWidth(), 0f, 1f);
try {
Method method = DrawerLayout.class.getDeclaredMethod("moveDrawerToOffset", View.class, float.class);
method.setAccessible(true);
method.invoke(drawerLayout, drawerView, slideOffsetPercent);
drawerView.setVisibility(VISIBLE);
} catch (Exception e) {
// throw to let developer know the api is changed
throw new RuntimeException(e);
}
}
// Copied from DrawerLayout
@Nullable
private View findDrawerWithGravity(int gravity) {
final int absHorizontalGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(drawerLayout)) & Gravity.HORIZONTAL_GRAVITY_MASK;
final int childCount = drawerLayout.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = drawerLayout.getChildAt(i);
final int childAbsGravity = getDrawerViewAbsoluteGravity(child);
if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizontalGravity) {
return child;
}
}
return null;
}
// Copied from DrawerLayout
private int getDrawerViewAbsoluteGravity(View drawerView) {
final int gravity = ((DrawerLayout.LayoutParams) drawerView.getLayoutParams()).gravity;
return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(drawerLayout));
}
public void addAboveSnackView(View view) {
if (null == mAboveSnackViewList) {
mAboveSnackViewList = new ArrayList<>();
}
mAboveSnackViewList.add(view);
}
public void removeAboveSnackView(View view) {
if (null == mAboveSnackViewList) {
return;
}
mAboveSnackViewList.remove(view);
}
public int getAboveSnackViewCount() {
return null == mAboveSnackViewList ? 0 : mAboveSnackViewList.size();
}
@Nullable
public View getAboveSnackViewAt(int index) {
if (null == mAboveSnackViewList || index < 0 || index >= mAboveSnackViewList.size()) {
return null;
} else {
return mAboveSnackViewList.get(index);
}
}
@NonNull
@Override
public AboveSnackerLayout.Behavior getBehavior() {
return new AboveSnackerLayout.Behavior();
}
public static class Behavior extends CoordinatorLayout.Behavior<AboveSnackerLayout> {
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull AboveSnackerLayout child, @NonNull View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, AboveSnackerLayout child, @NonNull View dependency) {
for (int i = 0, n = child.getAboveSnackViewCount(); i < n; i++) {
View view = child.getAboveSnackViewAt(i);
if (view != null) {
float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight() - LayoutUtils.dp2pix(view.getContext(), 8));
ViewCompat.animate(view).setInterpolator(new FastOutSlowInInterpolator()).translationY(translationY).setDuration(150).start();
}
}
return false;
}
@Override
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull AboveSnackerLayout child, @NonNull View dependency) {
for (int i = 0, n = child.getAboveSnackViewCount(); i < n; i++) {
View view = child.getAboveSnackViewAt(i);
if (view != null) {
ViewCompat.animate(view).setInterpolator(new FastOutSlowInInterpolator()).translationY(0).setDuration(75).start();
}
}
}
}
}
@@ -1,112 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.view.ViewCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.scene.StageLayout;
import com.hippo.yorozuya.LayoutUtils;
import java.util.ArrayList;
import java.util.List;
public class EhStageLayout extends StageLayout implements CoordinatorLayout.AttachedBehavior {
private List<View> mAboveSnackViewList;
public EhStageLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public EhStageLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void addAboveSnackView(View view) {
if (null == mAboveSnackViewList) {
mAboveSnackViewList = new ArrayList<>();
}
mAboveSnackViewList.add(view);
}
public void removeAboveSnackView(View view) {
if (null == mAboveSnackViewList) {
return;
}
mAboveSnackViewList.remove(view);
}
public int getAboveSnackViewCount() {
return null == mAboveSnackViewList ? 0 : mAboveSnackViewList.size();
}
@Nullable
public View getAboveSnackViewAt(int index) {
if (null == mAboveSnackViewList || index < 0 || index >= mAboveSnackViewList.size()) {
return null;
} else {
return mAboveSnackViewList.get(index);
}
}
@NonNull
@Override
public EhStageLayout.Behavior getBehavior() {
return new EhStageLayout.Behavior();
}
public static class Behavior extends CoordinatorLayout.Behavior<EhStageLayout> {
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull EhStageLayout child, @NonNull View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, EhStageLayout child, @NonNull View dependency) {
for (int i = 0, n = child.getAboveSnackViewCount(); i < n; i++) {
View view = child.getAboveSnackViewAt(i);
if (view != null) {
float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight() - LayoutUtils.dp2pix(view.getContext(), 8));
ViewCompat.animate(view).setInterpolator(new FastOutSlowInInterpolator()).translationY(translationY).setDuration(150).start();
}
}
return false;
}
@Override
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull EhStageLayout child, @NonNull View dependency) {
for (int i = 0, n = child.getAboveSnackViewCount(); i < n; i++) {
View view = child.getAboveSnackViewAt(i);
if (view != null) {
ViewCompat.animate(view).setInterpolator(new FastOutSlowInInterpolator()).translationY(0).setDuration(75).start();
}
}
}
}
}
+34 -7
View File
@@ -25,8 +25,14 @@ import android.graphics.ImageDecoder.Source
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import coil.decode.FrameDelayRewritingSource
import com.hippo.Native
import com.hippo.UriArchiveAccessor
import com.hippo.ehviewer.EhApplication
import okio.Buffer
import okio.BufferedSource
import okio.buffer
import okio.source
import java.io.FileInputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
@@ -80,20 +86,41 @@ class Image private constructor(source: Source, private val byteBuffer: ByteBuff
@Throws(DecodeException::class)
@JvmStatic
fun decode(stream: FileInputStream): Image {
val src = ImageDecoder.createSource(
stream.channel.map(
FileChannel.MapMode.READ_ONLY, 0,
stream.available().toLong()
)
val buffer = stream.channel.map(
FileChannel.MapMode.READ_ONLY, 0,
stream.available().toLong()
)
val source = if (checkIsGif(buffer)) {
rewriteSource(stream.source().buffer())
} else {
buffer
}
val src = ImageDecoder.createSource(source)
return Image(src)
}
@Throws(DecodeException::class)
@JvmStatic
fun decode(buffer: ByteBuffer): Image {
val src = ImageDecoder.createSource(buffer)
return Image(src, buffer)
return if (checkIsGif(buffer)) {
val rewritten = rewriteSource(Buffer().apply {
write(buffer)
UriArchiveAccessor.releaseByteBuffer(buffer)
})
Image(ImageDecoder.createSource(rewritten))
} else {
Image(ImageDecoder.createSource(buffer), buffer)
}
}
private fun rewriteSource(source: BufferedSource): ByteBuffer {
val bufferedSource = FrameDelayRewritingSource(source).buffer()
return ByteBuffer.wrap(bufferedSource.use { it.readByteArray() })
}
private fun checkIsGif(buffer: ByteBuffer): Boolean {
check(buffer.isDirect)
return Native.isGif(buffer)
}
}
}
@@ -1,181 +0,0 @@
/*
* Copyright 2017 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.network
import android.content.Context
import com.hippo.yorozuya.ObjectUtils
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import java.util.regex.Pattern
open class CookieRepository(context: Context, name: String) : CookieJar {
private val db: CookieDatabase
private val map: MutableMap<String, CookieSet>
@Synchronized
fun addCookie(cookie: Cookie) {
// For cookie database
var toAdd: Cookie? = null
var toUpdate: Cookie? = null
var toRemove: Cookie? = null
var set = map[cookie.domain]
if (set == null) {
set = CookieSet()
map[cookie.domain] = set
}
if (cookie.expiresAt <= System.currentTimeMillis()) {
toRemove = set.remove(cookie)
// If the cookie is not persistent, it's not in database
if (toRemove != null && !toRemove.persistent) {
toRemove = null
}
} else {
toAdd = cookie
toUpdate = set.add(cookie)
// If the cookie is not persistent, it's not in database
if (!toAdd.persistent) toAdd = null
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
// Remove the cookie if it updates to null
if (toAdd == null && toUpdate != null) {
toRemove = toUpdate
toUpdate = null
}
}
if (toRemove != null) {
db.remove(toRemove)
}
if (toAdd != null) {
if (toUpdate != null) {
db.update(toUpdate, toAdd)
} else {
db.add(toAdd)
}
}
}
fun getCookieHeader(url: HttpUrl): String {
val cookies = getCookies(url)
val cookieHeader = StringBuilder()
var i = 0
val size = cookies.size
while (i < size) {
if (i > 0) {
cookieHeader.append("; ")
}
val cookie = cookies[i]
cookieHeader.append(cookie.name).append('=').append(cookie.value)
i++
}
return cookieHeader.toString()
}
@Synchronized
fun getCookies(url: HttpUrl): List<Cookie> {
val accepted: MutableList<Cookie> = ArrayList()
val expired: MutableList<Cookie> = ArrayList()
for ((domain, cookieSet) in map) {
if (domainMatch(url, domain)) {
cookieSet[url, accepted, expired]
}
}
for (cookie in expired) {
if (cookie.persistent) {
db.remove(cookie)
}
}
// RFC 6265 Section-5.4 step 2, sort the cookie-list
// Cookies with longer paths are listed before cookies with shorter paths.
// Ignore creation-time, we don't store them.
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
return accepted
}
fun contains(url: HttpUrl, name: String?): Boolean {
for (cookie in getCookies(url)) {
if (ObjectUtils.equal(cookie.name, name)) {
return true
}
}
return false
}
/**
* Remove all cookies in this `CookieRepository`.
*/
@Synchronized
fun clear() {
map.clear()
db.clear()
}
@Synchronized
fun close() {
db.close()
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
addCookie(cookie)
}
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return getCookies(url)
}
companion object {
/**
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
* of Android's private InetAddress#isNumeric API.
*
*
* This matches IPv6 addresses as a hex string containing at least one colon, and possibly
* including dots after the first colon. It matches IPv4 addresses as strings containing only
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
* verification).
*/
private val VERIFY_AS_IP_ADDRESS =
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
/**
* Returns true if `host` is not a host name and might be an IP address.
*/
private fun verifyAsIpAddress(host: String): Boolean {
return VERIFY_AS_IP_ADDRESS.matcher(host).matches()
}
// okhttp3.Cookie.domainMatch(HttpUrl, String)
@JvmStatic
protected fun domainMatch(url: HttpUrl, domain: String?): Boolean {
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
)
// As in 'example.com' matching 'www.example.com'.
}
}
init {
db = CookieDatabase(context, name)
map = db.allCookies
}
}
@@ -1,50 +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.scene;
import android.os.Bundle;
public final class Announcer {
Class<?> clazz;
Bundle args;
SceneFragment requestFrom;
int requestCode;
public Announcer(Class<?> clazz) {
this.clazz = clazz;
}
public Class<?> getClazz() {
return clazz;
}
public Bundle getArgs() {
return args;
}
public Announcer setArgs(Bundle args) {
this.args = args;
return this;
}
public Announcer setRequestCode(SceneFragment requestFrom, int requestCode) {
this.requestFrom = requestFrom;
this.requestCode = requestCode;
return this;
}
}
@@ -1,56 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.scene;
import android.app.Application;
import android.util.SparseArray;
import com.hippo.yorozuya.IntIdGenerator;
public abstract class SceneApplication extends Application {
private final IntIdGenerator mIdGenerator = new IntIdGenerator();
private final SparseArray<StageActivity> mStageMap = new SparseArray<>();
void registerStageActivity(StageActivity stage) {
int id = mIdGenerator.nextId();
mStageMap.put(id, stage);
stage.onRegister(id);
}
void registerStageActivity(StageActivity stage, int id) {
if (mStageMap.indexOfKey(id) >= 0) {
throw new IllegalStateException("The id exists: " + id);
}
mStageMap.put(id, stage);
stage.onRegister(id);
}
void unregisterStageActivity(int id) {
int index = mStageMap.indexOfKey(id);
if (index >= 0) {
StageActivity stage = mStageMap.valueAt(index);
mStageMap.remove(id);
stage.onUnregister();
}
}
public StageActivity findStageActivityById(int id) {
return mStageMap.get(id);
}
}
@@ -1,169 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.scene;
import android.app.assist.AssistContent;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.collect.IntList;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import rikka.core.res.ResourcesKt;
public class SceneFragment extends Fragment {
public static final int LAUNCH_MODE_STANDARD = 0;
public static final int LAUNCH_MODE_SINGLE_TOP = 1;
public static final int LAUNCH_MODE_SINGLE_TASK = 2;
/**
* Standard scene result: operation canceled.
*/
public static final int RESULT_CANCELED = 0;
/**
* Standard scene result: operation succeeded.
*/
public static final int RESULT_OK = -1;
int resultCode = RESULT_CANCELED;
Bundle result = null;
List<String> mRequestSceneTagList = new ArrayList<>(0);
IntList mRequestCodeList = new IntList(0);
public void onNewArguments(@NonNull Bundle args) {
}
public void startScene(Announcer announcer, boolean horizontal) {
FragmentActivity activity = getActivity();
if (activity instanceof StageActivity) {
((StageActivity) activity).startScene(announcer, horizontal);
}
}
public void startScene(Announcer announcer) {
FragmentActivity activity = getActivity();
if (activity instanceof StageActivity) {
((StageActivity) activity).startScene(announcer);
}
}
public void finish() {
FragmentActivity activity = getActivity();
if (activity instanceof StageActivity) {
((StageActivity) activity).finishScene(this);
}
}
public void finishStage() {
FragmentActivity activity = getActivity();
if (activity != null) {
activity.finish();
}
}
/**
* @return negative for error
*/
public int getStackIndex() {
FragmentActivity activity = getActivity();
if (activity instanceof StageActivity) {
return ((StageActivity) activity).getSceneIndex(this);
} else {
return -1;
}
}
public void onProvideAssistContent(AssistContent outContent) {
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
view.setTag(R.id.fragment_tag, getTag());
view.setBackground(ResourcesKt.resolveDrawable(requireActivity().getTheme(), android.R.attr.windowBackground));
// Notify
FragmentActivity activity = getActivity();
if (activity instanceof StageActivity) {
((StageActivity) activity).onSceneViewCreated(this, savedInstanceState);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
// Notify
FragmentActivity activity = getActivity();
if (activity instanceof StageActivity) {
((StageActivity) activity).onSceneViewDestroyed(this);
}
}
@Override
public void onDestroy() {
super.onDestroy();
FragmentActivity activity = getActivity();
if (activity instanceof StageActivity) {
((StageActivity) activity).onSceneDestroyed(this);
}
}
void addRequest(String requestSceneTag, int requestCode) {
mRequestSceneTagList.add(requestSceneTag);
mRequestCodeList.add(requestCode);
}
void returnResult(StageActivity stage) {
for (int i = 0, size = Math.min(mRequestSceneTagList.size(), mRequestCodeList.size()); i < size; i++) {
String tag = mRequestSceneTagList.get(i);
int code = mRequestCodeList.get(i);
SceneFragment scene = stage.findSceneByTag(tag);
if (scene != null) {
scene.onSceneResult(code, resultCode, result);
}
}
mRequestSceneTagList.clear();
mRequestCodeList.clear();
}
protected void onSceneResult(int requestCode, int resultCode, Bundle data) {
}
public void setResult(int resultCode, Bundle result) {
this.resultCode = resultCode;
this.result = result;
}
@IntDef({LAUNCH_MODE_STANDARD, LAUNCH_MODE_SINGLE_TOP, LAUNCH_MODE_SINGLE_TASK})
@Retention(RetentionPolicy.SOURCE)
public @interface LaunchMode {
}
}
@@ -1,655 +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.scene;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.ui.EhActivity;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.IntIdGenerator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public abstract class StageActivity extends EhActivity {
public static final String ACTION_START_SCENE = "start_scene";
public static final String KEY_SCENE_NAME = "stage_activity_scene_name";
public static final String KEY_SCENE_ARGS = "stage_activity_scene_args";
private static final String TAG = StageActivity.class.getSimpleName();
private static final String KEY_STAGE_ID = "stage_activity_stage_id";
private static final String KEY_SCENE_TAG_LIST = "stage_activity_scene_tag_list";
private static final String KEY_NEXT_ID = "stage_activity_next_id";
private static final Map<Class<?>, Integer> sLaunchModeMap = new HashMap<>();
private final ArrayList<String> mSceneTagList = new ArrayList<>();
private final ArrayList<String> mDelaySceneTagList = new ArrayList<>();
private final AtomicInteger mIdGenerator = new AtomicInteger();
private final SceneViewComparator mSceneViewComparator = new SceneViewComparator();
private final OnBackPressedCallback callback = new BackPressCallBack();
private int mStageId = IntIdGenerator.INVALID_ID;
public static void registerLaunchMode(Class<?> clazz, @SceneFragment.LaunchMode int launchMode) {
if (launchMode != SceneFragment.LAUNCH_MODE_STANDARD &&
launchMode != SceneFragment.LAUNCH_MODE_SINGLE_TOP &&
launchMode != SceneFragment.LAUNCH_MODE_SINGLE_TASK) {
throw new IllegalStateException("Invalid launch mode: " + launchMode);
}
sLaunchModeMap.put(clazz, launchMode);
}
public abstract int getContainerViewId();
/**
* @return {@code true} for start scene
*/
private boolean startSceneFromIntent(Intent intent) {
String clazzStr = intent.getStringExtra(KEY_SCENE_NAME);
if (null == clazzStr) {
return false;
}
Class<?> clazz;
try {
clazz = Class.forName(clazzStr);
} catch (ClassNotFoundException e) {
Log.e(TAG, "Can't find class " + clazzStr, e);
return false;
}
Bundle args = intent.getBundleExtra(KEY_SCENE_ARGS);
Announcer announcer = onStartSceneFromIntent(clazz, args);
if (announcer == null) {
return false;
}
startScene(announcer);
return true;
}
/**
* Start scene from {@code Intent}, it might be not safe,
* Correct it here.
*
* @return {@code null} for do not start scene
*/
@Nullable
protected Announcer onStartSceneFromIntent(@NonNull Class<?> clazz, @Nullable Bundle args) {
return new Announcer(clazz).setArgs(args);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent == null || !ACTION_START_SCENE.equals(intent.getAction()) ||
!startSceneFromIntent(intent)) {
onUnrecognizedIntent(intent);
}
}
/**
* Called when launch with action {@code android.intent.action.MAIN}
*/
@Nullable
protected abstract Announcer getLaunchAnnouncer();
/**
* Can't recognize intent in first time {@code onCreate} and {@code onNewIntent},
* null included.
*/
protected void onUnrecognizedIntent(@Nullable Intent intent) {
}
/**
* Call {@code setContentView} here. Do <b>NOT</b> call {@code startScene} here
*/
protected abstract void onCreate2(@Nullable Bundle savedInstanceState);
@Override
protected final void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getOnBackPressedDispatcher().addCallback(callback);
if (savedInstanceState != null) {
mStageId = savedInstanceState.getInt(KEY_STAGE_ID, IntIdGenerator.INVALID_ID);
ArrayList<String> list = savedInstanceState.getStringArrayList(KEY_SCENE_TAG_LIST);
if (list != null) {
mSceneTagList.addAll(list);
mDelaySceneTagList.addAll(list);
}
mIdGenerator.lazySet(savedInstanceState.getInt(KEY_NEXT_ID));
updateBackPressCallBackStatus();
}
if (mStageId == IntIdGenerator.INVALID_ID) {
((SceneApplication) getApplicationContext()).registerStageActivity(this);
} else {
((SceneApplication) getApplicationContext()).registerStageActivity(this, mStageId);
}
// Create layout
onCreate2(savedInstanceState);
Intent intent = getIntent();
if (savedInstanceState == null) {
if (intent != null) {
String action = intent.getAction();
if (Intent.ACTION_MAIN.equals(action)) {
Announcer announcer = getLaunchAnnouncer();
if (announcer != null) {
startScene(announcer);
return;
}
} else if (ACTION_START_SCENE.equals(action)) {
if (startSceneFromIntent(intent)) {
return;
}
}
}
// Can't recognize intent
onUnrecognizedIntent(intent);
}
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_STAGE_ID, mStageId);
outState.putStringArrayList(KEY_SCENE_TAG_LIST, mSceneTagList);
outState.putInt(KEY_NEXT_ID, mIdGenerator.getAndIncrement());
}
@Override
protected void onDestroy() {
super.onDestroy();
((SceneApplication) getApplicationContext()).unregisterStageActivity(mStageId);
}
public void onSceneViewCreated(SceneFragment scene, Bundle savedInstanceState) {
}
public void onSceneViewDestroyed(SceneFragment scene) {
}
void onSceneDestroyed(SceneFragment scene) {
mDelaySceneTagList.remove(scene.getTag());
}
protected void onRegister(int id) {
mStageId = id;
}
protected void onUnregister() {
}
protected void onTransactScene() {
}
public int getStageId() {
return mStageId;
}
public int getSceneCount() {
return mSceneTagList.size();
}
public int getSceneLaunchMode(Class<?> clazz) {
Integer integer = sLaunchModeMap.get(clazz);
if (integer == null) {
throw new RuntimeException("Not register " + clazz.getName());
} else {
return integer;
}
}
private SceneFragment newSceneInstance(Class<?> clazz) {
try {
return (SceneFragment) clazz.newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException("Can't instance " + clazz.getName(), e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("The constructor of " +
clazz.getName() + " is not visible", e);
} catch (ClassCastException e) {
throw new IllegalStateException(clazz.getName() + " can not cast to scene", e);
}
}
public void startScene(Announcer announcer) {
startScene(announcer, false);
}
public void startScene(Announcer announcer, boolean horizontal) {
startSceneInternal(announcer, horizontal);
updateBackPressCallBackStatus();
}
public void startSceneInternal(Announcer announcer, boolean horizontal) {
Class<?> clazz = announcer.clazz;
Bundle args = announcer.args;
FragmentManager fragmentManager = getSupportFragmentManager();
int launchMode = getSceneLaunchMode(clazz);
// Check LAUNCH_MODE_SINGLE_TASK
if (launchMode == SceneFragment.LAUNCH_MODE_SINGLE_TASK) {
for (int i = 0, n = mSceneTagList.size(); i < n; i++) {
String tag = mSceneTagList.get(i);
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment == null) {
Log.e(TAG, "Can't find fragment with tag: " + tag);
continue;
}
if (clazz.isInstance(fragment)) { // Get it
FragmentTransaction transaction = fragmentManager.beginTransaction();
// Use default animation
if (horizontal) {
transaction.setCustomAnimations(R.anim.scene_open_enter_horizontal, R.anim.scene_open_exit);
} else {
transaction.setCustomAnimations(R.anim.scene_open_enter, R.anim.scene_open_exit);
}
// Remove top fragments
for (int j = i + 1; j < n; j++) {
String topTag = mSceneTagList.get(j);
Fragment topFragment = fragmentManager.findFragmentByTag(topTag);
if (null == topFragment) {
Log.e(TAG, "Can't find fragment with tag: " + topTag);
continue;
}
// Clear shared element
topFragment.setSharedElementEnterTransition(null);
topFragment.setSharedElementReturnTransition(null);
topFragment.setEnterTransition(null);
topFragment.setExitTransition(null);
// Remove it
transaction.remove(topFragment);
}
// Remove tag from index i+1
mSceneTagList.subList(i + 1, mSceneTagList.size()).clear();
mDelaySceneTagList.subList(i + 1, mDelaySceneTagList.size()).clear();
// Attach fragment
if (fragment.isDetached()) {
transaction.attach(fragment);
}
// Commit
transaction.commitAllowingStateLoss();
onTransactScene();
// New arguments
if (args != null && fragment instanceof SceneFragment) {
// TODO Call onNewArguments when view created ?
((SceneFragment) fragment).onNewArguments(args);
}
return;
}
}
}
// Get current fragment
SceneFragment currentScene = null;
if (mSceneTagList.size() > 0) {
// Get last tag
String tag = mSceneTagList.get(mSceneTagList.size() - 1);
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment != null) {
AssertUtils.assertTrue(fragment instanceof SceneFragment);
currentScene = (SceneFragment) fragment;
}
}
// Check LAUNCH_MODE_SINGLE_TASK
if (clazz.isInstance(currentScene) && launchMode == SceneFragment.LAUNCH_MODE_SINGLE_TOP) {
if (args != null) {
currentScene.onNewArguments(args);
}
return;
}
// Create new scene
SceneFragment newScene = newSceneInstance(clazz);
newScene.setArguments(args);
// Create new scene tag
String newTag = Integer.toString(mIdGenerator.getAndIncrement());
// Add new tag to list
mSceneTagList.add(newTag);
mDelaySceneTagList.add(newTag);
FragmentTransaction transaction = fragmentManager.beginTransaction();
// Animation
if (currentScene != null) {
// Clear shared item
currentScene.setSharedElementEnterTransition(null);
currentScene.setSharedElementReturnTransition(null);
currentScene.setEnterTransition(null);
currentScene.setExitTransition(null);
newScene.setSharedElementEnterTransition(null);
newScene.setSharedElementReturnTransition(null);
newScene.setEnterTransition(null);
newScene.setExitTransition(null);
// Set default animation
if (horizontal) {
transaction.setCustomAnimations(R.anim.scene_open_enter_horizontal, R.anim.scene_open_exit);
} else {
transaction.setCustomAnimations(R.anim.scene_open_enter, R.anim.scene_open_exit);
}
// Detach current scene
if (!currentScene.isDetached()) {
transaction.detach(currentScene);
} else {
Log.e(TAG, "Current scene is detached");
}
}
// Add new scene
transaction.add(getContainerViewId(), newScene, newTag);
// Commit
transaction.commitAllowingStateLoss();
onTransactScene();
// Check request
if (announcer.requestFrom != null) {
newScene.addRequest(announcer.requestFrom.getTag(), announcer.requestCode);
}
}
public void startSceneFirstly(Announcer announcer) {
Class<?> clazz = announcer.clazz;
Bundle args = announcer.args;
FragmentManager fragmentManager = getSupportFragmentManager();
int launchMode = getSceneLaunchMode(clazz);
boolean forceNewScene = launchMode == SceneFragment.LAUNCH_MODE_STANDARD;
boolean createNewScene = true;
boolean findScene = false;
SceneFragment scene = null;
FragmentTransaction transaction = fragmentManager.beginTransaction();
// Set default animation
transaction.setCustomAnimations(R.anim.scene_open_enter, R.anim.scene_open_exit);
String findSceneTag = null;
for (int i = 0, n = mSceneTagList.size(); i < n; i++) {
String tag = mSceneTagList.get(i);
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment == null) {
Log.e(TAG, "Can't find fragment with tag: " + tag);
continue;
}
// Clear shared element
fragment.setSharedElementEnterTransition(null);
fragment.setSharedElementReturnTransition(null);
fragment.setEnterTransition(null);
fragment.setExitTransition(null);
// Check is target scene
if (!forceNewScene && !findScene && clazz.isInstance(fragment) &&
(launchMode == SceneFragment.LAUNCH_MODE_SINGLE_TASK || !fragment.isDetached())) {
scene = (SceneFragment) fragment;
findScene = true;
createNewScene = false;
findSceneTag = tag;
if (fragment.isDetached()) {
transaction.attach(fragment);
}
} else {
// Remove it
transaction.remove(fragment);
}
}
// Handle tag list
mSceneTagList.clear();
if (null != findSceneTag) {
mSceneTagList.add(findSceneTag);
}
if (createNewScene) {
scene = newSceneInstance(clazz);
scene.setArguments(args);
// Create scene tag
String tag = Integer.toString(mIdGenerator.getAndIncrement());
// Add tag to list
mSceneTagList.add(tag);
mDelaySceneTagList.add(tag);
// Add scene
transaction.add(getContainerViewId(), scene, tag);
}
// Commit
transaction.commitAllowingStateLoss();
onTransactScene();
if (!createNewScene && args != null) {
// TODO Call onNewArguments when view created ?
scene.onNewArguments(args);
}
updateBackPressCallBackStatus();
}
int getSceneIndex(SceneFragment scene) {
return getTagIndex(scene.getTag());
}
int getTagIndex(String tag) {
return mSceneTagList.indexOf(tag);
}
void sortSceneViews(List<View> views) {
views.sort(mSceneViewComparator);
}
public void finishScene(SceneFragment scene) {
finishScene(scene.getTag());
updateBackPressCallBackStatus();
}
private void finishScene(String tag) {
FragmentManager fragmentManager = getSupportFragmentManager();
// Get scene
Fragment scene = fragmentManager.findFragmentByTag(tag);
if (scene == null) {
Log.e(TAG, "finishScene: Can't find scene by tag: " + tag);
return;
}
// Get scene index
int index = mSceneTagList.indexOf(tag);
if (index < 0) {
Log.e(TAG, "finishScene: Can't find the tag in tag list: " + tag);
return;
}
if (mSceneTagList.size() == 1) {
// It is the last fragment, finish Activity now
Log.i(TAG, "finishScene: It is the last scene, finish activity now");
finish();
return;
}
Fragment next = null;
if (index == mSceneTagList.size() - 1) {
// It is first fragment, show the next one
next = fragmentManager.findFragmentByTag(mSceneTagList.get(index - 1));
}
FragmentTransaction transaction = fragmentManager.beginTransaction();
if (next != null) {
// Clear shared item
scene.setSharedElementEnterTransition(null);
scene.setSharedElementReturnTransition(null);
scene.setEnterTransition(null);
scene.setExitTransition(null);
next.setSharedElementEnterTransition(null);
next.setSharedElementReturnTransition(null);
next.setEnterTransition(null);
next.setExitTransition(null);
// Do not show animate if it is not the first fragment
transaction.setCustomAnimations(R.anim.scene_close_enter, R.anim.scene_close_exit);
// Attach fragment
transaction.attach(next);
}
transaction.remove(scene);
transaction.commitAllowingStateLoss();
onTransactScene();
// Remove tag
mSceneTagList.remove(index);
// Return result
if (scene instanceof SceneFragment) {
((SceneFragment) scene).returnResult(this);
}
}
public void refreshTopScene() {
int index = mSceneTagList.size() - 1;
if (index < 0) {
return;
}
String tag = mSceneTagList.get(index);
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment == null) {
return;
}
fragmentManager.beginTransaction().detach(fragment).commitAllowingStateLoss();
fragmentManager.beginTransaction().attach(fragment).commitAllowingStateLoss();
updateBackPressCallBackStatus();
}
public void updateBackPressCallBackStatus() {
callback.setEnabled(mSceneTagList.size() > 1);
}
@Override
public void onProvideAssistContent(AssistContent outContent) {
super.onProvideAssistContent(outContent);
int size = mSceneTagList.size();
String tag = mSceneTagList.get(size - 1);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (fragment == null) {
Log.e(TAG, "onProvideAssistContent: Can't find scene by tag: " + tag);
return;
}
if (fragment instanceof SceneFragment scene) {
scene.onProvideAssistContent(outContent);
} else {
Log.e(TAG, "onProvideAssistContent: The fragment is not SceneFragment");
}
}
public SceneFragment findSceneByTag(String tag) {
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment != null) {
return (SceneFragment) fragment;
} else {
return null;
}
}
@Nullable
public Class<?> getTopSceneClass() {
int index = mSceneTagList.size() - 1;
if (index < 0) {
return null;
}
String tag = mSceneTagList.get(index);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (null == fragment) {
return null;
}
return fragment.getClass();
}
class BackPressCallBack extends OnBackPressedCallback {
public BackPressCallBack() {
super(false);
}
@Override
public void handleOnBackPressed() {
int size = mSceneTagList.size();
String tag = mSceneTagList.get(size - 1);
SceneFragment scene;
Fragment fragment = getSupportFragmentManager().findFragmentByTag(tag);
if (fragment == null) {
Log.e(TAG, "onBackPressed: Can't find scene by tag: " + tag);
return;
}
if (!(fragment instanceof SceneFragment)) {
Log.e(TAG, "onBackPressed: The fragment is not SceneFragment");
return;
}
scene = (SceneFragment) fragment;
finishScene(scene);
}
}
private final class SceneViewComparator implements Comparator<View> {
private int getIndex(View view) {
Object o = view.getTag(R.id.fragment_tag);
if (o instanceof String) {
return mDelaySceneTagList.indexOf(o);
} else {
return -1;
}
}
@Override
public int compare(View lhs, View rhs) {
return getIndex(lhs) - getIndex(rhs);
}
}
}
@@ -1,156 +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.scene;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.hippo.ehviewer.R;
import java.lang.reflect.Field;
import java.util.ArrayList;
public class StageLayout extends FrameLayout {
private Field mDisappearingChildrenField;
private ArrayList<View> mSuperDisappearingChildren;
private ArrayList<View> mSortedScenes;
private View mDumpView;
private boolean mDoTrick;
public StageLayout(Context context) {
super(context);
init(context);
}
public StageLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public StageLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
try {
mDisappearingChildrenField = ViewGroup.class.getDeclaredField("mDisappearingChildren");
mDisappearingChildrenField.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
if (mDisappearingChildrenField != null) {
mDumpView = new View(context);
addView(mDumpView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
}
@SuppressWarnings("unchecked")
private void getSuperDisappearingChildren() {
if (mDisappearingChildrenField == null || mSuperDisappearingChildren != null) {
return;
}
try {
mSuperDisappearingChildren = (ArrayList<View>) mDisappearingChildrenField.get(this);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private boolean beforeDispatchDraw() {
getSuperDisappearingChildren();
if (mSuperDisappearingChildren == null ||
mSuperDisappearingChildren.size() <= 0 || getChildCount() <= 1) { // only dump view
return false;
}
// Get stage
StageActivity stage = null;
Context context = getContext();
if (context instanceof StageActivity) {
stage = (StageActivity) context;
}
if (stage == null) {
return false;
}
if (null == mSortedScenes) {
mSortedScenes = new ArrayList<>();
}
// Add all scene view to mSortedScenes
ArrayList<View> disappearingChildren = mSuperDisappearingChildren;
ArrayList<View> sortedScenes = mSortedScenes;
for (int i = 1, n = getChildCount(); i < n; i++) { // Skip dump view
View view = getChildAt(i);
if (null != view.getTag(R.id.fragment_tag)) {
sortedScenes.add(view);
}
}
for (int i = 0, n = disappearingChildren.size(); i < n; i++) {
View view = disappearingChildren.get(i);
if (null != view.getTag(R.id.fragment_tag)) {
sortedScenes.add(view);
}
}
stage.sortSceneViews(sortedScenes);
return true;
}
private void afterDispatchDraw() {
if (null != mSortedScenes) {
mSortedScenes.clear();
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
mDoTrick = beforeDispatchDraw();
super.dispatchDraw(canvas);
afterDispatchDraw();
mDoTrick = false;
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
if (mDoTrick) {
ArrayList<View> sortedScenes = mSortedScenes;
if (child == mDumpView) {
boolean more = false;
for (int i = 0, n = sortedScenes.size(); i < n; i++) {
more |= super.drawChild(canvas, sortedScenes.get(i), drawingTime);
}
return more;
} else if (sortedScenes.contains(child)) {
// Skip
return false;
}
}
return super.drawChild(canvas, child, drawingTime);
}
}
@@ -38,7 +38,12 @@ infix fun Context.tellClipboard(text: String?) {
addTextToClipboard(text, false)
}
fun Context.addTextToClipboard(text: String?, isSensitive: Boolean) {
infix fun Context.tellClipboardWithToast(text: String?) {
addTextToClipboard(text, false, true)
}
@JvmOverloads
fun Context.addTextToClipboard(text: String?, isSensitive: Boolean, useToast: Boolean = false) {
getClipboardManager().apply {
setPrimaryClip(ClipData.newPlainText(null, text).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && isSensitive)
@@ -50,7 +55,7 @@ fun Context.addTextToClipboard(text: String?, isSensitive: Boolean) {
// Avoid double notify user since system have done that on Tiramisu above
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
if (this is MainActivity) {
showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT)
showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT, useToast)
} else if (this is SettingsActivity) {
showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT)
}
@@ -1,47 +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.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.drakeet.drawer.FullDraggableContainer;
public class IgnoreFitsSystemWindowsFullyDraggableDrawerContentLayout extends FullDraggableContainer {
public IgnoreFitsSystemWindowsFullyDraggableDrawerContentLayout(@NonNull Context context) {
super(context);
}
public IgnoreFitsSystemWindowsFullyDraggableDrawerContentLayout(@NonNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs) {
super(context, attrs);
}
public IgnoreFitsSystemWindowsFullyDraggableDrawerContentLayout(@NonNull Context context, @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
return insets;
}
}
@@ -174,7 +174,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
(pageView as? SubsamplingScaleImageView)?.let { view ->
RectF().let {
view.getPanRemaining(it)
return fn(it) > 0
return fn(it) > 1
}
}
return false
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<alpha
android:duration="200"
android:fillAfter="true"
android:fillBefore="false"
android:fillEnabled="true"
android:fromAlpha="0.0"
android:interpolator="@anim/decelerate_quart"
android:toAlpha="1.0" />
<translate
android:duration="350"
android:fillAfter="true"
android:fillBefore="true"
android:fillEnabled="true"
android:fromXDelta="8%"
android:interpolator="@anim/decelerate_quint"
android:toXDelta="0" />
</set>
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more